@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.
Files changed (296) 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 +538 -0
  33. package/packages/server/dist/config.js +625 -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/environment-prompt.d.ts +52 -0
  37. package/packages/server/dist/environment-prompt.js +61 -0
  38. package/packages/server/dist/event-bus.d.ts +87 -0
  39. package/packages/server/dist/event-bus.js +48 -0
  40. package/packages/server/dist/git.d.ts +128 -0
  41. package/packages/server/dist/git.js +446 -0
  42. package/packages/server/dist/github-auth.d.ts +44 -0
  43. package/packages/server/dist/github-auth.js +169 -0
  44. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  45. package/packages/server/dist/herdctl-agent-config.js +331 -0
  46. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  47. package/packages/server/dist/herdctl-agent-names.js +228 -0
  48. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  49. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  50. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  51. package/packages/server/dist/herdctl-jobs.js +225 -0
  52. package/packages/server/dist/herdctl.d.ts +623 -0
  53. package/packages/server/dist/herdctl.js +1279 -0
  54. package/packages/server/dist/hook-config.d.ts +217 -0
  55. package/packages/server/dist/hook-config.js +224 -0
  56. package/packages/server/dist/http-bytes.d.ts +24 -0
  57. package/packages/server/dist/http-bytes.js +52 -0
  58. package/packages/server/dist/index.d.ts +1 -0
  59. package/packages/server/dist/index.js +14 -0
  60. package/packages/server/dist/instance-config.d.ts +143 -0
  61. package/packages/server/dist/instance-config.js +385 -0
  62. package/packages/server/dist/localcommand.d.ts +23 -0
  63. package/packages/server/dist/localcommand.js +140 -0
  64. package/packages/server/dist/management-auth.d.ts +99 -0
  65. package/packages/server/dist/management-auth.js +165 -0
  66. package/packages/server/dist/management-config.d.ts +164 -0
  67. package/packages/server/dist/management-config.js +286 -0
  68. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  69. package/packages/server/dist/management-mcp-server.js +95 -0
  70. package/packages/server/dist/management-metadata.d.ts +67 -0
  71. package/packages/server/dist/management-metadata.js +56 -0
  72. package/packages/server/dist/management-ops.d.ts +110 -0
  73. package/packages/server/dist/management-ops.js +449 -0
  74. package/packages/server/dist/management-policy.d.ts +172 -0
  75. package/packages/server/dist/management-policy.js +259 -0
  76. package/packages/server/dist/message-provenance.d.ts +112 -0
  77. package/packages/server/dist/message-provenance.js +260 -0
  78. package/packages/server/dist/models.d.ts +164 -0
  79. package/packages/server/dist/models.js +208 -0
  80. package/packages/server/dist/openapi.d.ts +66 -0
  81. package/packages/server/dist/openapi.js +183 -0
  82. package/packages/server/dist/parent-detach.d.ts +30 -0
  83. package/packages/server/dist/parent-detach.js +109 -0
  84. package/packages/server/dist/preload.d.ts +43 -0
  85. package/packages/server/dist/preload.js +55 -0
  86. package/packages/server/dist/project-files.d.ts +76 -0
  87. package/packages/server/dist/project-files.js +191 -0
  88. package/packages/server/dist/project-mime.d.ts +35 -0
  89. package/packages/server/dist/project-mime.js +55 -0
  90. package/packages/server/dist/project-paths.d.ts +88 -0
  91. package/packages/server/dist/project-paths.js +126 -0
  92. package/packages/server/dist/project-types.d.ts +316 -0
  93. package/packages/server/dist/project-types.js +48 -0
  94. package/packages/server/dist/projects.d.ts +202 -0
  95. package/packages/server/dist/projects.js +894 -0
  96. package/packages/server/dist/queued-message.d.ts +35 -0
  97. package/packages/server/dist/queued-message.js +116 -0
  98. package/packages/server/dist/read-state.d.ts +46 -0
  99. package/packages/server/dist/read-state.js +139 -0
  100. package/packages/server/dist/recovery-config.d.ts +111 -0
  101. package/packages/server/dist/recovery-config.js +105 -0
  102. package/packages/server/dist/recovery.d.ts +262 -0
  103. package/packages/server/dist/recovery.js +490 -0
  104. package/packages/server/dist/resume-drain.d.ts +122 -0
  105. package/packages/server/dist/resume-drain.js +158 -0
  106. package/packages/server/dist/route-context.d.ts +168 -0
  107. package/packages/server/dist/route-context.js +39 -0
  108. package/packages/server/dist/route-errors.d.ts +8 -0
  109. package/packages/server/dist/route-errors.js +13 -0
  110. package/packages/server/dist/routes/chats.d.ts +12 -0
  111. package/packages/server/dist/routes/chats.js +1406 -0
  112. package/packages/server/dist/routes/git.d.ts +13 -0
  113. package/packages/server/dist/routes/git.js +236 -0
  114. package/packages/server/dist/routes/mcp.d.ts +83 -0
  115. package/packages/server/dist/routes/mcp.js +250 -0
  116. package/packages/server/dist/routes/meta.d.ts +15 -0
  117. package/packages/server/dist/routes/meta.js +447 -0
  118. package/packages/server/dist/routes/projects.d.ts +25 -0
  119. package/packages/server/dist/routes/projects.js +736 -0
  120. package/packages/server/dist/routes/triggers.d.ts +21 -0
  121. package/packages/server/dist/routes/triggers.js +324 -0
  122. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  123. package/packages/server/dist/routes/workspace-mount.js +32 -0
  124. package/packages/server/dist/routes.d.ts +26 -0
  125. package/packages/server/dist/routes.js +29 -0
  126. package/packages/server/dist/run-provenance.d.ts +109 -0
  127. package/packages/server/dist/run-provenance.js +206 -0
  128. package/packages/server/dist/runs.d.ts +92 -0
  129. package/packages/server/dist/runs.js +72 -0
  130. package/packages/server/dist/schedule-config.d.ts +68 -0
  131. package/packages/server/dist/schedule-config.js +75 -0
  132. package/packages/server/dist/schedule-session.d.ts +34 -0
  133. package/packages/server/dist/schedule-session.js +120 -0
  134. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  135. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  136. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  137. package/packages/server/dist/self-mcp-read.js +97 -0
  138. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  139. package/packages/server/dist/self-mcp-triggers.js +144 -0
  140. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  141. package/packages/server/dist/self-mcp-types.js +10 -0
  142. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  143. package/packages/server/dist/self-mcp-util.js +157 -0
  144. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  145. package/packages/server/dist/self-mcp-write.js +215 -0
  146. package/packages/server/dist/self-mcp.d.ts +95 -0
  147. package/packages/server/dist/self-mcp.js +448 -0
  148. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  149. package/packages/server/dist/send-file-mcp.js +281 -0
  150. package/packages/server/dist/session-hub.d.ts +186 -0
  151. package/packages/server/dist/session-hub.js +291 -0
  152. package/packages/server/dist/spawn-capability.d.ts +99 -0
  153. package/packages/server/dist/spawn-capability.js +106 -0
  154. package/packages/server/dist/star.d.ts +26 -0
  155. package/packages/server/dist/star.js +94 -0
  156. package/packages/server/dist/start.d.ts +1 -0
  157. package/packages/server/dist/start.js +21 -0
  158. package/packages/server/dist/subagents.d.ts +337 -0
  159. package/packages/server/dist/subagents.js +626 -0
  160. package/packages/server/dist/sweep.d.ts +122 -0
  161. package/packages/server/dist/sweep.js +696 -0
  162. package/packages/server/dist/tooldetails.d.ts +33 -0
  163. package/packages/server/dist/tooldetails.js +466 -0
  164. package/packages/server/dist/transcribe.d.ts +69 -0
  165. package/packages/server/dist/transcribe.js +119 -0
  166. package/packages/server/dist/transcripts.d.ts +42 -0
  167. package/packages/server/dist/transcripts.js +164 -0
  168. package/packages/server/dist/trigger-config.d.ts +278 -0
  169. package/packages/server/dist/trigger-config.js +417 -0
  170. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  171. package/packages/server/dist/trigger-runtime.js +82 -0
  172. package/packages/server/dist/trigger-session.d.ts +34 -0
  173. package/packages/server/dist/trigger-session.js +122 -0
  174. package/packages/server/dist/triggers.d.ts +83 -0
  175. package/packages/server/dist/triggers.js +124 -0
  176. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  177. package/packages/server/dist/trusted-proxy.js +157 -0
  178. package/packages/server/dist/turn-notice.d.ts +154 -0
  179. package/packages/server/dist/turn-notice.js +271 -0
  180. package/packages/server/dist/unread.d.ts +36 -0
  181. package/packages/server/dist/unread.js +123 -0
  182. package/packages/server/dist/usage.d.ts +66 -0
  183. package/packages/server/dist/usage.js +243 -0
  184. package/packages/server/dist/wake-injection.d.ts +156 -0
  185. package/packages/server/dist/wake-injection.js +136 -0
  186. package/packages/server/dist/whisper-local.d.ts +9 -0
  187. package/packages/server/dist/whisper-local.js +122 -0
  188. package/packages/server/dist/ws-context.d.ts +158 -0
  189. package/packages/server/dist/ws-context.js +1 -0
  190. package/packages/server/dist/ws-protocol.d.ts +423 -0
  191. package/packages/server/dist/ws-protocol.js +251 -0
  192. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  193. package/packages/server/dist/ws-self-mcp.js +28 -0
  194. package/packages/server/dist/ws-triggers.d.ts +29 -0
  195. package/packages/server/dist/ws-triggers.js +261 -0
  196. package/packages/server/dist/ws-turn.d.ts +86 -0
  197. package/packages/server/dist/ws-turn.js +751 -0
  198. package/packages/server/dist/ws.d.ts +62 -0
  199. package/packages/server/dist/ws.js +1024 -0
  200. package/packages/server/package.json +6 -0
  201. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  202. package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
  203. package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
  204. package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
  205. package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
  206. package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
  207. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
  208. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  209. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
  210. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  211. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
  212. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  213. package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
  214. package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
  215. package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
  216. package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
  217. package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
  218. package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
  219. package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
  220. package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
  221. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  222. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  223. package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
  224. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
  225. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  226. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  227. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  228. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  229. package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
  230. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  231. package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
  232. package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
  233. package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
  234. package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
  235. package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
  236. package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
  237. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
  238. package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
  239. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
  240. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
  241. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  242. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  243. package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
  244. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  245. package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
  246. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  247. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  248. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
  249. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  250. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  251. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
  252. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  253. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
  254. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  255. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  256. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  257. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  258. package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
  259. package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
  260. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
  261. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  262. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  263. package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
  264. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  265. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
  266. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  267. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
  268. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  269. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  270. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
  271. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  272. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
  273. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  274. package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
  275. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  276. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  277. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
  278. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  279. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
  280. package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
  281. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
  282. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  283. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
  284. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  285. package/packages/web/dist/favicon.ico +0 -0
  286. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  287. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  288. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  289. package/packages/web/dist/icons/favicon-16.png +0 -0
  290. package/packages/web/dist/icons/favicon-32.png +0 -0
  291. package/packages/web/dist/icons/icon-192.png +0 -0
  292. package/packages/web/dist/icons/icon-512.png +0 -0
  293. package/packages/web/dist/icons/maskable-512.png +0 -0
  294. package/packages/web/dist/index.html +56 -0
  295. package/packages/web/dist/manifest.webmanifest +32 -0
  296. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ParentDetachStore — a Paddock-side sidecar recording which chats the user has
3
+ * explicitly DETACHED from their parent (#508).
4
+ *
5
+ * The nested chat list (#485) draws a chat under its parent, and that edge is
6
+ * resolved in two tiers (see `makeParentResolver`): the RECORDED
7
+ * `RunProvenance.parentSessionId`, and — for chats created before that field
8
+ * existed — an edge INFERRED from who injected the kickoff message. Detach
9
+ * therefore cannot be implemented by clearing the recorded edge: the inference
10
+ * tier would simply re-derive the old parent on the next page load. It needs an
11
+ * explicit, persisted "this chat is a root, whatever the tiers say" sentinel that
12
+ * is checked AHEAD of both.
13
+ *
14
+ * That sentinel lives here rather than on {@link RunProvenance}, which #508
15
+ * suggested. Provenance is documented — and implemented — as write-once creation
16
+ * metadata: "stamped once, at creation, and NEVER clobbered by a later turn".
17
+ * Detach is a user decision made long after creation, and a chat with no recorded
18
+ * provenance at all (every pre-#485 chat, i.e. exactly the population whose parent
19
+ * is inferred) would have needed an `origin`/`depth` fabricated for it just to
20
+ * carry the flag — which would then show a "spawned" badge the chat never earned.
21
+ * A separate flag keeps provenance honest and detach cheap.
22
+ *
23
+ * Shape/pattern is a near-verbatim copy of {@link ArchiveStore} — a tiny JSON
24
+ * array sidecar in the data dir, lazy-loaded, write-through, serialised,
25
+ * corruption-tolerant. Keyed by `<agent>\0<sessionId>` (NUL-separated) like
26
+ * archive/star, and SHARED rather than per-user: detaching restructures the list
27
+ * everyone sees, so it is not personal state the way read/unread is.
28
+ */
29
+ import { promises as fs } from "node:fs";
30
+ import path from "node:path";
31
+ const STATE_FILE = "parent-detach.json";
32
+ /** Separator in a storage key; a NUL can't occur in an agent name or a UUID. */
33
+ const KEY_SEP = "\u0000";
34
+ /** Compose the storage key for an (agent, session) pair. */
35
+ function keyOf(agent, sessionId) {
36
+ return `${agent}${KEY_SEP}${sessionId}`;
37
+ }
38
+ export class ParentDetachStore {
39
+ stateFile;
40
+ /** In-memory set of detached keys (loaded once, written through on change). */
41
+ detached = null;
42
+ /**
43
+ * The in-flight load, cached so concurrent first-callers share ONE read — the
44
+ * same lost-update guard {@link ArchiveStore} carries.
45
+ */
46
+ loadPromise = null;
47
+ /** Serialises concurrent writes so the file never interleaves. */
48
+ writing = Promise.resolve();
49
+ constructor(dataDir) {
50
+ this.stateFile = path.join(dataDir, STATE_FILE);
51
+ }
52
+ /** Load the persisted set (lazily, deduped; tolerant of a missing/corrupt file). */
53
+ ensureLoaded() {
54
+ if (this.detached)
55
+ return Promise.resolve(this.detached);
56
+ if (!this.loadPromise) {
57
+ this.loadPromise = (async () => {
58
+ let keys = [];
59
+ try {
60
+ const raw = await fs.readFile(this.stateFile, "utf8");
61
+ const parsed = JSON.parse(raw);
62
+ if (Array.isArray(parsed))
63
+ keys = parsed.filter((k) => typeof k === "string");
64
+ }
65
+ catch {
66
+ /* missing or unreadable — start empty */
67
+ }
68
+ this.detached = new Set(keys);
69
+ return this.detached;
70
+ })();
71
+ }
72
+ return this.loadPromise;
73
+ }
74
+ /**
75
+ * Has this chat been detached from its parent? Non-throwing: any load error
76
+ * reads as false, so a corrupt sidecar degrades to "nesting as recorded"
77
+ * rather than flattening the whole list.
78
+ */
79
+ async isDetached(agent, sessionId) {
80
+ const set = await this.ensureLoaded().catch(() => new Set());
81
+ return set.has(keyOf(agent, sessionId));
82
+ }
83
+ /**
84
+ * Set (or clear) a chat's detached flag, persisting the change. Idempotent;
85
+ * returns whether the flag actually CHANGED. Clearing it re-exposes whatever
86
+ * the two resolver tiers say — which is what makes re-attach free: nothing was
87
+ * ever destroyed, only overridden.
88
+ */
89
+ async setDetached(agent, sessionId, detached) {
90
+ const set = await this.ensureLoaded();
91
+ const key = keyOf(agent, sessionId);
92
+ if (detached === set.has(key))
93
+ return false; // no-op — avoid a needless write
94
+ if (detached)
95
+ set.add(key);
96
+ else
97
+ set.delete(key);
98
+ await this.persist(set);
99
+ return true;
100
+ }
101
+ /** Write-through, serialised so overlapping toggles can't corrupt the file. */
102
+ persist(set) {
103
+ this.writing = this.writing.then(async () => {
104
+ const json = JSON.stringify([...set], null, 2);
105
+ await fs.writeFile(this.stateFile, json, "utf8").catch(() => undefined);
106
+ });
107
+ return this.writing;
108
+ }
109
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The project-context "preload" wrapper (issue #1) — single-sourced here so the
3
+ * WS layer that BUILDS it and the chat-list that STRIPS it for display can't
4
+ * drift (issue #62).
5
+ *
6
+ * On the first turn of a new project chat with preload enabled, the server
7
+ * prepends the project's curated context — OVERVIEW.md (current state) plus
8
+ * CHANGELOG.md (cross-session history, issue #188) — to the user's message as a
9
+ * delimited block:
10
+ *
11
+ * <project-context>
12
+ * …OVERVIEW.md…
13
+ *
14
+ * …CHANGELOG.md…
15
+ * </project-context>
16
+ *
17
+ * My request:
18
+ * <the user's actual message>
19
+ *
20
+ * That block becomes the session's first user message, so Claude Code's derived
21
+ * preview (and thus the sidebar name) is the context text, not the request —
22
+ * exactly the #62 complaint. {@link stripPreloadWrapper} recovers the request.
23
+ */
24
+ export declare const PRELOAD_CONTEXT_OPEN = "<project-context>";
25
+ /** The literal boundary between the context block and the user's real request. */
26
+ export declare const PRELOAD_REQUEST_MARKER = "</project-context>\n\nMy request:\n";
27
+ /**
28
+ * Assemble the preload context body from the project's curated docs (issue
29
+ * #188). OVERVIEW.md carries current state; CHANGELOG.md carries the
30
+ * cross-session narrative — both reach the chat so the history is no longer
31
+ * write-only. Each doc self-labels via its own `#` heading, so they are joined
32
+ * with a blank line; empty docs are dropped.
33
+ */
34
+ export declare function composePreloadContext(overview: string, changelog: string): string;
35
+ /** Build the preload-wrapped prompt for a new project chat's first turn. */
36
+ export declare function wrapPreload(context: string, message: string): string;
37
+ /**
38
+ * Recover the user's real request from a (possibly) preload-wrapped message. If
39
+ * `text` isn't the wrapper it's returned unchanged; if it is, everything after
40
+ * the `My request:` marker is returned. Returns the input unchanged when the
41
+ * marker is absent (e.g. the wrapper was truncated before it).
42
+ */
43
+ export declare function stripPreloadWrapper(text: string): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The project-context "preload" wrapper (issue #1) — single-sourced here so the
3
+ * WS layer that BUILDS it and the chat-list that STRIPS it for display can't
4
+ * drift (issue #62).
5
+ *
6
+ * On the first turn of a new project chat with preload enabled, the server
7
+ * prepends the project's curated context — OVERVIEW.md (current state) plus
8
+ * CHANGELOG.md (cross-session history, issue #188) — to the user's message as a
9
+ * delimited block:
10
+ *
11
+ * <project-context>
12
+ * …OVERVIEW.md…
13
+ *
14
+ * …CHANGELOG.md…
15
+ * </project-context>
16
+ *
17
+ * My request:
18
+ * <the user's actual message>
19
+ *
20
+ * That block becomes the session's first user message, so Claude Code's derived
21
+ * preview (and thus the sidebar name) is the context text, not the request —
22
+ * exactly the #62 complaint. {@link stripPreloadWrapper} recovers the request.
23
+ */
24
+ export const PRELOAD_CONTEXT_OPEN = "<project-context>";
25
+ /** The literal boundary between the context block and the user's real request. */
26
+ export const PRELOAD_REQUEST_MARKER = "</project-context>\n\nMy request:\n";
27
+ /**
28
+ * Assemble the preload context body from the project's curated docs (issue
29
+ * #188). OVERVIEW.md carries current state; CHANGELOG.md carries the
30
+ * cross-session narrative — both reach the chat so the history is no longer
31
+ * write-only. Each doc self-labels via its own `#` heading, so they are joined
32
+ * with a blank line; empty docs are dropped.
33
+ */
34
+ export function composePreloadContext(overview, changelog) {
35
+ return [overview, changelog]
36
+ .map((s) => s.trim())
37
+ .filter((s) => s.length > 0)
38
+ .join("\n\n");
39
+ }
40
+ /** Build the preload-wrapped prompt for a new project chat's first turn. */
41
+ export function wrapPreload(context, message) {
42
+ return `${PRELOAD_CONTEXT_OPEN}\n${context.trim()}\n${PRELOAD_REQUEST_MARKER}${message}`;
43
+ }
44
+ /**
45
+ * Recover the user's real request from a (possibly) preload-wrapped message. If
46
+ * `text` isn't the wrapper it's returned unchanged; if it is, everything after
47
+ * the `My request:` marker is returned. Returns the input unchanged when the
48
+ * marker is absent (e.g. the wrapper was truncated before it).
49
+ */
50
+ export function stripPreloadWrapper(text) {
51
+ if (!text.startsWith(PRELOAD_CONTEXT_OPEN))
52
+ return text;
53
+ const i = text.indexOf(PRELOAD_REQUEST_MARKER);
54
+ return i === -1 ? text : text.slice(i + PRELOAD_REQUEST_MARKER.length);
55
+ }
@@ -0,0 +1,76 @@
1
+ import type { FileEntry, FileKind } from "./project-types.js";
2
+ /**
3
+ * Resolve a freeform file name to an absolute path inside the project dir,
4
+ * rejecting path traversal AND traversal *through* a hidden (dot-prefixed)
5
+ * directory. The single guard shared by every file read and by the directory
6
+ * listing (issue #259). The project root itself (`name === ""`) resolves to the
7
+ * project dir and is allowed, so a root listing passes through.
8
+ *
9
+ * **Why hidden directories are refused, not merely omitted.** {@link listFiles}
10
+ * drops dot-prefixed entries from what it *returns*, which is presentation, not
11
+ * access control: naming the path explicitly still resolved it. And the read
12
+ * route's `:name` param decodes `%2F`, so a nominally single-segment route
13
+ * accepts a whole nested path. Together those let a caller read
14
+ * `…/files/.chats%2F<id>.jsonl` (a full chat transcript) or
15
+ * `…/files/.git%2Fconfig` (which carries credentials when a remote embeds a
16
+ * token). The root project (#516) widened the blast radius from one project's
17
+ * subtree to the instance's own backing repo and every project at once.
18
+ *
19
+ * **Why the LEAF may still be a dotfile.** Refusing every dot segment was the
20
+ * first cut, and it broke Changes: an UNTRACKED file has no diff, so the pane
21
+ * renders its content through this very surface — and `.gitignore` is untracked
22
+ * in a freshly-created repo-backed project, because `ensureSidecarGitignore`
23
+ * writes it. So a legitimate, visible-in-the-UI file became unopenable. The harm
24
+ * is *descending into* `.git/` and `.chats/`, not reading a dotfile git is
25
+ * already showing you, so the guard is scoped to directory segments.
26
+ * {@link listFiles} additionally refuses a hidden LEAF, because listing one is
27
+ * how `?path=.chats` enumerated every transcript.
28
+ *
29
+ * Honest severity: **defense-in-depth, not a privilege boundary.** Paddock has
30
+ * no per-user role model, and any caller who can reach these routes can already
31
+ * start a keeper chat and run Bash — strictly more capability than reading a
32
+ * file. The `/mcp` read-only token surface exposes no file verb, so it is not
33
+ * reachable there either. This is worth closing because "hidden in the listing"
34
+ * should not be the only thing standing between an API and a transcript, not
35
+ * because it grants anything new.
36
+ */
37
+ export declare function resolveInProject(root: string, slug: string, name: string): string;
38
+ /**
39
+ * List one level of a project directory (issue #259). `subpath` is a
40
+ * project-relative directory ("" = the project root); the returned entries
41
+ * carry a `kind` so the UI can distinguish (and descend into) subdirectories.
42
+ * Dotfiles are hidden as before; entries sort directories-first, then by name.
43
+ *
44
+ * Traversal is guarded by the shared `resolveInProject`, so `subpath` can't
45
+ * escape the project dir. Throws `ProjectError("not_found")` when the directory
46
+ * doesn't exist and `ProjectError("not_directory")` when `subpath` is a file —
47
+ * the latter lets the caller fall back to rendering that file.
48
+ */
49
+ export declare function listFiles(root: string, slug: string, subpath?: string): Promise<FileEntry[]>;
50
+ /** Read a freeform file's contents as UTF-8 text (path-traversal guarded). */
51
+ export declare function readProjectFile(root: string, slug: string, name: string): Promise<string>;
52
+ /**
53
+ * Read a file's raw bytes + its MIME type (issue #61), for the binary/image
54
+ * endpoint. Path-traversal guarded; throws ProjectError("not_found") if the
55
+ * file is missing so the route can 404 cleanly. NOT decoded as text, so binary
56
+ * (image) bytes survive intact.
57
+ */
58
+ export declare function readFileBytes(root: string, slug: string, name: string): Promise<{
59
+ bytes: Buffer;
60
+ mime: string;
61
+ }>;
62
+ /**
63
+ * Read a file plus a render-kind hint derived from its extension, for the
64
+ * UI's markdown/Mermaid + sandboxed-iframe renderers (issue #3) and the image
65
+ * viewer (issue #61).
66
+ *
67
+ * For an IMAGE the raw bytes are NOT returned here (decoding binary as UTF-8
68
+ * would mangle it): `content` is empty and the client fetches the bytes from
69
+ * the raw endpoint. We still stat the file so a missing image 404s. Path-
70
+ * traversal guarded; throws ProjectError("not_found") when missing.
71
+ */
72
+ export declare function readFileWithKind(root: string, slug: string, name: string): Promise<{
73
+ name: string;
74
+ kind: FileKind;
75
+ content: string;
76
+ }>;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * project-files — the read-only freeform-file surface for a project directory.
3
+ *
4
+ * Extracted from projects.ts (issue #403): the path-traversal guard plus the
5
+ * directory listing + file readers (text / raw bytes / kind-hinted). Pure free
6
+ * functions taking `(root, slug, …)` — no `ProjectStore` state beyond the
7
+ * projects root — so `ProjectStore` keeps thin delegate methods over these and
8
+ * the public API is unchanged. `ProjectError` codes are preserved exactly.
9
+ */
10
+ import { promises as fs } from "node:fs";
11
+ import path from "node:path";
12
+ import { ProjectError } from "./project-paths.js";
13
+ import { fileKind, contentTypeFor } from "./project-mime.js";
14
+ /**
15
+ * The directory backing a workspace key — a second copy of
16
+ * `ProjectStore.dirFor`'s resolution, kept in step by being the same one-liner.
17
+ *
18
+ * A key is a path relative to the projects root and the root workspace's key is
19
+ * `""`, so `path.join` handles both with no branch. That matters historically:
20
+ * the previous design DID need a branch here for a reserved slug, this copy was
21
+ * missed, and every root file route 404'd until it was found in live QA. There
22
+ * is no longer a branch to miss.
23
+ *
24
+ * The traversal guard below is unaffected: it compares against whatever this
25
+ * returns, so at the root "inside the workspace dir" means "inside the projects
26
+ * root" — exactly the scope of a keeper whose cwd is that directory.
27
+ */
28
+ function dirFor(root, key) {
29
+ return path.join(root, key);
30
+ }
31
+ /**
32
+ * Resolve a freeform file name to an absolute path inside the project dir,
33
+ * rejecting path traversal AND traversal *through* a hidden (dot-prefixed)
34
+ * directory. The single guard shared by every file read and by the directory
35
+ * listing (issue #259). The project root itself (`name === ""`) resolves to the
36
+ * project dir and is allowed, so a root listing passes through.
37
+ *
38
+ * **Why hidden directories are refused, not merely omitted.** {@link listFiles}
39
+ * drops dot-prefixed entries from what it *returns*, which is presentation, not
40
+ * access control: naming the path explicitly still resolved it. And the read
41
+ * route's `:name` param decodes `%2F`, so a nominally single-segment route
42
+ * accepts a whole nested path. Together those let a caller read
43
+ * `…/files/.chats%2F<id>.jsonl` (a full chat transcript) or
44
+ * `…/files/.git%2Fconfig` (which carries credentials when a remote embeds a
45
+ * token). The root project (#516) widened the blast radius from one project's
46
+ * subtree to the instance's own backing repo and every project at once.
47
+ *
48
+ * **Why the LEAF may still be a dotfile.** Refusing every dot segment was the
49
+ * first cut, and it broke Changes: an UNTRACKED file has no diff, so the pane
50
+ * renders its content through this very surface — and `.gitignore` is untracked
51
+ * in a freshly-created repo-backed project, because `ensureSidecarGitignore`
52
+ * writes it. So a legitimate, visible-in-the-UI file became unopenable. The harm
53
+ * is *descending into* `.git/` and `.chats/`, not reading a dotfile git is
54
+ * already showing you, so the guard is scoped to directory segments.
55
+ * {@link listFiles} additionally refuses a hidden LEAF, because listing one is
56
+ * how `?path=.chats` enumerated every transcript.
57
+ *
58
+ * Honest severity: **defense-in-depth, not a privilege boundary.** Paddock has
59
+ * no per-user role model, and any caller who can reach these routes can already
60
+ * start a keeper chat and run Bash — strictly more capability than reading a
61
+ * file. The `/mcp` read-only token surface exposes no file verb, so it is not
62
+ * reachable there either. This is worth closing because "hidden in the listing"
63
+ * should not be the only thing standing between an API and a transcript, not
64
+ * because it grants anything new.
65
+ */
66
+ export function resolveInProject(root, slug, name) {
67
+ const dir = dirFor(root, slug);
68
+ const resolved = path.resolve(dir, name);
69
+ if (resolved !== dir && !resolved.startsWith(dir + path.sep)) {
70
+ throw new ProjectError("Invalid file path", "invalid");
71
+ }
72
+ // Check the RESOLVED path's segments relative to the project dir, not the
73
+ // caller's raw string: `a/./.git/config` and `a/../.git/config` both normalise
74
+ // to a hidden directory segment the raw string doesn't literally contain. The
75
+ // project dir itself may legitimately sit under a dot-prefixed ancestor (a
76
+ // data dir like `/srv/.paddock/projects`), which is why only the relative part
77
+ // is examined. `slice(0, -1)` leaves the leaf to the caller (see doc-comment).
78
+ const segments = relSegments(dir, resolved);
79
+ if (segments.slice(0, -1).some(isHidden)) {
80
+ throw new ProjectError("Invalid file path", "invalid");
81
+ }
82
+ return resolved;
83
+ }
84
+ /** Path segments of `resolved` relative to `dir` ([] when they're the same). */
85
+ function relSegments(dir, resolved) {
86
+ const rel = path.relative(dir, resolved);
87
+ return rel ? rel.split(path.sep) : [];
88
+ }
89
+ function isHidden(segment) {
90
+ return segment.startsWith(".");
91
+ }
92
+ /**
93
+ * List one level of a project directory (issue #259). `subpath` is a
94
+ * project-relative directory ("" = the project root); the returned entries
95
+ * carry a `kind` so the UI can distinguish (and descend into) subdirectories.
96
+ * Dotfiles are hidden as before; entries sort directories-first, then by name.
97
+ *
98
+ * Traversal is guarded by the shared `resolveInProject`, so `subpath` can't
99
+ * escape the project dir. Throws `ProjectError("not_found")` when the directory
100
+ * doesn't exist and `ProjectError("not_directory")` when `subpath` is a file —
101
+ * the latter lets the caller fall back to rendering that file.
102
+ */
103
+ export async function listFiles(root, slug, subpath = "") {
104
+ const target = resolveInProject(root, slug, subpath);
105
+ // A LISTING target is a directory, so unlike a read the leaf gets no pass:
106
+ // `?path=.chats` is exactly how every transcript filename was enumerable.
107
+ const leaf = relSegments(dirFor(root, slug), target).at(-1);
108
+ if (leaf && isHidden(leaf)) {
109
+ throw new ProjectError("Invalid file path", "invalid");
110
+ }
111
+ let entries;
112
+ try {
113
+ entries = await fs.readdir(target, { withFileTypes: true });
114
+ }
115
+ catch (err) {
116
+ const code = err.code;
117
+ if (code === "ENOENT")
118
+ throw new ProjectError(`Directory not found: ${subpath}`, "not_found");
119
+ if (code === "ENOTDIR")
120
+ throw new ProjectError(`Not a directory: ${subpath}`, "not_directory");
121
+ throw err;
122
+ }
123
+ return entries
124
+ .filter((e) => !e.name.startsWith("."))
125
+ .map((e) => ({ name: e.name, kind: e.isDirectory() ? "dir" : "file" }))
126
+ .sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
127
+ }
128
+ /** Read a freeform file's contents as UTF-8 text (path-traversal guarded). */
129
+ export async function readProjectFile(root, slug, name) {
130
+ return fs.readFile(resolveInProject(root, slug, name), "utf8");
131
+ }
132
+ /**
133
+ * Read a file's raw bytes + its MIME type (issue #61), for the binary/image
134
+ * endpoint. Path-traversal guarded; throws ProjectError("not_found") if the
135
+ * file is missing so the route can 404 cleanly. NOT decoded as text, so binary
136
+ * (image) bytes survive intact.
137
+ */
138
+ export async function readFileBytes(root, slug, name) {
139
+ const resolved = resolveInProject(root, slug, name);
140
+ try {
141
+ const bytes = await fs.readFile(resolved);
142
+ return { bytes, mime: contentTypeFor(name) };
143
+ }
144
+ catch (err) {
145
+ if (err.code === "ENOENT") {
146
+ throw new ProjectError(`File not found: ${name}`, "not_found");
147
+ }
148
+ throw err;
149
+ }
150
+ }
151
+ /**
152
+ * Read a file plus a render-kind hint derived from its extension, for the
153
+ * UI's markdown/Mermaid + sandboxed-iframe renderers (issue #3) and the image
154
+ * viewer (issue #61).
155
+ *
156
+ * For an IMAGE the raw bytes are NOT returned here (decoding binary as UTF-8
157
+ * would mangle it): `content` is empty and the client fetches the bytes from
158
+ * the raw endpoint. We still stat the file so a missing image 404s. Path-
159
+ * traversal guarded; throws ProjectError("not_found") when missing.
160
+ */
161
+ export async function readFileWithKind(root, slug, name) {
162
+ const kind = fileKind(name);
163
+ if (kind === "image") {
164
+ // Existence check only — the bytes go over the raw endpoint.
165
+ try {
166
+ await fs.stat(resolveInProject(root, slug, name));
167
+ }
168
+ catch (err) {
169
+ if (err instanceof ProjectError)
170
+ throw err;
171
+ if (err.code === "ENOENT") {
172
+ throw new ProjectError(`File not found: ${name}`, "not_found");
173
+ }
174
+ throw err;
175
+ }
176
+ return { name, kind, content: "" };
177
+ }
178
+ let content;
179
+ try {
180
+ content = await readProjectFile(root, slug, name);
181
+ }
182
+ catch (err) {
183
+ if (err instanceof ProjectError)
184
+ throw err; // traversal -> "invalid"
185
+ if (err.code === "ENOENT") {
186
+ throw new ProjectError(`File not found: ${name}`, "not_found");
187
+ }
188
+ throw err;
189
+ }
190
+ return { name, kind, content };
191
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * project-mime — MIME maps + file-kind/content-type helpers for project files.
3
+ *
4
+ * Extracted from projects.ts (issue #403) as a zero-coupling leaf module: the
5
+ * three extension→MIME maps plus the two pure classifiers (`fileKind` for the UI
6
+ * renderer choice, `contentTypeFor` for the raw-byte endpoint's Content-Type).
7
+ * Re-exported from projects.ts so existing importers keep resolving in one place.
8
+ */
9
+ import type { FileKind } from "./project-types.js";
10
+ /**
11
+ * Image extensions → their MIME type, for the render kind + the raw byte
12
+ * endpoint's Content-Type (issue #61). SVG is included but is served with a
13
+ * locked-down CSP by the byte route (it can carry scripts).
14
+ */
15
+ export declare const IMAGE_MIME: Record<string, string>;
16
+ /**
17
+ * Video extensions → their MIME type, for the raw byte endpoint's Content-Type
18
+ * (issue #126). Kept SEPARATE from IMAGE_MIME so the image file-kind classifier
19
+ * (`fileKind`) is untouched — video only affects the content-type served, which
20
+ * (together with HTTP range support) is what lets a `<video>` play, incl. iOS.
21
+ */
22
+ export declare const VIDEO_MIME: Record<string, string>;
23
+ /**
24
+ * Non-image document extensions → their MIME type. Kept SEPARATE from
25
+ * `IMAGE_MIME` on purpose: the file-kind classifier (`fileKind`) treats every
26
+ * `IMAGE_MIME` entry as `kind: "image"`, so a `.pdf` must not live there. It's
27
+ * used only for the byte endpoint's Content-Type (a PDF must serve as
28
+ * `application/pdf`, not the octet-stream the attachment store rewrites to
29
+ * `text/plain`).
30
+ */
31
+ export declare const DOCUMENT_MIME: Record<string, string>;
32
+ /** Derive a render kind from a file name's extension. */
33
+ export declare function fileKind(name: string): FileKind;
34
+ /** The MIME type for a file name's extension, defaulting to octet-stream. */
35
+ export declare function contentTypeFor(name: string): string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Image extensions → their MIME type, for the render kind + the raw byte
3
+ * endpoint's Content-Type (issue #61). SVG is included but is served with a
4
+ * locked-down CSP by the byte route (it can carry scripts).
5
+ */
6
+ export const IMAGE_MIME = {
7
+ ".png": "image/png",
8
+ ".jpg": "image/jpeg",
9
+ ".jpeg": "image/jpeg",
10
+ ".gif": "image/gif",
11
+ ".webp": "image/webp",
12
+ ".avif": "image/avif",
13
+ ".bmp": "image/bmp",
14
+ ".ico": "image/x-icon",
15
+ ".svg": "image/svg+xml",
16
+ };
17
+ /**
18
+ * Video extensions → their MIME type, for the raw byte endpoint's Content-Type
19
+ * (issue #126). Kept SEPARATE from IMAGE_MIME so the image file-kind classifier
20
+ * (`fileKind`) is untouched — video only affects the content-type served, which
21
+ * (together with HTTP range support) is what lets a `<video>` play, incl. iOS.
22
+ */
23
+ export const VIDEO_MIME = {
24
+ ".mp4": "video/mp4",
25
+ ".webm": "video/webm",
26
+ ".mov": "video/quicktime",
27
+ ".m4v": "video/x-m4v",
28
+ };
29
+ /**
30
+ * Non-image document extensions → their MIME type. Kept SEPARATE from
31
+ * `IMAGE_MIME` on purpose: the file-kind classifier (`fileKind`) treats every
32
+ * `IMAGE_MIME` entry as `kind: "image"`, so a `.pdf` must not live there. It's
33
+ * used only for the byte endpoint's Content-Type (a PDF must serve as
34
+ * `application/pdf`, not the octet-stream the attachment store rewrites to
35
+ * `text/plain`).
36
+ */
37
+ export const DOCUMENT_MIME = {
38
+ ".pdf": "application/pdf",
39
+ };
40
+ /** Derive a render kind from a file name's extension. */
41
+ export function fileKind(name) {
42
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
43
+ if (ext === ".md" || ext === ".markdown")
44
+ return "markdown";
45
+ if (ext === ".html" || ext === ".htm")
46
+ return "html";
47
+ if (ext in IMAGE_MIME)
48
+ return "image";
49
+ return "text";
50
+ }
51
+ /** The MIME type for a file name's extension, defaulting to octet-stream. */
52
+ export function contentTypeFor(name) {
53
+ const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
54
+ return (IMAGE_MIME[ext] ?? VIDEO_MIME[ext] ?? DOCUMENT_MIME[ext] ?? "application/octet-stream");
55
+ }
@@ -0,0 +1,88 @@
1
+ export declare const SLUG_RE: RegExp;
2
+ /**
3
+ * A **workspace key** is a workspace's path RELATIVE to `projectsRoot`.
4
+ *
5
+ * The root workspace's key is the empty string. That is not a sentinel — it is
6
+ * the zero value already present in the key space, and it is what makes the
7
+ * resolution seam vanish instead of branch:
8
+ *
9
+ * ```ts
10
+ * dirFor(key) => path.join(this.root, key) // path.join(root, "") === root
11
+ * ```
12
+ *
13
+ * The previous design (issue #516) modelled the root as a *project* with a
14
+ * reserved slug `__root`, which forced a `dirFor` branch — duplicated in
15
+ * `project-files.ts`, where it was missed, 404ing every root file route. With a
16
+ * relative-path key there is no branch to forget.
17
+ *
18
+ * A project is a workspace with a non-empty key. Nested workspaces (`repo/sub`)
19
+ * are simply longer relative paths; nothing here assumes a single segment.
20
+ */
21
+ export type WorkspaceKey = string;
22
+ /** The root workspace's key: its path relative to `projectsRoot` is empty. */
23
+ export declare const ROOT_KEY: WorkspaceKey;
24
+ /** Whether a key addresses the root workspace (i.e. `projectsRoot` itself). */
25
+ export declare function isRootKey(key: WorkspaceKey): boolean;
26
+ /**
27
+ * The root workspace's name in the **herdctl agent namespace**.
28
+ *
29
+ * This is the one place a sentinel is unavoidable, because that namespace
30
+ * genuinely cannot represent the empty key: herdctl agent names must be
31
+ * non-empty and must satisfy TWO disagreeing patterns — `AGENT_NAME_PATTERN`
32
+ * (`/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/`, used by `addAgent`) and the session store's
33
+ * `SAFE_IDENTIFIER_PATTERN` (`/^[a-zA-Z0-9]([a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/`,
34
+ * which requires a trailing alphanumeric). A bare `keeper-` fails both, and the
35
+ * shape #516 originally proposed (`keeper-__root__`) registered cleanly and then
36
+ * threw PathTraversalError on the first resume.
37
+ *
38
+ * `_root` satisfies both patterns and can never collide with a project, because
39
+ * {@link SLUG_RE} rejects underscores — no slug can equal it.
40
+ *
41
+ * **This is a NAME, not an identity.** A workspace is identified by its relative
42
+ * path ({@link WorkspaceKey}); this encoding exists only at the herdctl
43
+ * boundary, is applied in one function, and keeps the four agent-name builders
44
+ * (`keeper-`/`sweeper-`/`hook-`/`trigger-`) uniform with no root branch.
45
+ */
46
+ export declare const ROOT_AGENT_KEY = "_root";
47
+ /** Encode a workspace key for use inside a herdctl agent name. */
48
+ export declare function agentKeyFor(key: WorkspaceKey): string;
49
+ /** Decode a herdctl agent-name segment back to a workspace key. */
50
+ export declare function keyFromAgentKey(agentKey: string): WorkspaceKey;
51
+ /**
52
+ * Accepted repo-URL shapes for a repo-backed project (issue #187): https(s),
53
+ * ssh (`git@host:owner/repo`), git://, and a local `file://` or absolute path
54
+ * (the last two make deterministic tests possible without a network). Anything
55
+ * else is rejected up front so a bad value never reaches `git clone`.
56
+ */
57
+ export declare const REPO_URL_RE: RegExp;
58
+ /** Validate a candidate repo URL (issue #187). */
59
+ export declare function isValidRepoUrl(url: string): boolean;
60
+ /**
61
+ * Derive the checkout directory NAME for a repo-backed project from its repo URL
62
+ * (issue #187) — the repo's own basename, sans a trailing `.git`, sanitised to a
63
+ * filesystem-safe token. The keeper's cwd becomes `<projectDir>/<thisName>`, so
64
+ * the cwd basename reads like the repo (e.g. `paddock`). Deterministic (same URL
65
+ * ⇒ same name), which is why `repo` is immutable once set.
66
+ */
67
+ export declare function repoCheckoutName(repo: string): string;
68
+ /**
69
+ * Resolve a project's working directory (keeper cwd) from its metadata dir + repo
70
+ * URL: the nested checkout for a repo-backed project, else the metadata dir
71
+ * itself for a notebook project (issue #187).
72
+ */
73
+ export declare function workingDirFor(dir: string, repo?: string): string;
74
+ export declare class ProjectError extends Error {
75
+ readonly code: "not_found" | "exists" | "invalid" | "not_directory";
76
+ constructor(message: string, code: "not_found" | "exists" | "invalid" | "not_directory");
77
+ }
78
+ export declare function slugify(name: string): string;
79
+ export declare function today(): string;
80
+ /**
81
+ * Minimal seed for a per-project CLAUDE.md (issue #177): the durable-identity
82
+ * layer of the two-level native-context model (instance-wide + per-project
83
+ * CLAUDE.md, loaded natively once #176 lands). Deliberately terse — the sweeper
84
+ * amends it conservatively over time under "Curated notes"; everything a human
85
+ * writes is preserved. OVERVIEW.md holds current state, CHANGELOG.md holds
86
+ * history; this file holds only what the project durably IS and how we work on it.
87
+ */
88
+ export declare function claudeTemplate(name: string, summary: string): string;