@piecemaker-legal/piecemaker 1.0.6

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 (1285) hide show
  1. package/LICENSE +718 -0
  2. package/README.md +79 -0
  3. package/dist/Mistral-Icon-Gradient-RGB.svg +12 -0
  4. package/dist/api-docs.html +901 -0
  5. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/dist/assets/abnfDiagram-VCTEODGH-DF7LFtT-.js +1 -0
  65. package/dist/assets/arc-xOmjWeVH.js +1 -0
  66. package/dist/assets/architectureDiagram-5GKGNRK7-CESzNPwa.js +36 -0
  67. package/dist/assets/blockDiagram-I7D4REHJ-BjdN78Ad.js +129 -0
  68. package/dist/assets/c4Diagram-7LVT6UL2-BcK9EDWT.js +38 -0
  69. package/dist/assets/channel-Dz3eE5vU.js +1 -0
  70. package/dist/assets/chunk-2Q5K7J3B-DGUN1JFP.js +1 -0
  71. package/dist/assets/chunk-5VM5RSS4-BG3BBcFr.js +15 -0
  72. package/dist/assets/chunk-F27PBJKO-CLIOfrF5.js +1 -0
  73. package/dist/assets/chunk-IMKFNOWR-D6lGkqij.js +231 -0
  74. package/dist/assets/chunk-JWPE2WC7-BmF9vLBT.js +1 -0
  75. package/dist/assets/chunk-POPQ4Y6H-C_0SR3ak.js +1 -0
  76. package/dist/assets/chunk-SVP7TREG-rT20Wfpp.js +88 -0
  77. package/dist/assets/chunk-TICWLB2K-Cn2hkmcf.js +206 -0
  78. package/dist/assets/chunk-XXDRQBXY-Dm_eP1_v.js +1 -0
  79. package/dist/assets/classDiagram-ZZMXUADV-BC1eKV2j.js +1 -0
  80. package/dist/assets/classDiagram-v2-VYDZK3BY-BC1eKV2j.js +1 -0
  81. package/dist/assets/cose-bilkent-JH36ORCC-cJeeJsep.js +1 -0
  82. package/dist/assets/cynefin-OW5HDTMX-DX2ePIXe.js +166 -0
  83. package/dist/assets/cynefinDiagram-5FMLGOSQ-BvWXN-RR.js +62 -0
  84. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  85. package/dist/assets/dagre-GXQ25YYZ-CkwPRhGR.js +4 -0
  86. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  87. package/dist/assets/diagram-S7CK7UJ4-CBGNzYyv.js +30 -0
  88. package/dist/assets/diagram-UQ7AKVKN-CiwCU0SJ.js +41 -0
  89. package/dist/assets/diagram-VSXAHHWV-Aekg4nTk.js +3 -0
  90. package/dist/assets/diagram-VX7I27RA-DPFP5K7r.js +24 -0
  91. package/dist/assets/diagram-Z3DM3KII-BHJkxoey.js +24 -0
  92. package/dist/assets/ebnfDiagram-PWID7BFC-2rV1P32T.js +1 -0
  93. package/dist/assets/erDiagram-RLTQ6QDP-C4BjyS_H.js +99 -0
  94. package/dist/assets/flowDiagram-HODETNUW-DL1TBlAG.js +168 -0
  95. package/dist/assets/ganttDiagram-EL5Y4UJY-MKjvzCWz.js +292 -0
  96. package/dist/assets/gitGraphDiagram-WWUBYQGX-CS_oTGK5.js +106 -0
  97. package/dist/assets/index-DhwJpHCq.css +1 -0
  98. package/dist/assets/index-XOGQDqU3.js +3993 -0
  99. package/dist/assets/infoDiagram-27XIBGKW-ihUr2GjN.js +2 -0
  100. package/dist/assets/init-Gi6I4Gst.js +1 -0
  101. package/dist/assets/ishikawaDiagram-5VMMS53U-D2YAnuir.js +70 -0
  102. package/dist/assets/journeyDiagram-3NMN7TZE-Bc2hSZGD.js +139 -0
  103. package/dist/assets/kanban-definition-UXKFOSKX-LRMGNonX.js +89 -0
  104. package/dist/assets/layout-CcfWOlPV.js +1 -0
  105. package/dist/assets/linear-7JoS2gQO.js +1 -0
  106. package/dist/assets/mermaid.core-ijh_SsuN.js +308 -0
  107. package/dist/assets/mindmap-definition-YA3MSWOX-DCE4nzfF.js +96 -0
  108. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  109. package/dist/assets/pegDiagram-XKGWAZYB-SjaGa7lE.js +1 -0
  110. package/dist/assets/pieDiagram-E7YTZNPT-1xcRidSZ.js +39 -0
  111. package/dist/assets/quadrantDiagram-AXDQQJYC-obHAHnYV.js +7 -0
  112. package/dist/assets/railroadDiagram-O6MQD6OU-BsCVcRZb.js +1 -0
  113. package/dist/assets/requirementDiagram-BXWQKSXE-CUNkhvqb.js +84 -0
  114. package/dist/assets/sankeyDiagram-P5KCCOFB-DhezXMKm.js +40 -0
  115. package/dist/assets/sequenceDiagram-WJ2MYXX4-CL9VswXH.js +162 -0
  116. package/dist/assets/server.browser-BzI1jrZw.js +11 -0
  117. package/dist/assets/sizeCapture-INFHLROL-t0g9jiEz.js +1 -0
  118. package/dist/assets/stateDiagram-D77RDMKH-BW-g-IOz.js +1 -0
  119. package/dist/assets/stateDiagram-v2-MP3YSRHH-BXuT4zDG.js +1 -0
  120. package/dist/assets/swimlanes-42K2YHIH-LsuyeBte.js +1 -0
  121. package/dist/assets/swimlanesDiagram-VR7AAH4N-B0eYrB4U.js +8 -0
  122. package/dist/assets/timeline-definition-24CTP7MA-B0cFka1R.js +120 -0
  123. package/dist/assets/vendor-codemirror-C5qqu4R7.js +33 -0
  124. package/dist/assets/vendor-react-D809iwXR.js +8 -0
  125. package/dist/assets/vendor-xterm-BWUgpGtR.js +66 -0
  126. package/dist/assets/vennDiagram-4TSXK5OY-D3HNCDR3.js +34 -0
  127. package/dist/assets/wardleyDiagram-VM6X3IG4-DJ3Co8nb.js +78 -0
  128. package/dist/assets/xychartDiagram-S5SC5T6Z-Cz5E9New.js +7 -0
  129. package/dist/clear-cache.html +85 -0
  130. package/dist/convert-icons.md +53 -0
  131. package/dist/favicon.png +0 -0
  132. package/dist/favicon.svg +9 -0
  133. package/dist/generate-icons.js +49 -0
  134. package/dist/icons/claude-ai-icon.svg +1 -0
  135. package/dist/icons/codex-white.svg +3 -0
  136. package/dist/icons/codex.svg +3 -0
  137. package/dist/icons/cursor-white.svg +12 -0
  138. package/dist/icons/cursor.svg +1 -0
  139. package/dist/icons/icon-128x128.png +0 -0
  140. package/dist/icons/icon-128x128.svg +12 -0
  141. package/dist/icons/icon-144x144.png +0 -0
  142. package/dist/icons/icon-144x144.svg +12 -0
  143. package/dist/icons/icon-152x152.png +0 -0
  144. package/dist/icons/icon-152x152.svg +12 -0
  145. package/dist/icons/icon-192x192.png +0 -0
  146. package/dist/icons/icon-192x192.svg +12 -0
  147. package/dist/icons/icon-384x384.png +0 -0
  148. package/dist/icons/icon-384x384.svg +12 -0
  149. package/dist/icons/icon-512x512.png +0 -0
  150. package/dist/icons/icon-512x512.svg +12 -0
  151. package/dist/icons/icon-72x72.png +0 -0
  152. package/dist/icons/icon-72x72.svg +12 -0
  153. package/dist/icons/icon-96x96.png +0 -0
  154. package/dist/icons/icon-96x96.svg +12 -0
  155. package/dist/icons/icon-template.svg +12 -0
  156. package/dist/index.html +60 -0
  157. package/dist/logo-128.png +0 -0
  158. package/dist/logo-256.png +0 -0
  159. package/dist/logo-32.png +0 -0
  160. package/dist/logo-512.png +0 -0
  161. package/dist/logo-64.png +0 -0
  162. package/dist/logo.svg +17 -0
  163. package/dist/manifest.json +61 -0
  164. package/dist/screenshots/cli-selection.png +0 -0
  165. package/dist/screenshots/desktop-main.png +0 -0
  166. package/dist/screenshots/mobile-chat.png +0 -0
  167. package/dist/screenshots/tools-modal.png +0 -0
  168. package/dist/sw.js +125 -0
  169. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +272 -0
  170. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -0
  171. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +160 -0
  172. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -0
  173. package/dist-server/plugins/piecemaker-dossier/src/types.js +3 -0
  174. package/dist-server/plugins/piecemaker-dossier/src/types.js.map +1 -0
  175. package/dist-server/server/index.js +345 -0
  176. package/dist-server/server/index.js.map +1 -0
  177. package/dist-server/server/load-env.js +46 -0
  178. package/dist-server/server/load-env.js.map +1 -0
  179. package/dist-server/server/modules/agent/agent.module.js +38 -0
  180. package/dist-server/server/modules/agent/agent.module.js.map +1 -0
  181. package/dist-server/server/modules/agent/agent.routes.js +1161 -0
  182. package/dist-server/server/modules/agent/agent.routes.js.map +1 -0
  183. package/dist-server/server/modules/agent/index.js +3 -0
  184. package/dist-server/server/modules/agent/index.js.map +1 -0
  185. package/dist-server/server/modules/agent/tests/agent.routes.test.js +205 -0
  186. package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -0
  187. package/dist-server/server/modules/assets/assets.routes.js +130 -0
  188. package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
  189. package/dist-server/server/modules/assets/index.js +4 -0
  190. package/dist-server/server/modules/assets/index.js.map +1 -0
  191. package/dist-server/server/modules/assets/services/image-assets.service.js +95 -0
  192. package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
  193. package/dist-server/server/modules/assets/tests/image-assets.service.test.js +56 -0
  194. package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
  195. package/dist-server/server/modules/auth/auth.middleware.js +133 -0
  196. package/dist-server/server/modules/auth/auth.middleware.js.map +1 -0
  197. package/dist-server/server/modules/auth/auth.module.js +29 -0
  198. package/dist-server/server/modules/auth/auth.module.js.map +1 -0
  199. package/dist-server/server/modules/auth/auth.routes.js +45 -0
  200. package/dist-server/server/modules/auth/auth.routes.js.map +1 -0
  201. package/dist-server/server/modules/auth/auth.service.js +113 -0
  202. package/dist-server/server/modules/auth/auth.service.js.map +1 -0
  203. package/dist-server/server/modules/auth/index.js +9 -0
  204. package/dist-server/server/modules/auth/index.js.map +1 -0
  205. package/dist-server/server/modules/auth/tests/auth.service.test.js +80 -0
  206. package/dist-server/server/modules/auth/tests/auth.service.test.js.map +1 -0
  207. package/dist-server/server/modules/browser-use/browser-use-mcp.js +352 -0
  208. package/dist-server/server/modules/browser-use/browser-use-mcp.js.map +1 -0
  209. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
  210. package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
  211. package/dist-server/server/modules/browser-use/browser-use-runtime.js +10 -0
  212. package/dist-server/server/modules/browser-use/browser-use-runtime.js.map +1 -0
  213. package/dist-server/server/modules/browser-use/browser-use.routes.js +99 -0
  214. package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
  215. package/dist-server/server/modules/browser-use/browser-use.service.js +686 -0
  216. package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
  217. package/dist-server/server/modules/browser-use/index.js +11 -0
  218. package/dist-server/server/modules/browser-use/index.js.map +1 -0
  219. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
  220. package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
  221. package/dist-server/server/modules/cli/cli.js +17 -0
  222. package/dist-server/server/modules/cli/cli.js.map +1 -0
  223. package/dist-server/server/modules/cli/cli.module.js +93 -0
  224. package/dist-server/server/modules/cli/cli.module.js.map +1 -0
  225. package/dist-server/server/modules/cli/cli.service.js +230 -0
  226. package/dist-server/server/modules/cli/cli.service.js.map +1 -0
  227. package/dist-server/server/modules/cli/index.js +3 -0
  228. package/dist-server/server/modules/cli/index.js.map +1 -0
  229. package/dist-server/server/modules/cli/pwa-launcher.js +116 -0
  230. package/dist-server/server/modules/cli/pwa-launcher.js.map +1 -0
  231. package/dist-server/server/modules/cli/sandbox.service.js +332 -0
  232. package/dist-server/server/modules/cli/sandbox.service.js.map +1 -0
  233. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js +154 -0
  234. package/dist-server/server/modules/cli/tests/cli-environment-bootstrap.test.js.map +1 -0
  235. package/dist-server/server/modules/cli/tests/cli.service.test.js +91 -0
  236. package/dist-server/server/modules/cli/tests/cli.service.test.js.map +1 -0
  237. package/dist-server/server/modules/cli/tests/sandbox.service.test.js +55 -0
  238. package/dist-server/server/modules/cli/tests/sandbox.service.test.js.map +1 -0
  239. package/dist-server/server/modules/commands/commands.module.js +20 -0
  240. package/dist-server/server/modules/commands/commands.module.js.map +1 -0
  241. package/dist-server/server/modules/commands/commands.routes.js +498 -0
  242. package/dist-server/server/modules/commands/commands.routes.js.map +1 -0
  243. package/dist-server/server/modules/commands/index.js +3 -0
  244. package/dist-server/server/modules/commands/index.js.map +1 -0
  245. package/dist-server/server/modules/commands/tests/commands.test.js +91 -0
  246. package/dist-server/server/modules/commands/tests/commands.test.js.map +1 -0
  247. package/dist-server/server/modules/database/connection.js +125 -0
  248. package/dist-server/server/modules/database/connection.js.map +1 -0
  249. package/dist-server/server/modules/database/index.js +23 -0
  250. package/dist-server/server/modules/database/index.js.map +1 -0
  251. package/dist-server/server/modules/database/init-db.js +18 -0
  252. package/dist-server/server/modules/database/init-db.js.map +1 -0
  253. package/dist-server/server/modules/database/migrations.js +465 -0
  254. package/dist-server/server/modules/database/migrations.js.map +1 -0
  255. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  256. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  257. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  258. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  259. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  260. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  261. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  262. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  263. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
  264. package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
  265. package/dist-server/server/modules/database/repositories/notification-preferences.js +82 -0
  266. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  267. package/dist-server/server/modules/database/repositories/projects.db.js +172 -0
  268. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  269. package/dist-server/server/modules/database/repositories/provider-models.js +112 -0
  270. package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -0
  271. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  272. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  273. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  274. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  275. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
  276. package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
  277. package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
  278. package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
  279. package/dist-server/server/modules/database/repositories/sessions.db.js +507 -0
  280. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  281. package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
  282. package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
  283. package/dist-server/server/modules/database/repositories/users.js +88 -0
  284. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  285. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  286. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  287. package/dist-server/server/modules/database/schema.js +291 -0
  288. package/dist-server/server/modules/database/schema.js.map +1 -0
  289. package/dist-server/server/modules/database/tests/projects.db.integration.test.js +64 -0
  290. package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
  291. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +102 -0
  292. package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -0
  293. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +79 -0
  294. package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
  295. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +132 -0
  296. package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
  297. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
  298. package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
  299. package/dist-server/server/modules/file-tree/file-tree.module.js +95 -0
  300. package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -0
  301. package/dist-server/server/modules/file-tree/file-tree.routes.js +208 -0
  302. package/dist-server/server/modules/file-tree/file-tree.routes.js.map +1 -0
  303. package/dist-server/server/modules/file-tree/file-tree.service.js +584 -0
  304. package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -0
  305. package/dist-server/server/modules/file-tree/index.js +3 -0
  306. package/dist-server/server/modules/file-tree/index.js.map +1 -0
  307. package/dist-server/server/modules/file-tree/tests/file-tree.routes.test.js +123 -0
  308. package/dist-server/server/modules/file-tree/tests/file-tree.routes.test.js.map +1 -0
  309. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +311 -0
  310. package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -0
  311. package/dist-server/server/modules/git/git-branch.service.js +15 -0
  312. package/dist-server/server/modules/git/git-branch.service.js.map +1 -0
  313. package/dist-server/server/modules/git/git-parsing.service.js +61 -0
  314. package/dist-server/server/modules/git/git-parsing.service.js.map +1 -0
  315. package/dist-server/server/modules/git/git.module.js +14 -0
  316. package/dist-server/server/modules/git/git.module.js.map +1 -0
  317. package/dist-server/server/modules/git/git.routes.js +1347 -0
  318. package/dist-server/server/modules/git/git.routes.js.map +1 -0
  319. package/dist-server/server/modules/git/index.js +3 -0
  320. package/dist-server/server/modules/git/index.js.map +1 -0
  321. package/dist-server/server/modules/git/tests/git-branch.service.test.js +52 -0
  322. package/dist-server/server/modules/git/tests/git-branch.service.test.js.map +1 -0
  323. package/dist-server/server/modules/git/tests/git-init.routes.test.js +132 -0
  324. package/dist-server/server/modules/git/tests/git-init.routes.test.js.map +1 -0
  325. package/dist-server/server/modules/git/tests/git.test.js +80 -0
  326. package/dist-server/server/modules/git/tests/git.test.js.map +1 -0
  327. package/dist-server/server/modules/notifications/index.js +20 -0
  328. package/dist-server/server/modules/notifications/index.js.map +1 -0
  329. package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
  330. package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
  331. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
  332. package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
  333. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +249 -0
  334. package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
  335. package/dist-server/server/modules/notifications/tests/notification-orchestrator.integration.test.js +44 -0
  336. package/dist-server/server/modules/notifications/tests/notification-orchestrator.integration.test.js.map +1 -0
  337. package/dist-server/server/modules/notifications/vapid-keys.service.js +28 -0
  338. package/dist-server/server/modules/notifications/vapid-keys.service.js.map +1 -0
  339. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
  340. package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
  341. package/dist-server/server/modules/plugins/index.js +9 -0
  342. package/dist-server/server/modules/plugins/index.js.map +1 -0
  343. package/dist-server/server/modules/plugins/plugin-process.service.js +191 -0
  344. package/dist-server/server/modules/plugins/plugin-process.service.js.map +1 -0
  345. package/dist-server/server/modules/plugins/plugin-registry.service.js +415 -0
  346. package/dist-server/server/modules/plugins/plugin-registry.service.js.map +1 -0
  347. package/dist-server/server/modules/plugins/plugins.module.js +30 -0
  348. package/dist-server/server/modules/plugins/plugins.module.js.map +1 -0
  349. package/dist-server/server/modules/plugins/plugins.routes.js +70 -0
  350. package/dist-server/server/modules/plugins/plugins.routes.js.map +1 -0
  351. package/dist-server/server/modules/plugins/plugins.service.js +123 -0
  352. package/dist-server/server/modules/plugins/plugins.service.js.map +1 -0
  353. package/dist-server/server/modules/plugins/tests/plugins.service.test.js +28 -0
  354. package/dist-server/server/modules/plugins/tests/plugins.service.test.js.map +1 -0
  355. package/dist-server/server/modules/projects/index.js +9 -0
  356. package/dist-server/server/modules/projects/index.js.map +1 -0
  357. package/dist-server/server/modules/projects/projects.routes.js +205 -0
  358. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  359. package/dist-server/server/modules/projects/services/project-clone.service.js +220 -0
  360. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  361. package/dist-server/server/modules/projects/services/project-delete.service.js +89 -0
  362. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  363. package/dist-server/server/modules/projects/services/project-management.service.js +90 -0
  364. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  365. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  366. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  367. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  368. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  369. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +188 -0
  370. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  371. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  372. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  373. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  374. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  375. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  376. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  377. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  378. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  379. package/dist-server/server/modules/providers/index.js +12 -0
  380. package/dist-server/server/modules/providers/index.js.map +1 -0
  381. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +151 -0
  382. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -0
  383. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
  384. package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
  385. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js +103 -0
  386. package/dist-server/server/modules/providers/list/claude/claude-mcp.provider.js.map +1 -0
  387. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +278 -0
  388. package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
  389. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +1058 -0
  390. package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -0
  391. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +141 -0
  392. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  393. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +924 -0
  394. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -0
  395. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +208 -0
  396. package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
  397. package/dist-server/server/modules/providers/list/claude/claude.provider.js +23 -0
  398. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -0
  399. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
  400. package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
  401. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +84 -0
  402. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -0
  403. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
  404. package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
  405. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js +107 -0
  406. package/dist-server/server/modules/providers/list/codex/codex-mcp.provider.js.map +1 -0
  407. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +125 -0
  408. package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
  409. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +473 -0
  410. package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -0
  411. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +153 -0
  412. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  413. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1955 -0
  414. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -0
  415. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +62 -0
  416. package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
  417. package/dist-server/server/modules/providers/list/codex/codex.provider.js +23 -0
  418. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -0
  419. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +118 -0
  420. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -0
  421. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js +80 -0
  422. package/dist-server/server/modules/providers/list/cursor/cursor-mcp.provider.js.map +1 -0
  423. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +100 -0
  424. package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
  425. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +305 -0
  426. package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -0
  427. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +111 -0
  428. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  429. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +566 -0
  430. package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -0
  431. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +35 -0
  432. package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
  433. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +21 -0
  434. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -0
  435. package/dist-server/server/modules/providers/list/mistral/mistral-auth.provider.js +147 -0
  436. package/dist-server/server/modules/providers/list/mistral/mistral-auth.provider.js.map +1 -0
  437. package/dist-server/server/modules/providers/list/mistral/mistral-mcp.provider.js +15 -0
  438. package/dist-server/server/modules/providers/list/mistral/mistral-mcp.provider.js.map +1 -0
  439. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js +69 -0
  440. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js.map +1 -0
  441. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js +326 -0
  442. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js.map +1 -0
  443. package/dist-server/server/modules/providers/list/mistral/mistral-session-synchronizer.provider.js +12 -0
  444. package/dist-server/server/modules/providers/list/mistral/mistral-session-synchronizer.provider.js.map +1 -0
  445. package/dist-server/server/modules/providers/list/mistral/mistral-sessions.provider.js +180 -0
  446. package/dist-server/server/modules/providers/list/mistral/mistral-sessions.provider.js.map +1 -0
  447. package/dist-server/server/modules/providers/list/mistral/mistral-skills.provider.js +15 -0
  448. package/dist-server/server/modules/providers/list/mistral/mistral-skills.provider.js.map +1 -0
  449. package/dist-server/server/modules/providers/list/mistral/mistral.provider.js +25 -0
  450. package/dist-server/server/modules/providers/list/mistral/mistral.provider.js.map +1 -0
  451. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +91 -0
  452. package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
  453. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
  454. package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
  455. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +429 -0
  456. package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
  457. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +368 -0
  458. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -0
  459. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +225 -0
  460. package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.test.js.map +1 -0
  461. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +136 -0
  462. package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
  463. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +404 -0
  464. package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
  465. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
  466. package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
  467. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +21 -0
  468. package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
  469. package/dist-server/server/modules/providers/provider.registry.js +33 -0
  470. package/dist-server/server/modules/providers/provider.registry.js.map +1 -0
  471. package/dist-server/server/modules/providers/provider.routes.js +676 -0
  472. package/dist-server/server/modules/providers/provider.routes.js.map +1 -0
  473. package/dist-server/server/modules/providers/services/mcp.service.js +84 -0
  474. package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -0
  475. package/dist-server/server/modules/providers/services/provider-auth.service.js +25 -0
  476. package/dist-server/server/modules/providers/services/provider-auth.service.js.map +1 -0
  477. package/dist-server/server/modules/providers/services/provider-capabilities.service.js +94 -0
  478. package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
  479. package/dist-server/server/modules/providers/services/provider-models.service.js +289 -0
  480. package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
  481. package/dist-server/server/modules/providers/services/provider-runtime.service.js +66 -0
  482. package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -0
  483. package/dist-server/server/modules/providers/services/provider-token-usage.service.js +372 -0
  484. package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -0
  485. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +974 -0
  486. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  487. package/dist-server/server/modules/providers/services/session-history-cache.service.js +87 -0
  488. package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
  489. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +130 -0
  490. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  491. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +214 -0
  492. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  493. package/dist-server/server/modules/providers/services/sessions.service.js +519 -0
  494. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -0
  495. package/dist-server/server/modules/providers/services/skills.service.js +22 -0
  496. package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
  497. package/dist-server/server/modules/providers/shared/base/abstract.provider.js +13 -0
  498. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -0
  499. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js +102 -0
  500. package/dist-server/server/modules/providers/shared/mcp/mcp.provider.js.map +1 -0
  501. package/dist-server/server/modules/providers/shared/skills/skills.provider.js +207 -0
  502. package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
  503. package/dist-server/server/modules/providers/tests/claude-auth.test.js +174 -0
  504. package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -0
  505. package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
  506. package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
  507. package/dist-server/server/modules/providers/tests/claude-sessions.test.js +470 -0
  508. package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -0
  509. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
  510. package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
  511. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
  512. package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
  513. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
  514. package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
  515. package/dist-server/server/modules/providers/tests/codex-runtime.test.js +51 -0
  516. package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
  517. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
  518. package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
  519. package/dist-server/server/modules/providers/tests/codex-sessions.test.js +491 -0
  520. package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
  521. package/dist-server/server/modules/providers/tests/mcp.test.js +291 -0
  522. package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -0
  523. package/dist-server/server/modules/providers/tests/opencode-models.test.js +132 -0
  524. package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
  525. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +424 -0
  526. package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
  527. package/dist-server/server/modules/providers/tests/provider-attachment-history.test.js +205 -0
  528. package/dist-server/server/modules/providers/tests/provider-attachment-history.test.js.map +1 -0
  529. package/dist-server/server/modules/providers/tests/provider-models.service.test.js +268 -0
  530. package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
  531. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js +126 -0
  532. package/dist-server/server/modules/providers/tests/provider-runtime.service.test.js.map +1 -0
  533. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +329 -0
  534. package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -0
  535. package/dist-server/server/modules/providers/tests/provider.routes.test.js +161 -0
  536. package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -0
  537. package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
  538. package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
  539. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +120 -0
  540. package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
  541. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
  542. package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
  543. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
  544. package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
  545. package/dist-server/server/modules/providers/tests/sessions-details.test.js +62 -0
  546. package/dist-server/server/modules/providers/tests/sessions-details.test.js.map +1 -0
  547. package/dist-server/server/modules/providers/tests/sessions.service.test.js +131 -0
  548. package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -0
  549. package/dist-server/server/modules/providers/tests/skills.test.js +410 -0
  550. package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
  551. package/dist-server/server/modules/scheduled-messages/index.js +5 -0
  552. package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
  553. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
  554. package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
  555. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +163 -0
  556. package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
  557. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
  558. package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
  559. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +255 -0
  560. package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
  561. package/dist-server/server/modules/settings/index.js +3 -0
  562. package/dist-server/server/modules/settings/index.js.map +1 -0
  563. package/dist-server/server/modules/settings/settings.module.js +35 -0
  564. package/dist-server/server/modules/settings/settings.module.js.map +1 -0
  565. package/dist-server/server/modules/settings/settings.routes.js +34 -0
  566. package/dist-server/server/modules/settings/settings.routes.js.map +1 -0
  567. package/dist-server/server/modules/settings/settings.service.js +116 -0
  568. package/dist-server/server/modules/settings/settings.service.js.map +1 -0
  569. package/dist-server/server/modules/settings/tests/settings.service.test.js +48 -0
  570. package/dist-server/server/modules/settings/tests/settings.service.test.js.map +1 -0
  571. package/dist-server/server/modules/system/index.js +3 -0
  572. package/dist-server/server/modules/system/index.js.map +1 -0
  573. package/dist-server/server/modules/system/system.module.js +44 -0
  574. package/dist-server/server/modules/system/system.module.js.map +1 -0
  575. package/dist-server/server/modules/system/system.routes.js +16 -0
  576. package/dist-server/server/modules/system/system.routes.js.map +1 -0
  577. package/dist-server/server/modules/system/system.service.js +45 -0
  578. package/dist-server/server/modules/system/system.service.js.map +1 -0
  579. package/dist-server/server/modules/system/tests/system.service.test.js +100 -0
  580. package/dist-server/server/modules/system/tests/system.service.test.js.map +1 -0
  581. package/dist-server/server/modules/taskmaster/index.js +3 -0
  582. package/dist-server/server/modules/taskmaster/index.js.map +1 -0
  583. package/dist-server/server/modules/taskmaster/taskmaster.module.js +20 -0
  584. package/dist-server/server/modules/taskmaster/taskmaster.module.js.map +1 -0
  585. package/dist-server/server/modules/taskmaster/taskmaster.routes.js +1372 -0
  586. package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -0
  587. package/dist-server/server/modules/taskmaster/taskmaster.service.js +107 -0
  588. package/dist-server/server/modules/taskmaster/taskmaster.service.js.map +1 -0
  589. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +180 -0
  590. package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -0
  591. package/dist-server/server/modules/taskmaster/tests/taskmaster.service.test.js +85 -0
  592. package/dist-server/server/modules/taskmaster/tests/taskmaster.service.test.js.map +1 -0
  593. package/dist-server/server/modules/user/index.js +3 -0
  594. package/dist-server/server/modules/user/index.js.map +1 -0
  595. package/dist-server/server/modules/user/tests/user.service.test.js +123 -0
  596. package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -0
  597. package/dist-server/server/modules/user/user.module.js +56 -0
  598. package/dist-server/server/modules/user/user.module.js.map +1 -0
  599. package/dist-server/server/modules/user/user.routes.js +88 -0
  600. package/dist-server/server/modules/user/user.routes.js.map +1 -0
  601. package/dist-server/server/modules/user/user.service.js +134 -0
  602. package/dist-server/server/modules/user/user.service.js.map +1 -0
  603. package/dist-server/server/modules/voice/index.js +3 -0
  604. package/dist-server/server/modules/voice/index.js.map +1 -0
  605. package/dist-server/server/modules/voice/tests/voice.service.test.js +87 -0
  606. package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -0
  607. package/dist-server/server/modules/voice/voice.module.js +44 -0
  608. package/dist-server/server/modules/voice/voice.module.js.map +1 -0
  609. package/dist-server/server/modules/voice/voice.routes.js +87 -0
  610. package/dist-server/server/modules/voice/voice.routes.js.map +1 -0
  611. package/dist-server/server/modules/voice/voice.service.js +148 -0
  612. package/dist-server/server/modules/voice/voice.service.js.map +1 -0
  613. package/dist-server/server/modules/websocket/index.js +11 -0
  614. package/dist-server/server/modules/websocket/index.js.map +1 -0
  615. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +232 -0
  616. package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
  617. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +135 -0
  618. package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
  619. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +467 -0
  620. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  621. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  622. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  623. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
  624. package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
  625. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +472 -0
  626. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  627. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +36 -0
  628. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  629. package/dist-server/server/modules/websocket/services/websocket-server.service.js +90 -0
  630. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  631. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  632. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  633. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  634. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  635. package/dist-server/server/modules/websocket/tests/chat-attachment-filter.test.js +40 -0
  636. package/dist-server/server/modules/websocket/tests/chat-attachment-filter.test.js.map +1 -0
  637. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
  638. package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
  639. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +275 -0
  640. package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
  641. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
  642. package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
  643. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +177 -0
  644. package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -0
  645. package/dist-server/server/modules/websocket/tests/websocket-heartbeat.service.test.js +61 -0
  646. package/dist-server/server/modules/websocket/tests/websocket-heartbeat.service.test.js.map +1 -0
  647. package/dist-server/server/modules/worktrees/index.js +3 -0
  648. package/dist-server/server/modules/worktrees/index.js.map +1 -0
  649. package/dist-server/server/modules/worktrees/services/worktree-create-and-open.service.js +37 -0
  650. package/dist-server/server/modules/worktrees/services/worktree-create-and-open.service.js.map +1 -0
  651. package/dist-server/server/modules/worktrees/services/worktree-create.service.js +67 -0
  652. package/dist-server/server/modules/worktrees/services/worktree-create.service.js.map +1 -0
  653. package/dist-server/server/modules/worktrees/services/worktree-git.service.js +172 -0
  654. package/dist-server/server/modules/worktrees/services/worktree-git.service.js.map +1 -0
  655. package/dist-server/server/modules/worktrees/services/worktree-list.service.js +91 -0
  656. package/dist-server/server/modules/worktrees/services/worktree-list.service.js.map +1 -0
  657. package/dist-server/server/modules/worktrees/services/worktree-merge.service.js +118 -0
  658. package/dist-server/server/modules/worktrees/services/worktree-merge.service.js.map +1 -0
  659. package/dist-server/server/modules/worktrees/services/worktree-open.service.js +58 -0
  660. package/dist-server/server/modules/worktrees/services/worktree-open.service.js.map +1 -0
  661. package/dist-server/server/modules/worktrees/services/worktree-remove.service.js +65 -0
  662. package/dist-server/server/modules/worktrees/services/worktree-remove.service.js.map +1 -0
  663. package/dist-server/server/modules/worktrees/tests/worktree-create-and-open.service.test.js +58 -0
  664. package/dist-server/server/modules/worktrees/tests/worktree-create-and-open.service.test.js.map +1 -0
  665. package/dist-server/server/modules/worktrees/tests/worktree-create.service.test.js +85 -0
  666. package/dist-server/server/modules/worktrees/tests/worktree-create.service.test.js.map +1 -0
  667. package/dist-server/server/modules/worktrees/tests/worktree-git.service.test.js +58 -0
  668. package/dist-server/server/modules/worktrees/tests/worktree-git.service.test.js.map +1 -0
  669. package/dist-server/server/modules/worktrees/tests/worktree-list.service.test.js +34 -0
  670. package/dist-server/server/modules/worktrees/tests/worktree-list.service.test.js.map +1 -0
  671. package/dist-server/server/modules/worktrees/tests/worktree-merge.service.test.js +137 -0
  672. package/dist-server/server/modules/worktrees/tests/worktree-merge.service.test.js.map +1 -0
  673. package/dist-server/server/modules/worktrees/tests/worktree-open.service.test.js +99 -0
  674. package/dist-server/server/modules/worktrees/tests/worktree-open.service.test.js.map +1 -0
  675. package/dist-server/server/modules/worktrees/tests/worktree-remove.service.test.js +157 -0
  676. package/dist-server/server/modules/worktrees/tests/worktree-remove.service.test.js.map +1 -0
  677. package/dist-server/server/modules/worktrees/tests/worktrees.routes.test.js +167 -0
  678. package/dist-server/server/modules/worktrees/tests/worktrees.routes.test.js.map +1 -0
  679. package/dist-server/server/modules/worktrees/worktrees.module.js +97 -0
  680. package/dist-server/server/modules/worktrees/worktrees.module.js.map +1 -0
  681. package/dist-server/server/modules/worktrees/worktrees.routes.js +84 -0
  682. package/dist-server/server/modules/worktrees/worktrees.routes.js.map +1 -0
  683. package/dist-server/server/piecemaker/anonymizer/lifecycle.js +20 -0
  684. package/dist-server/server/piecemaker/anonymizer/lifecycle.js.map +1 -0
  685. package/dist-server/server/piecemaker/harness/chat-harness.js +157 -0
  686. package/dist-server/server/piecemaker/harness/chat-harness.js.map +1 -0
  687. package/dist-server/server/piecemaker/harness/citation-instructions.js +17 -0
  688. package/dist-server/server/piecemaker/harness/citation-instructions.js.map +1 -0
  689. package/dist-server/server/piecemaker/harness/citation-store.js +72 -0
  690. package/dist-server/server/piecemaker/harness/citation-store.js.map +1 -0
  691. package/dist-server/server/piecemaker/harness/citation-turn.js +211 -0
  692. package/dist-server/server/piecemaker/harness/citation-turn.js.map +1 -0
  693. package/dist-server/server/piecemaker/harness/citations.routes.js +16 -0
  694. package/dist-server/server/piecemaker/harness/citations.routes.js.map +1 -0
  695. package/dist-server/server/piecemaker/harness/source-passages.js +19 -0
  696. package/dist-server/server/piecemaker/harness/source-passages.js.map +1 -0
  697. package/dist-server/server/piecemaker/index.js +49 -0
  698. package/dist-server/server/piecemaker/index.js.map +1 -0
  699. package/dist-server/server/piecemaker/knowledge/index.js +20 -0
  700. package/dist-server/server/piecemaker/knowledge/index.js.map +1 -0
  701. package/dist-server/server/piecemaker/knowledge/pipeline.js +162 -0
  702. package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -0
  703. package/dist-server/server/piecemaker/knowledge/routes.js +26 -0
  704. package/dist-server/server/piecemaker/knowledge/routes.js.map +1 -0
  705. package/dist-server/server/piecemaker/knowledge/service.js +78 -0
  706. package/dist-server/server/piecemaker/knowledge/service.js.map +1 -0
  707. package/dist-server/server/piecemaker/library/index.js +66 -0
  708. package/dist-server/server/piecemaker/library/index.js.map +1 -0
  709. package/dist-server/server/piecemaker/library/marketplace.js +258 -0
  710. package/dist-server/server/piecemaker/library/marketplace.js.map +1 -0
  711. package/dist-server/server/piecemaker/library/migrate-cli.js +13 -0
  712. package/dist-server/server/piecemaker/library/migrate-cli.js.map +1 -0
  713. package/dist-server/server/piecemaker/library/migrate.js +53 -0
  714. package/dist-server/server/piecemaker/library/migrate.js.map +1 -0
  715. package/dist-server/server/piecemaker/library/provider-skills.js +40 -0
  716. package/dist-server/server/piecemaker/library/provider-skills.js.map +1 -0
  717. package/dist-server/server/piecemaker/library/routes.js +83 -0
  718. package/dist-server/server/piecemaker/library/routes.js.map +1 -0
  719. package/dist-server/server/piecemaker/library/runtime.js +34 -0
  720. package/dist-server/server/piecemaker/library/runtime.js.map +1 -0
  721. package/dist-server/server/piecemaker/library/store.js +468 -0
  722. package/dist-server/server/piecemaker/library/store.js.map +1 -0
  723. package/dist-server/server/piecemaker/library/workspace-installation.js +70 -0
  724. package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -0
  725. package/dist-server/server/piecemaker/timesheet/index.js +9 -0
  726. package/dist-server/server/piecemaker/timesheet/index.js.map +1 -0
  727. package/dist-server/server/piecemaker/timesheet/routes.js +42 -0
  728. package/dist-server/server/piecemaker/timesheet/routes.js.map +1 -0
  729. package/dist-server/server/piecemaker/timesheet/service.js +241 -0
  730. package/dist-server/server/piecemaker/timesheet/service.js.map +1 -0
  731. package/dist-server/server/piecemaker/timesheet/store.js +126 -0
  732. package/dist-server/server/piecemaker/timesheet/store.js.map +1 -0
  733. package/dist-server/server/shared/claude-cli-path.js +120 -0
  734. package/dist-server/server/shared/claude-cli-path.js.map +1 -0
  735. package/dist-server/server/shared/frontmatter.js +16 -0
  736. package/dist-server/server/shared/frontmatter.js.map +1 -0
  737. package/dist-server/server/shared/image-attachments.js +344 -0
  738. package/dist-server/server/shared/image-attachments.js.map +1 -0
  739. package/dist-server/server/shared/index.js +3 -0
  740. package/dist-server/server/shared/index.js.map +1 -0
  741. package/dist-server/server/shared/interfaces.js +2 -0
  742. package/dist-server/server/shared/interfaces.js.map +1 -0
  743. package/dist-server/server/shared/message-unification.js +376 -0
  744. package/dist-server/server/shared/message-unification.js.map +1 -0
  745. package/dist-server/server/shared/tests/application-data-root.test.js +33 -0
  746. package/dist-server/server/shared/tests/application-data-root.test.js.map +1 -0
  747. package/dist-server/server/shared/tests/claude-cli-path.test.js +89 -0
  748. package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
  749. package/dist-server/server/shared/tests/image-attachments.test.js +262 -0
  750. package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
  751. package/dist-server/server/shared/tests/message-unification.test.js +174 -0
  752. package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
  753. package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
  754. package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
  755. package/dist-server/server/shared/types.js +2 -0
  756. package/dist-server/server/shared/types.js.map +1 -0
  757. package/dist-server/server/shared/utils.js +1010 -0
  758. package/dist-server/server/shared/utils.js.map +1 -0
  759. package/dist-server/shared/networkHosts.js +20 -0
  760. package/dist-server/shared/networkHosts.js.map +1 -0
  761. package/dist-server/shared/product-config.mjs +79 -0
  762. package/dist-server/shared/product-config.mjs.map +1 -0
  763. package/package.json +271 -0
  764. package/product.config.json +17 -0
  765. package/public/api-docs.html +901 -0
  766. package/scripts/fix-node-pty.js +67 -0
  767. package/scripts/piecemaker/cli/install-command.mjs +130 -0
  768. package/scripts/piecemaker/cli/lib/composants.mjs +67 -0
  769. package/scripts/piecemaker/cli/lib/config.mjs +32 -0
  770. package/scripts/piecemaker/cli/lib/exec.mjs +79 -0
  771. package/scripts/piecemaker/cli/lib/node-runtime.mjs +76 -0
  772. package/scripts/piecemaker/cli/lib/plugins.mjs +35 -0
  773. package/scripts/piecemaker/cli/lib/ports.mjs +96 -0
  774. package/scripts/piecemaker/cli/lib/pwa.mjs +323 -0
  775. package/scripts/piecemaker/cli/lib/repos.mjs +140 -0
  776. package/scripts/piecemaker/cli/lib/services.mjs +71 -0
  777. package/scripts/piecemaker/cli/lib/ui.mjs +41 -0
  778. package/scripts/piecemaker/cli/piecemaker.mjs +161 -0
  779. package/scripts/piecemaker/cli/piecemaker.ps1 +63 -0
  780. package/scripts/piecemaker/cli/piecemaker.sh +60 -0
  781. package/scripts/piecemaker/generate-i18n-overrides.mjs +219 -0
  782. package/scripts/piecemaker/hooks/proxy-guard.mjs +286 -0
  783. package/scripts/promote-dist-server.mjs +50 -0
  784. package/scripts/release/build-server-bundle.js +178 -0
  785. package/scripts/release/package-piecemaker.sh +73 -0
  786. package/scripts/release/prepare-desktop-app.js +168 -0
  787. package/server/index.ts +410 -0
  788. package/server/load-env.ts +48 -0
  789. package/server/modules/agent/agent.module.ts +52 -0
  790. package/server/modules/agent/agent.routes.ts +1287 -0
  791. package/server/modules/agent/index.ts +2 -0
  792. package/server/modules/agent/tests/agent.routes.test.ts +237 -0
  793. package/server/modules/assets/assets.routes.ts +150 -0
  794. package/server/modules/assets/index.ts +3 -0
  795. package/server/modules/assets/services/image-assets.service.ts +127 -0
  796. package/server/modules/assets/tests/image-assets.service.test.ts +74 -0
  797. package/server/modules/auth/auth.middleware.ts +163 -0
  798. package/server/modules/auth/auth.module.ts +38 -0
  799. package/server/modules/auth/auth.routes.ts +57 -0
  800. package/server/modules/auth/auth.service.ts +155 -0
  801. package/server/modules/auth/index.ts +9 -0
  802. package/server/modules/auth/tests/auth.service.test.ts +95 -0
  803. package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
  804. package/server/modules/browser-use/browser-use-mcp.ts +386 -0
  805. package/server/modules/browser-use/browser-use-runtime.ts +10 -0
  806. package/server/modules/browser-use/browser-use.routes.ts +159 -0
  807. package/server/modules/browser-use/browser-use.service.ts +835 -0
  808. package/server/modules/browser-use/index.ts +11 -0
  809. package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
  810. package/server/modules/cli/cli.module.ts +110 -0
  811. package/server/modules/cli/cli.service.ts +266 -0
  812. package/server/modules/cli/cli.ts +19 -0
  813. package/server/modules/cli/index.ts +2 -0
  814. package/server/modules/cli/pwa-launcher.ts +131 -0
  815. package/server/modules/cli/sandbox.service.ts +413 -0
  816. package/server/modules/cli/tests/cli-environment-bootstrap.test.ts +186 -0
  817. package/server/modules/cli/tests/cli.service.test.ts +107 -0
  818. package/server/modules/cli/tests/sandbox.service.test.ts +61 -0
  819. package/server/modules/commands/commands.module.ts +22 -0
  820. package/server/modules/commands/commands.routes.ts +605 -0
  821. package/server/modules/commands/index.ts +2 -0
  822. package/server/modules/commands/tests/commands.test.ts +114 -0
  823. package/server/modules/database/connection.ts +144 -0
  824. package/server/modules/database/index.ts +27 -0
  825. package/server/modules/database/init-db.ts +17 -0
  826. package/server/modules/database/migrations.ts +552 -0
  827. package/server/modules/database/repositories/api-keys.ts +119 -0
  828. package/server/modules/database/repositories/app-config.ts +53 -0
  829. package/server/modules/database/repositories/credentials.ts +106 -0
  830. package/server/modules/database/repositories/github-tokens.ts +100 -0
  831. package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
  832. package/server/modules/database/repositories/notification-preferences.ts +117 -0
  833. package/server/modules/database/repositories/projects.db.ts +196 -0
  834. package/server/modules/database/repositories/provider-models.ts +168 -0
  835. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  836. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  837. package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
  838. package/server/modules/database/repositories/session-drafts.db.ts +171 -0
  839. package/server/modules/database/repositories/sessions.db.ts +717 -0
  840. package/server/modules/database/repositories/user-preferences.db.ts +80 -0
  841. package/server/modules/database/repositories/users.ts +140 -0
  842. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  843. package/server/modules/database/schema.ts +306 -0
  844. package/server/modules/database/tests/projects.db.integration.test.ts +72 -0
  845. package/server/modules/database/tests/provider-models.db.integration.test.ts +125 -0
  846. package/server/modules/database/tests/sessions-provider-mapping.test.ts +109 -0
  847. package/server/modules/database/tests/sessions.db.integration.test.ts +168 -0
  848. package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
  849. package/server/modules/file-tree/file-tree.module.ts +116 -0
  850. package/server/modules/file-tree/file-tree.routes.ts +258 -0
  851. package/server/modules/file-tree/file-tree.service.ts +675 -0
  852. package/server/modules/file-tree/index.ts +2 -0
  853. package/server/modules/file-tree/tests/file-tree.routes.test.ts +157 -0
  854. package/server/modules/file-tree/tests/file-tree.service.test.ts +371 -0
  855. package/server/modules/git/git-branch.service.ts +43 -0
  856. package/server/modules/git/git-parsing.service.ts +66 -0
  857. package/server/modules/git/git.module.ts +22 -0
  858. package/server/modules/git/git.routes.ts +1600 -0
  859. package/server/modules/git/index.ts +2 -0
  860. package/server/modules/git/tests/git-branch.service.test.ts +60 -0
  861. package/server/modules/git/tests/git-init.routes.test.ts +135 -0
  862. package/server/modules/git/tests/git.test.ts +106 -0
  863. package/server/modules/notifications/index.ts +24 -0
  864. package/server/modules/notifications/notifications.routes.ts +127 -0
  865. package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
  866. package/server/modules/notifications/services/notification-orchestrator.service.js +310 -0
  867. package/server/modules/notifications/tests/notification-orchestrator.integration.test.ts +55 -0
  868. package/server/modules/notifications/vapid-keys.service.ts +38 -0
  869. package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
  870. package/server/modules/plugins/index.ts +9 -0
  871. package/server/modules/plugins/plugin-process.service.ts +217 -0
  872. package/server/modules/plugins/plugin-registry.service.ts +461 -0
  873. package/server/modules/plugins/plugins.module.ts +33 -0
  874. package/server/modules/plugins/plugins.routes.ts +66 -0
  875. package/server/modules/plugins/plugins.service.ts +147 -0
  876. package/server/modules/plugins/tests/plugins.service.test.ts +32 -0
  877. package/server/modules/projects/index.ts +11 -0
  878. package/server/modules/projects/projects.routes.ts +273 -0
  879. package/server/modules/projects/services/project-clone.service.ts +323 -0
  880. package/server/modules/projects/services/project-delete.service.ts +102 -0
  881. package/server/modules/projects/services/project-management.service.ts +144 -0
  882. package/server/modules/projects/services/project-star.service.ts +78 -0
  883. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  884. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +314 -0
  885. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  886. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  887. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  888. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  889. package/server/modules/providers/README.md +380 -0
  890. package/server/modules/providers/index.ts +14 -0
  891. package/server/modules/providers/list/claude/claude-auth.provider.ts +180 -0
  892. package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
  893. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -0
  894. package/server/modules/providers/list/claude/claude-models.provider.ts +326 -0
  895. package/server/modules/providers/list/claude/claude-runtime.provider.js +1217 -0
  896. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +203 -0
  897. package/server/modules/providers/list/claude/claude-sessions.provider.ts +1108 -0
  898. package/server/modules/providers/list/claude/claude-skills.provider.ts +268 -0
  899. package/server/modules/providers/list/claude/claude.provider.ts +33 -0
  900. package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
  901. package/server/modules/providers/list/codex/codex-auth.provider.ts +100 -0
  902. package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
  903. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -0
  904. package/server/modules/providers/list/codex/codex-models.provider.ts +139 -0
  905. package/server/modules/providers/list/codex/codex-runtime.provider.ts +560 -0
  906. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +212 -0
  907. package/server/modules/providers/list/codex/codex-sessions.provider.ts +2267 -0
  908. package/server/modules/providers/list/codex/codex-skills.provider.ts +73 -0
  909. package/server/modules/providers/list/codex/codex.provider.ts +33 -0
  910. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -0
  911. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -0
  912. package/server/modules/providers/list/cursor/cursor-models.provider.ts +119 -0
  913. package/server/modules/providers/list/cursor/cursor-runtime.provider.js +387 -0
  914. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +158 -0
  915. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +654 -0
  916. package/server/modules/providers/list/cursor/cursor-skills.provider.ts +39 -0
  917. package/server/modules/providers/list/cursor/cursor.provider.ts +30 -0
  918. package/server/modules/providers/list/mistral/mistral-auth.provider.ts +181 -0
  919. package/server/modules/providers/list/mistral/mistral-mcp.provider.ts +20 -0
  920. package/server/modules/providers/list/mistral/mistral-models.provider.ts +76 -0
  921. package/server/modules/providers/list/mistral/mistral-runtime.provider.ts +417 -0
  922. package/server/modules/providers/list/mistral/mistral-session-synchronizer.provider.ts +15 -0
  923. package/server/modules/providers/list/mistral/mistral-sessions.provider.ts +210 -0
  924. package/server/modules/providers/list/mistral/mistral-skills.provider.ts +20 -0
  925. package/server/modules/providers/list/mistral/mistral.provider.ts +35 -0
  926. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +110 -0
  927. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
  928. package/server/modules/providers/list/opencode/opencode-models.provider.ts +476 -0
  929. package/server/modules/providers/list/opencode/opencode-runtime.provider.js +434 -0
  930. package/server/modules/providers/list/opencode/opencode-runtime.provider.test.js +261 -0
  931. package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +179 -0
  932. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +500 -0
  933. package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
  934. package/server/modules/providers/list/opencode/opencode.provider.ts +30 -0
  935. package/server/modules/providers/provider.registry.ts +38 -0
  936. package/server/modules/providers/provider.routes.ts +909 -0
  937. package/server/modules/providers/services/mcp.service.ts +109 -0
  938. package/server/modules/providers/services/provider-auth.service.ts +26 -0
  939. package/server/modules/providers/services/provider-capabilities.service.ts +133 -0
  940. package/server/modules/providers/services/provider-models.service.ts +401 -0
  941. package/server/modules/providers/services/provider-runtime.service.ts +108 -0
  942. package/server/modules/providers/services/provider-token-usage.service.ts +484 -0
  943. package/server/modules/providers/services/session-conversations-search.service.ts +1321 -0
  944. package/server/modules/providers/services/session-history-cache.service.ts +139 -0
  945. package/server/modules/providers/services/session-synchronizer.service.ts +162 -0
  946. package/server/modules/providers/services/sessions-watcher.service.ts +266 -0
  947. package/server/modules/providers/services/sessions.service.ts +663 -0
  948. package/server/modules/providers/services/skills.service.ts +39 -0
  949. package/server/modules/providers/shared/base/abstract.provider.ts +32 -0
  950. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -0
  951. package/server/modules/providers/shared/skills/skills.provider.ts +287 -0
  952. package/server/modules/providers/tests/claude-auth.test.ts +208 -0
  953. package/server/modules/providers/tests/claude-models.test.ts +87 -0
  954. package/server/modules/providers/tests/claude-sessions.test.ts +573 -0
  955. package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
  956. package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
  957. package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
  958. package/server/modules/providers/tests/codex-runtime.test.ts +58 -0
  959. package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
  960. package/server/modules/providers/tests/codex-sessions.test.ts +553 -0
  961. package/server/modules/providers/tests/mcp.test.ts +349 -0
  962. package/server/modules/providers/tests/opencode-models.test.ts +199 -0
  963. package/server/modules/providers/tests/opencode-sessions.test.ts +522 -0
  964. package/server/modules/providers/tests/provider-attachment-history.test.ts +247 -0
  965. package/server/modules/providers/tests/provider-models.service.test.ts +353 -0
  966. package/server/modules/providers/tests/provider-runtime.service.test.ts +138 -0
  967. package/server/modules/providers/tests/provider-token-usage.service.test.ts +373 -0
  968. package/server/modules/providers/tests/provider.routes.test.ts +244 -0
  969. package/server/modules/providers/tests/session-fork.test.ts +173 -0
  970. package/server/modules/providers/tests/session-history-cache.service.test.ts +143 -0
  971. package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
  972. package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
  973. package/server/modules/providers/tests/sessions-details.test.ts +73 -0
  974. package/server/modules/providers/tests/sessions.service.test.ts +193 -0
  975. package/server/modules/providers/tests/skills.test.ts +708 -0
  976. package/server/modules/scheduled-messages/index.ts +8 -0
  977. package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
  978. package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +203 -0
  979. package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
  980. package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +310 -0
  981. package/server/modules/settings/index.ts +2 -0
  982. package/server/modules/settings/settings.module.ts +50 -0
  983. package/server/modules/settings/settings.routes.ts +51 -0
  984. package/server/modules/settings/settings.service.ts +186 -0
  985. package/server/modules/settings/tests/settings.service.test.ts +54 -0
  986. package/server/modules/system/index.ts +2 -0
  987. package/server/modules/system/system.module.ts +62 -0
  988. package/server/modules/system/system.routes.ts +21 -0
  989. package/server/modules/system/system.service.ts +76 -0
  990. package/server/modules/system/tests/system.service.test.ts +118 -0
  991. package/server/modules/taskmaster/index.ts +2 -0
  992. package/server/modules/taskmaster/taskmaster.module.ts +24 -0
  993. package/server/modules/taskmaster/taskmaster.routes.ts +1472 -0
  994. package/server/modules/taskmaster/taskmaster.service.ts +132 -0
  995. package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +201 -0
  996. package/server/modules/taskmaster/tests/taskmaster.service.test.ts +97 -0
  997. package/server/modules/user/index.ts +2 -0
  998. package/server/modules/user/tests/user.service.test.ts +151 -0
  999. package/server/modules/user/user.module.ts +67 -0
  1000. package/server/modules/user/user.routes.ts +94 -0
  1001. package/server/modules/user/user.service.ts +190 -0
  1002. package/server/modules/voice/index.ts +2 -0
  1003. package/server/modules/voice/tests/voice.service.test.ts +99 -0
  1004. package/server/modules/voice/voice.module.ts +47 -0
  1005. package/server/modules/voice/voice.routes.ts +113 -0
  1006. package/server/modules/voice/voice.service.ts +192 -0
  1007. package/server/modules/websocket/README.md +273 -0
  1008. package/server/modules/websocket/index.ts +11 -0
  1009. package/server/modules/websocket/services/chat-run-registry.service.ts +310 -0
  1010. package/server/modules/websocket/services/chat-session-writer.service.ts +175 -0
  1011. package/server/modules/websocket/services/chat-websocket.service.ts +654 -0
  1012. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  1013. package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
  1014. package/server/modules/websocket/services/shell-websocket.service.ts +628 -0
  1015. package/server/modules/websocket/services/websocket-auth.service.ts +59 -0
  1016. package/server/modules/websocket/services/websocket-server.service.ts +126 -0
  1017. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  1018. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  1019. package/server/modules/websocket/tests/chat-attachment-filter.test.ts +59 -0
  1020. package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
  1021. package/server/modules/websocket/tests/chat-run-registry.test.ts +313 -0
  1022. package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
  1023. package/server/modules/websocket/tests/shell-websocket.service.test.ts +227 -0
  1024. package/server/modules/websocket/tests/websocket-heartbeat.service.test.ts +78 -0
  1025. package/server/modules/worktrees/index.ts +2 -0
  1026. package/server/modules/worktrees/services/worktree-create-and-open.service.ts +53 -0
  1027. package/server/modules/worktrees/services/worktree-create.service.ts +104 -0
  1028. package/server/modules/worktrees/services/worktree-git.service.ts +207 -0
  1029. package/server/modules/worktrees/services/worktree-list.service.ts +143 -0
  1030. package/server/modules/worktrees/services/worktree-merge.service.ts +157 -0
  1031. package/server/modules/worktrees/services/worktree-open.service.ts +85 -0
  1032. package/server/modules/worktrees/services/worktree-remove.service.ts +88 -0
  1033. package/server/modules/worktrees/tests/worktree-create-and-open.service.test.ts +68 -0
  1034. package/server/modules/worktrees/tests/worktree-create.service.test.ts +142 -0
  1035. package/server/modules/worktrees/tests/worktree-git.service.test.ts +84 -0
  1036. package/server/modules/worktrees/tests/worktree-list.service.test.ts +45 -0
  1037. package/server/modules/worktrees/tests/worktree-merge.service.test.ts +213 -0
  1038. package/server/modules/worktrees/tests/worktree-open.service.test.ts +122 -0
  1039. package/server/modules/worktrees/tests/worktree-remove.service.test.ts +227 -0
  1040. package/server/modules/worktrees/tests/worktrees.routes.test.ts +199 -0
  1041. package/server/modules/worktrees/worktrees.module.ts +113 -0
  1042. package/server/modules/worktrees/worktrees.routes.ts +116 -0
  1043. package/server/piecemaker/activation/claude-mcp.cjs +115 -0
  1044. package/server/piecemaker/activation/claude-plugins.cjs +94 -0
  1045. package/server/piecemaker/activation/codex-mcp.cjs +102 -0
  1046. package/server/piecemaker/activation/errors.cjs +15 -0
  1047. package/server/piecemaker/activation/index.cjs +5 -0
  1048. package/server/piecemaker/activation/library.cjs +284 -0
  1049. package/server/piecemaker/activation/markdown.cjs +108 -0
  1050. package/server/piecemaker/activation/paths.cjs +104 -0
  1051. package/server/piecemaker/activation/routes.cjs +101 -0
  1052. package/server/piecemaker/activation/snapshot.cjs +30 -0
  1053. package/server/piecemaker/anonymizer/client-config.cjs +228 -0
  1054. package/server/piecemaker/anonymizer/dictionary.cjs +171 -0
  1055. package/server/piecemaker/anonymizer/lifecycle.ts +22 -0
  1056. package/server/piecemaker/anonymizer/providers.cjs +241 -0
  1057. package/server/piecemaker/anonymizer/proxy.cjs +365 -0
  1058. package/server/piecemaker/anonymizer/rewrite.cjs +343 -0
  1059. package/server/piecemaker/anonymizer/routes.cjs +43 -0
  1060. package/server/piecemaker/anonymizer/service.cjs +223 -0
  1061. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +121 -0
  1062. package/server/piecemaker/harness/chat-harness.ts +151 -0
  1063. package/server/piecemaker/harness/citation-instructions.ts +18 -0
  1064. package/server/piecemaker/harness/citation-store.ts +80 -0
  1065. package/server/piecemaker/harness/citation-turn.ts +197 -0
  1066. package/server/piecemaker/harness/citations.routes.ts +18 -0
  1067. package/server/piecemaker/harness/decisions.cjs +302 -0
  1068. package/server/piecemaker/harness/flux-sse.cjs +111 -0
  1069. package/server/piecemaker/harness/index.cjs +103 -0
  1070. package/server/piecemaker/harness/source-passages.ts +21 -0
  1071. package/server/piecemaker/harness/verification.cjs +159 -0
  1072. package/server/piecemaker/index.ts +62 -0
  1073. package/server/piecemaker/knowledge/index.ts +22 -0
  1074. package/server/piecemaker/knowledge/pipeline.ts +173 -0
  1075. package/server/piecemaker/knowledge/routes.ts +29 -0
  1076. package/server/piecemaker/knowledge/service.ts +80 -0
  1077. package/server/piecemaker/library/index.ts +54 -0
  1078. package/server/piecemaker/library/marketplace.ts +200 -0
  1079. package/server/piecemaker/library/migrate-cli.ts +11 -0
  1080. package/server/piecemaker/library/migrate.ts +49 -0
  1081. package/server/piecemaker/library/policy.cjs +9 -0
  1082. package/server/piecemaker/library/provider-skills.ts +48 -0
  1083. package/server/piecemaker/library/routes.ts +51 -0
  1084. package/server/piecemaker/library/runtime.ts +39 -0
  1085. package/server/piecemaker/library/store.ts +456 -0
  1086. package/server/piecemaker/library/workspace-installation.ts +61 -0
  1087. package/server/piecemaker/originals-cache.cjs +54 -0
  1088. package/server/piecemaker/protection/bypass.cjs +73 -0
  1089. package/server/piecemaker/protection/routes.cjs +39 -0
  1090. package/server/piecemaker/router.cjs +107 -0
  1091. package/server/piecemaker/timesheet/index.ts +9 -0
  1092. package/server/piecemaker/timesheet/routes.ts +48 -0
  1093. package/server/piecemaker/timesheet/service.ts +298 -0
  1094. package/server/piecemaker/timesheet/store.ts +167 -0
  1095. package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +189 -0
  1096. package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +230 -0
  1097. package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-secret-paths.default.json +1 -0
  1098. package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
  1099. package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
  1100. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +1066 -0
  1101. package/server/piecemaker/vendor/installer/lib/banner.mjs +35 -0
  1102. package/server/piecemaker/vendor/installer/lib/codex-skills.mjs +343 -0
  1103. package/server/piecemaker/vendor/installer/lib/commandes.mjs +13 -0
  1104. package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +305 -0
  1105. package/server/piecemaker/vendor/installer/lib/legacy-legifrance.mjs +72 -0
  1106. package/server/piecemaker/vendor/installer/lib/platform.mjs +163 -0
  1107. package/server/piecemaker/vendor/installer/lib/prompt.mjs +138 -0
  1108. package/server/piecemaker/vendor/installer/lib/resume-steps.mjs +174 -0
  1109. package/server/piecemaker/vendor/installer/lib/secrets-guard.mjs +193 -0
  1110. package/server/piecemaker/vendor/installer/lib/service.mjs +467 -0
  1111. package/server/piecemaker/vendor/installer/lib/state.mjs +113 -0
  1112. package/server/piecemaker/vendor/installer/lib/ui.mjs +159 -0
  1113. package/server/piecemaker/vendor/installer/steps/00-identite.mjs +71 -0
  1114. package/server/piecemaker/vendor/installer/steps/01-prerequis.mjs +119 -0
  1115. package/server/piecemaker/vendor/installer/steps/02-dependances-node.mjs +127 -0
  1116. package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +260 -0
  1117. package/server/piecemaker/vendor/installer/steps/04-conversion-md.mjs +147 -0
  1118. package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +171 -0
  1119. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +266 -0
  1120. package/server/piecemaker/vendor/installer/steps/07-legifrance.mjs +279 -0
  1121. package/server/piecemaker/vendor/installer/steps/08-telegram.mjs +482 -0
  1122. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +151 -0
  1123. package/server/piecemaker/vendor/installer/steps/09-codex-plugin.mjs +142 -0
  1124. package/server/piecemaker/vendor/installer/steps/10-libreoffice.mjs +130 -0
  1125. package/server/piecemaker/vendor/installer/steps/10-pandoc.mjs +212 -0
  1126. package/server/piecemaker/vendor/installer/steps/11-docx-cli.mjs +144 -0
  1127. package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +124 -0
  1128. package/server/piecemaker/vendor/installer/steps/13-garde-secrets.mjs +136 -0
  1129. package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +271 -0
  1130. package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +64 -0
  1131. package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +56 -0
  1132. package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +52 -0
  1133. package/server/piecemaker/vendor/mcp/README.md +13 -0
  1134. package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +217 -0
  1135. package/server/piecemaker/vendor/mike-defaults-fr/PROVENANCE.md +10 -0
  1136. package/server/piecemaker/vendor/mike-defaults-fr/assistant-workflows/compare-documents/SKILL.md +51 -0
  1137. package/server/piecemaker/vendor/mike-defaults-fr/assistant-workflows/draft-from-template/SKILL.md +27 -0
  1138. package/server/piecemaker/vendor/mike-defaults-fr/assistant-workflows/extract-key-terms/SKILL.md +43 -0
  1139. package/server/piecemaker/vendor/mike-defaults-fr/assistant-workflows/proofread/SKILL.md +98 -0
  1140. package/server/piecemaker/vendor/mike-defaults-fr/catalog.json +22 -0
  1141. package/server/piecemaker/vendor/mike-defaults-fr/tabular-review-workflows/commercial-agreement-tabular-review/SKILL.md +31 -0
  1142. package/server/piecemaker/vendor/mike-defaults-fr/tabular-review-workflows/commercial-agreement-tabular-review/table-columns.yaml +92 -0
  1143. package/server/piecemaker/vendor/package.json +7 -0
  1144. package/server/piecemaker/vendor/piecemaker-plugin/README.md +115 -0
  1145. package/server/piecemaker/vendor/piecemaker-plugin/agents/analyste-piece.md +24 -0
  1146. package/server/piecemaker/vendor/piecemaker-plugin/agents/jurisprudence-scanner.md +50 -0
  1147. package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +84 -0
  1148. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +194 -0
  1149. package/server/piecemaker/vendor/piecemaker-plugin/scripts/build-recherche-pdf.mjs +91 -0
  1150. package/server/piecemaker/vendor/piecemaker-plugin/scripts/classify-ai-documents.mjs +123 -0
  1151. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +71 -0
  1152. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +107 -0
  1153. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/anonymization-state.cjs +146 -0
  1154. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folder-structure.cjs +297 -0
  1155. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +86 -0
  1156. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/central-mapping.cjs +0 -0
  1157. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/citation-sources.cjs +519 -0
  1158. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/citations.cjs +292 -0
  1159. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +1683 -0
  1160. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/docx-text-worker.cjs +43 -0
  1161. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +249 -0
  1162. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +160 -0
  1163. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/legifrance-reads.cjs +85 -0
  1164. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +346 -0
  1165. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +419 -0
  1166. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/recherche-report.cjs +177 -0
  1167. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +80 -0
  1168. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/substitution.cjs +288 -0
  1169. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/verify-citations.cjs +318 -0
  1170. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +256 -0
  1171. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +260 -0
  1172. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit.mjs +83 -0
  1173. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +179 -0
  1174. package/server/piecemaker/vendor/piecemaker-plugin/scripts/track-legifrance-reads.mjs +46 -0
  1175. package/server/piecemaker/vendor/piecemaker-plugin/skills/ajout-de-fonctionnalites/SKILL.md +66 -0
  1176. package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +60 -0
  1177. package/server/piecemaker/vendor/piecemaker-plugin/skills/recherche-juridique/SKILL.md +234 -0
  1178. package/server/piecemaker/vendor/piecemaker-plugin/skills/redaction-juridique/SKILL.md +96 -0
  1179. package/server/piecemaker/vendor/piecemaker-plugin/skills/tamponnage/SKILL.md +148 -0
  1180. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +3116 -0
  1181. package/server/piecemaker/vendor/websocket-server/case-folder-structure.cjs +3 -0
  1182. package/server/piecemaker/vendor/websocket-server/case-instructions.cjs +111 -0
  1183. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +93 -0
  1184. package/server/piecemaker/vendor/websocket-server/claude-assets.cjs +338 -0
  1185. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +380 -0
  1186. package/server/piecemaker/vendor/websocket-server/document-index.cjs +665 -0
  1187. package/server/piecemaker/vendor/websocket-server/legal-forms.cjs +89 -0
  1188. package/server/piecemaker/vendor/websocket-server/lib/doc-generate.cjs +229 -0
  1189. package/server/piecemaker/vendor/websocket-server/lib/export-render.cjs +440 -0
  1190. package/server/piecemaker/vendor/websocket-server/lib/office-to-pdf.cjs +306 -0
  1191. package/server/piecemaker/vendor/websocket-server/lib/stamping.cjs +39 -0
  1192. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +1150 -0
  1193. package/server/piecemaker/vendor/websocket-server/scripts/ADDRESS_ANONYMIZATION.md +216 -0
  1194. package/server/piecemaker/vendor/websocket-server/scripts/DEDUPLICATION_CHANGES.md +134 -0
  1195. package/server/piecemaker/vendor/websocket-server/scripts/GLINER2_README.md +138 -0
  1196. package/server/piecemaker/vendor/websocket-server/scripts/__pycache__/scan_utils.cpython-312.pyc +0 -0
  1197. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +1968 -0
  1198. package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +208 -0
  1199. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/AUDIT_PERF_QUALITE.md +246 -0
  1200. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/MODELE_COREML.md +62 -0
  1201. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/__pycache__/coreml_runtime.cpython-312.pyc +0 -0
  1202. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/__pycache__/model_config.cpython-312.pyc +0 -0
  1203. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/build_coreml.py +118 -0
  1204. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/coreml_runtime.py +164 -0
  1205. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/model_config.py +63 -0
  1206. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/presidio-gliner.py +479 -0
  1207. package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +668 -0
  1208. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__init__.py +10 -0
  1209. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/__init__.cpython-312.pyc +0 -0
  1210. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/analysis_explanation.cpython-312.pyc +0 -0
  1211. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/entity_recognizer.cpython-312.pyc +0 -0
  1212. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/local_recognizer.cpython-312.pyc +0 -0
  1213. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/pattern.cpython-312.pyc +0 -0
  1214. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/pattern_recognizer.cpython-312.pyc +0 -0
  1215. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/__pycache__/recognizer_result.cpython-312.pyc +0 -0
  1216. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/analysis_explanation.py +65 -0
  1217. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/entity_recognizer.py +213 -0
  1218. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/input_validation/__init__.py +21 -0
  1219. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/input_validation/language_validation.py +18 -0
  1220. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/input_validation/schemas.py +132 -0
  1221. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/input_validation/yaml_recognizer_models.py +422 -0
  1222. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/local_recognizer.py +7 -0
  1223. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__init__.py +15 -0
  1224. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__pycache__/__init__.cpython-312.pyc +0 -0
  1225. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__pycache__/device_detector.cpython-312.pyc +0 -0
  1226. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__pycache__/ner_model_configuration.cpython-312.pyc +0 -0
  1227. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__pycache__/nlp_artifacts.cpython-312.pyc +0 -0
  1228. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__pycache__/nlp_engine.cpython-312.pyc +0 -0
  1229. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/__pycache__/spacy_nlp_engine.cpython-312.pyc +0 -0
  1230. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/device_detector.py +65 -0
  1231. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/ner_model_configuration.py +128 -0
  1232. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/nlp_artifacts.py +88 -0
  1233. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/nlp_engine.py +65 -0
  1234. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/nlp_engine_provider.py +114 -0
  1235. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/nlp_engine/spacy_nlp_engine.py +290 -0
  1236. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/pattern.py +66 -0
  1237. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/pattern_recognizer.py +297 -0
  1238. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/__init__.py +4 -0
  1239. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/__pycache__/__init__.cpython-312.pyc +0 -0
  1240. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/__pycache__/gliner_recognizer.cpython-312.pyc +0 -0
  1241. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__init__.py +0 -0
  1242. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/__init__.cpython-312.pyc +0 -0
  1243. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/credit_card_recognizer.cpython-312.pyc +0 -0
  1244. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/crypto_recognizer.cpython-312.pyc +0 -0
  1245. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/email_recognizer.cpython-312.pyc +0 -0
  1246. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/iban_patterns.cpython-312.pyc +0 -0
  1247. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/iban_recognizer.cpython-312.pyc +0 -0
  1248. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/ip_recognizer.cpython-312.pyc +0 -0
  1249. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/mac_recognizer.cpython-312.pyc +0 -0
  1250. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/__pycache__/url_recognizer.cpython-312.pyc +0 -0
  1251. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/credit_card_recognizer.py +82 -0
  1252. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/crypto_recognizer.py +141 -0
  1253. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/date_recognizer.py +102 -0
  1254. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/email_recognizer.py +48 -0
  1255. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/iban_patterns.py +173 -0
  1256. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/iban_recognizer.py +221 -0
  1257. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/ip_recognizer.py +65 -0
  1258. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/mac_recognizer.py +73 -0
  1259. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/generic/url_recognizer.py +50 -0
  1260. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/gliner_recognizer.py +218 -0
  1261. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/predefined_recognizers/spacy_recognizer.py +139 -0
  1262. package/server/piecemaker/vendor/websocket-server/scripts/presidio_analyzer/recognizer_result.py +188 -0
  1263. package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +29 -0
  1264. package/server/piecemaker/vendor/websocket-server/scripts/scan_utils.py +469 -0
  1265. package/server/piecemaker/vendor/websocket-server/scripts/smart_converter.py +366 -0
  1266. package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +464 -0
  1267. package/server/piecemaker/vendor/websocket-server/stamping-routes.cjs +334 -0
  1268. package/server/piecemaker/vendor/websocket-server/telegram-admin.cjs +602 -0
  1269. package/server/piecemaker/vendor/websocket-server/workspace-paths.cjs +22 -0
  1270. package/server/shared/claude-cli-path.ts +157 -0
  1271. package/server/shared/frontmatter.ts +18 -0
  1272. package/server/shared/image-attachments.ts +437 -0
  1273. package/server/shared/index.ts +3 -0
  1274. package/server/shared/interfaces.ts +241 -0
  1275. package/server/shared/message-unification.ts +449 -0
  1276. package/server/shared/tests/application-data-root.test.ts +29 -0
  1277. package/server/shared/tests/claude-cli-path.test.ts +118 -0
  1278. package/server/shared/tests/image-attachments.test.ts +353 -0
  1279. package/server/shared/tests/message-unification.test.ts +218 -0
  1280. package/server/shared/tests/slice-tail-page.test.ts +42 -0
  1281. package/server/shared/types.ts +1354 -0
  1282. package/server/shared/utils.ts +1212 -0
  1283. package/server/tsconfig.json +40 -0
  1284. package/shared/networkHosts.js +22 -0
  1285. package/shared/product-config.mjs +86 -0
@@ -0,0 +1,3116 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const crypto = require('node:crypto');
5
+ const { spawn, spawnSync } = require('child_process');
6
+ const { performance } = require('node:perf_hooks');
7
+ const {
8
+ listConfiguredCases,
9
+ readRegistryConfig,
10
+ registerCaseFolder,
11
+ resolveCaseReference,
12
+ validateSelectedCaseFolder,
13
+ } = require('./case-registry.cjs');
14
+ const { stampedPiecesDirectory } = require('./lib/stamping.cjs');
15
+ const {
16
+ COMMIT_USER_NAME_KEY,
17
+ caseOverview,
18
+ checkoutHistoryBranch,
19
+ createCommit,
20
+ createHistoryBranch,
21
+ historyBranches,
22
+ historyMonths,
23
+ isTechnicalCaseDirectoryName,
24
+ listCases,
25
+ listHistory,
26
+ listHistoryPeriod,
27
+ resolveCasesRoot,
28
+ resolveCommitIdentity,
29
+ restoreRevision,
30
+ revisionDetails,
31
+ safeCaseFiles,
32
+ worktreeDetails,
33
+ } = require('../piecemaker-plugin/scripts/lib/commits.cjs');
34
+ const {
35
+ cancelOriginalsJob,
36
+ getJob,
37
+ listOriginals,
38
+ readCaseMapping,
39
+ rebuildCaseMapping,
40
+ saveCaseMapping,
41
+ startOriginalsJob,
42
+ writeCaseMapping,
43
+ } = require('./originals-pipeline.cjs');
44
+ const { invalidateOriginals, listOriginalsCached } = require('../../originals-cache.cjs');
45
+ const {
46
+ documentKey,
47
+ readProtection,
48
+ writeProtection,
49
+ } = require('../piecemaker-plugin/scripts/lib/protection.cjs');
50
+ const { stateKey } = require('../piecemaker-plugin/scripts/lib/anonymization-state.cjs');
51
+ const {
52
+ applyDocumentIndexCorrection,
53
+ buildChronology,
54
+ documentIndexFile,
55
+ readDocumentIndex,
56
+ } = require('./document-index.cjs');
57
+ const { renderChronologyHtml, renderHistoryHtml } = require('./lib/export-render.cjs');
58
+ const { outputExtension } = require('./lib/office-to-pdf.cjs');
59
+ const { generateDocument } = require('./lib/doc-generate.cjs');
60
+ const {
61
+ readInstitutionalTerms,
62
+ writeInstitutionalTerms,
63
+ } = require('../piecemaker-plugin/scripts/lib/institutional-terms.cjs');
64
+ const {
65
+ claudeAssetOf,
66
+ claudeAssetStatus,
67
+ registerClaudeAsset,
68
+ repositoryAssets,
69
+ syncClaudeAssets,
70
+ unregisterClaudeAsset,
71
+ } = require('./claude-assets.cjs');
72
+ const { claudeHooksStatus } = require('./claude-hooks.cjs');
73
+ const {
74
+ controlDossierBot,
75
+ controlTelegram,
76
+ getTelegramState,
77
+ saveDossierBot,
78
+ saveTelegramConfig,
79
+ } = require('./telegram-admin.cjs');
80
+ const {
81
+ DEFAULT_CASE_FOLDER_STRUCTURE,
82
+ configuredCaseFolderStructure,
83
+ ensureCaseFolderStructure,
84
+ normalizeCaseFolderStructure,
85
+ } = require('./case-folder-structure.cjs');
86
+
87
+ const MAX_MARKDOWN_BYTES = 1024 * 1024;
88
+ const SECRET_KEYS = new Set([
89
+ 'LEGIFRANCE_CLIENT_ID',
90
+ 'LEGIFRANCE_CLIENT_SECRET',
91
+ ]);
92
+ const ENV_KEYS = new Set([
93
+ COMMIT_USER_NAME_KEY,
94
+ 'LEGIFRANCE_CLIENT_ID',
95
+ 'LEGIFRANCE_CLIENT_SECRET',
96
+ 'LEGIFRANCE_ENV',
97
+ 'PYTHON_PATH',
98
+ 'SMART_CONVERTER_PATH',
99
+ ]);
100
+
101
+ function validateNewCaseName(value) {
102
+ const name = String(value || '').trim();
103
+ if (!name || name.length > 120 || name === '.' || name === '..' || path.basename(name) !== name
104
+ || name.startsWith('.') || /[\x00-\x1f\x7f]/.test(name) || isTechnicalCaseDirectoryName(name)) {
105
+ throw new Error('Nom de dossier juridique invalide.');
106
+ }
107
+ return name;
108
+ }
109
+
110
+ async function createLegalCase({ casesRoot, homeDir, name, config = {} }) {
111
+ const root = resolveCasesRoot(casesRoot);
112
+ const safeName = validateNewCaseName(name);
113
+ const directory = path.join(root, safeName);
114
+ if (fs.existsSync(directory)) throw new Error(`Le dossier juridique « ${safeName} » existe déjà.`);
115
+ fs.mkdirSync(directory);
116
+ try {
117
+ const structure = ensureCaseFolderStructure(directory, config);
118
+ writeProtection(directory, { unprotected: [] });
119
+ const mapping = writeCaseMapping(directory, { mapping: {}, reverse_mapping: {} });
120
+ await createCommit({
121
+ casesRoot: root,
122
+ caseName: safeName,
123
+ homeDir,
124
+ label: 'Création du dossier juridique',
125
+ event: 'admin-case-create',
126
+ // Le mapping vit dans le sous-dossier des fichiers produits : le commit doit
127
+ // viser son chemin relatif au dossier, pas seulement son nom de base.
128
+ paths: [path.relative(directory, mapping.file).split(path.sep).join('/')],
129
+ waitForLockMs: 10_000,
130
+ });
131
+ const folder = await caseOverview(root, homeDir, safeName);
132
+ folder.branches = await historyBranches(root, homeDir, safeName);
133
+ folder.structure = structure;
134
+ return folder;
135
+ } catch (error) {
136
+ fs.rmSync(directory, { recursive: true, force: true });
137
+ throw error;
138
+ }
139
+ }
140
+
141
+ /** Native folder-picker commands. Arguments stay separate from the shell. */
142
+ function folderPickerCommands(platform, initialFolder) {
143
+ const start = path.resolve(initialFolder || os.homedir());
144
+ if (platform === 'darwin') {
145
+ return [{
146
+ command: 'osascript',
147
+ args: [
148
+ '-e', 'on run argv',
149
+ '-e', 'set initialFolder to POSIX file (item 1 of argv)',
150
+ '-e', 'set selectedFolder to choose folder with prompt "Choisir un dossier juridique PieceMaker" default location initialFolder',
151
+ '-e', 'return POSIX path of selectedFolder',
152
+ '-e', 'end run',
153
+ start,
154
+ ],
155
+ }];
156
+ }
157
+ if (platform === 'win32') {
158
+ const script = [
159
+ 'Add-Type -AssemblyName System.Windows.Forms',
160
+ '$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
161
+ '$dialog.Description = "Choisir un dossier juridique PieceMaker"',
162
+ '$dialog.ShowNewFolderButton = $false',
163
+ 'if (Test-Path -LiteralPath $args[0]) { $dialog.SelectedPath = $args[0] }',
164
+ 'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Write($dialog.SelectedPath) }',
165
+ ].join('; ');
166
+ return [{ command: 'powershell.exe', args: ['-NoProfile', '-STA', '-Command', script, start] }];
167
+ }
168
+ const withSlash = start.endsWith(path.sep) ? start : `${start}${path.sep}`;
169
+ return [
170
+ { command: 'zenity', args: ['--file-selection', '--directory', '--title=Choisir un dossier juridique PieceMaker', `--filename=${withSlash}`] },
171
+ { command: 'kdialog', args: ['--getexistingdirectory', start, '--title', 'Choisir un dossier juridique PieceMaker'] },
172
+ ];
173
+ }
174
+
175
+ function captureProcess(command, args, { cwd } = {}) {
176
+ return new Promise((resolve) => {
177
+ const child = spawn(command, args, { cwd, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
178
+ const stdout = [];
179
+ const stderr = [];
180
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
181
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
182
+ child.once('error', (error) => resolve({ code: null, stdout: '', stderr: '', error }));
183
+ child.once('close', (code) => resolve({
184
+ code,
185
+ stdout: Buffer.concat(stdout).toString('utf8').trim(),
186
+ stderr: Buffer.concat(stderr).toString('utf8').trim(),
187
+ error: null,
188
+ }));
189
+ });
190
+ }
191
+
192
+ async function selectLocalFolder(platform = process.platform, initialFolder = os.homedir()) {
193
+ let lastError = null;
194
+ for (const candidate of folderPickerCommands(platform, initialFolder)) {
195
+ const result = await captureProcess(candidate.command, candidate.args);
196
+ if (result.code === 0) return result.stdout ? validateSelectedCaseFolder(result.stdout) : null;
197
+ if (result.error?.code === 'ENOENT') {
198
+ lastError = result.error;
199
+ continue;
200
+ }
201
+ // Native dialogs use a non-zero status for an ordinary user cancellation.
202
+ if (!result.stdout && (result.code === 1 || /cancel|annul/i.test(result.stderr))) return null;
203
+ lastError = result.error || new Error(result.stderr || `Le sélecteur s’est arrêté avec le code ${result.code}.`);
204
+ }
205
+ throw new Error(`Aucun sélecteur de dossier n’est disponible sur ce poste (${lastError?.message || 'commande introuvable'}).`);
206
+ }
207
+
208
+ async function registerLegalCase({
209
+ folder,
210
+ configFile,
211
+ homeDir,
212
+ } = {}) {
213
+ const root = validateSelectedCaseFolder(folder);
214
+ const previous = readRegistryConfig(configFile);
215
+ const structure = ensureCaseFolderStructure(root, previous);
216
+ const protection = readProtection(root);
217
+ if (!protection.exists) writeProtection(root, { unprotected: [] });
218
+ const currentMapping = readCaseMapping(root);
219
+ const mapping = currentMapping.exists
220
+ ? currentMapping
221
+ : writeCaseMapping(root, { mapping: {}, reverse_mapping: {} });
222
+
223
+ const registered = registerCaseFolder(previous, root);
224
+ atomicWrite(configFile, `${JSON.stringify(registered.config, null, 2)}\n`);
225
+
226
+ const folderOverview = await caseOverview(path.dirname(root), homeDir, path.basename(root));
227
+ folderOverview.path = registered.entry.id;
228
+ folderOverview.location = root;
229
+ folderOverview.registered = true;
230
+ folderOverview.branches = await historyBranches(path.dirname(root), homeDir, path.basename(root));
231
+ return {
232
+ folder: folderOverview,
233
+ installed: {
234
+ mapping: path.relative(root, mapping.file).split(path.sep).join('/'),
235
+ protection: path.relative(root, protection.file).split(path.sep).join('/'),
236
+ structure: structure.directories,
237
+ },
238
+ };
239
+ }
240
+
241
+ function defaultConfig(repoRoot, homeDir = path.join(os.homedir(), '.piecemaker')) {
242
+ return {
243
+ port: 43098,
244
+ pythonPath: null,
245
+ venvPath: path.join(homeDir, 'venv'),
246
+ adminTheme: 'light',
247
+ caseFolderStructure: { ...DEFAULT_CASE_FOLDER_STRUCTURE },
248
+ };
249
+ }
250
+
251
+ function validateAdminTheme(value) {
252
+ const theme = String(value || '');
253
+ if (theme !== 'light' && theme !== 'dark') throw new Error('Le thème doit être « light » ou « dark ».');
254
+ return theme;
255
+ }
256
+
257
+ function readJson(file, fallback) {
258
+ try {
259
+ return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : fallback;
260
+ } catch {
261
+ return fallback;
262
+ }
263
+ }
264
+
265
+ function atomicWrite(file, content, mode = undefined) {
266
+ fs.mkdirSync(path.dirname(file), { recursive: true });
267
+ const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
268
+ fs.writeFileSync(temp, content, { encoding: 'utf8', ...(mode ? { mode } : {}) });
269
+ fs.renameSync(temp, file);
270
+ if (mode) {
271
+ try {
272
+ fs.chmodSync(file, mode);
273
+ } catch {
274
+ // Windows does not implement POSIX file modes.
275
+ }
276
+ }
277
+ }
278
+
279
+ function readEnvFile(file) {
280
+ const values = {};
281
+ if (!fs.existsSync(file)) return values;
282
+ for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
283
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=(.*)$/);
284
+ if (match) values[match[1]] = match[2];
285
+ }
286
+ return values;
287
+ }
288
+
289
+ function updateEnvFile(file, updates, clearKeys = []) {
290
+ const clear = new Set(clearKeys.filter((key) => SECRET_KEYS.has(key)));
291
+ const cleanUpdates = new Map();
292
+ for (const [key, rawValue] of Object.entries(updates || {})) {
293
+ if (!ENV_KEYS.has(key)) continue;
294
+ const value = String(rawValue ?? '').trim();
295
+ if (!value || SECRET_KEYS.has(key) && value === '********') continue;
296
+ if (/\r|\n/.test(value)) throw new Error(`Valeur invalide pour ${key}`);
297
+ cleanUpdates.set(key, value);
298
+ }
299
+
300
+ if (cleanUpdates.has(COMMIT_USER_NAME_KEY)) {
301
+ resolveCommitIdentity({ identity: { name: cleanUpdates.get(COMMIT_USER_NAME_KEY) } });
302
+ }
303
+
304
+ const original = fs.existsSync(file) ? fs.readFileSync(file, 'utf8').split(/\r?\n/) : [];
305
+ const seen = new Set();
306
+ const lines = [];
307
+ for (const line of original) {
308
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=/);
309
+ if (!match) {
310
+ if (line || lines.length) lines.push(line);
311
+ continue;
312
+ }
313
+ const key = match[1];
314
+ if (clear.has(key)) continue;
315
+ if (cleanUpdates.has(key)) {
316
+ lines.push(`${key}=${cleanUpdates.get(key)}`);
317
+ cleanUpdates.delete(key);
318
+ seen.add(key);
319
+ } else {
320
+ lines.push(line);
321
+ seen.add(key);
322
+ }
323
+ }
324
+ for (const [key, value] of cleanUpdates) {
325
+ if (!seen.has(key)) lines.push(`${key}=${value}`);
326
+ }
327
+ while (lines.at(-1) === '') lines.pop();
328
+ atomicWrite(file, `${lines.join('\n')}\n`, 0o600);
329
+ }
330
+
331
+ function maskSecret(value) {
332
+ if (!value) return { configured: false, hint: '' };
333
+ const suffix = value.length > 4 ? value.slice(-4) : '';
334
+ return { configured: true, hint: suffix ? `••••${suffix}` : 'configurée' };
335
+ }
336
+
337
+ function captureCommand(command, args = [], timeout = 3000) {
338
+ const executable = process.platform === 'win32' && !command.endsWith('.exe')
339
+ ? `${command}.cmd`
340
+ : command;
341
+ const result = spawnSync(executable, args, {
342
+ encoding: 'utf8',
343
+ timeout,
344
+ windowsHide: true,
345
+ // Windows cannot execute a .cmd shim directly. The command and arguments
346
+ // are constants owned by PieceMaker, never values supplied by the browser.
347
+ shell: process.platform === 'win32',
348
+ });
349
+ return {
350
+ ok: result.status === 0 && !result.error,
351
+ output: String(result.stdout || result.stderr || '').trim(),
352
+ };
353
+ }
354
+
355
+ // Une ancienne installation PieceMaker par marketplace peut encore être
356
+ // présente sur un poste mis à jour. On la masque des catalogues Anthropic
357
+ // pour ne pas dupliquer les composants désormais enregistrés directement.
358
+ const LEGACY_PIECEMAKER_MARKETPLACE_NAME = 'piecemaker';
359
+
360
+ function parseJsonOutput(output) {
361
+ try {
362
+ return JSON.parse(output);
363
+ } catch {
364
+ return null;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Enregistre directement les composants PieceMaker dans ~/.claude. Le nom de
370
+ * cette fonction est conservé pour la compatibilité de l'API d'administration,
371
+ * mais aucune commande `claude plugin` ni aucun manifest n'est utilisé.
372
+ */
373
+ async function ensureClaudePluginActive({
374
+ repoRoot,
375
+ userHome = os.homedir(),
376
+ runCommand = captureCommand,
377
+ syncAssets = syncClaudeAssets,
378
+ } = {}) {
379
+ if (!runCommand('claude', ['--version']).ok) {
380
+ return {
381
+ ok: false,
382
+ action: 'skipped',
383
+ installed: false,
384
+ reason: 'Claude Code est introuvable.',
385
+ };
386
+ }
387
+ const result = syncAssets(repoRoot, userHome);
388
+ return {
389
+ ok: result.conflicts.length === 0,
390
+ action: 'registered',
391
+ installed: true,
392
+ ...result,
393
+ };
394
+ }
395
+
396
+ // -----------------------------------------------------------------------
397
+ // Marketplace officiel Claude Code — onglet « Marketplace officiel » du
398
+ // même pop-up. Constat d'investigation (voir docs/plugin-marketplace, sinon
399
+ // le rapport de cette fonctionnalité) : la CLI `claude` n'expose AUCUNE
400
+ // recherche distante — `claude plugin --help` n'a pas de sous-commande
401
+ // `search`, et ni `plugin list` ni `plugin marketplace list` n'acceptent de
402
+ // requête. Le seul catalogue énumérable programmatiquement est
403
+ // `claude plugin list --available --json`, qui renvoie les plugins NON
404
+ // installés des marketplaces déjà enregistrées localement (nom,
405
+ // description, marketplace, popularité `installCount`) à côté de la liste
406
+ // des plugins installés (`claude plugin list --json`, avec leur état
407
+ // enabled/disabled). Rien n'est donc inventé ici : la recherche du pop-up
408
+ // filtre côté client cette même liste (voir admin/app.js). Le seul
409
+ // marketplace enregistrable en un clic depuis ce pop-up est le marketplace
410
+ // premier-parti d'Anthropic (`claude-plugins-official`, dépôt
411
+ // anthropics/claude-plugins-official). Tout autre marketplace se déclare via
412
+ // `claude plugin marketplace add` en dehors de ce pop-up.
413
+ // -----------------------------------------------------------------------
414
+ const OFFICIAL_MARKETPLACE_NAME = 'claude-plugins-official';
415
+ const OFFICIAL_MARKETPLACE_SLUG = 'anthropics/claude-plugins-official';
416
+ // Marketplace « Claude for Legal » d'Anthropic (dépôt anthropics/claude-for-legal),
417
+ // suite de plugins juridiques par domaine (commercial-legal, litigation-legal,
418
+ // ip-legal…). C'est le catalogue de l'onglet « Plugin legal Claude » du pop-up —
419
+ // distinct du marketplace généraliste claude-plugins-official (onglet
420
+ // « Marketplace officiel »). Même mécanique CLI que l'officiel : add / list /
421
+ // install / enable / disable, scoping par nom de marketplace.
422
+ const LEGAL_MARKETPLACE_NAME = 'claude-for-legal';
423
+ const LEGAL_MARKETPLACE_SLUG = 'anthropics/claude-for-legal';
424
+
425
+ // Les deux catalogues énumérables depuis le pop-up, indexés par le `scope`
426
+ // que le frontend passe (onglet legal vs officiel). Tout scope inconnu retombe
427
+ // sur l'officiel — le comportement historique.
428
+ const MARKETPLACE_SCOPES = {
429
+ legal: { name: LEGAL_MARKETPLACE_NAME, slug: LEGAL_MARKETPLACE_SLUG },
430
+ official: { name: OFFICIAL_MARKETPLACE_NAME, slug: OFFICIAL_MARKETPLACE_SLUG },
431
+ };
432
+ function marketplaceForScope(scope) {
433
+ return MARKETPLACE_SCOPES[scope] || MARKETPLACE_SCOPES.official;
434
+ }
435
+
436
+ /** Marketplaces Claude Code enregistrées sur ce poste, hors la nôtre. */
437
+ function listRegisteredMarketplaces(runCommand = captureCommand) {
438
+ const result = runCommand('claude', ['plugin', 'marketplace', 'list', '--json']);
439
+ if (!result.ok) return [];
440
+ const parsed = parseJsonOutput(result.output);
441
+ if (!Array.isArray(parsed)) return [];
442
+ return parsed
443
+ .filter((entry) => entry?.name && entry.name !== LEGACY_PIECEMAKER_MARKETPLACE_NAME)
444
+ .map((entry) => ({
445
+ name: entry.name,
446
+ repo: entry.repo || null,
447
+ source: entry.source || null,
448
+ // Heuristique locale, pas un champ renvoyé par la CLI : seul le
449
+ // marketplace historique publié par Anthropic sous ce dépôt exact est
450
+ // marqué « officiel » — voir le commentaire ci-dessus.
451
+ official: entry.source === 'github' && entry.repo === OFFICIAL_MARKETPLACE_SLUG,
452
+ }));
453
+ }
454
+
455
+ // Le catalogue officiel dépasse aujourd'hui ~300 plugins et `claude plugin
456
+ // list --available --json` rafraîchit les marketplaces par le réseau : l'appel
457
+ // prend couramment 10–16 s. On lui laisse donc un délai large (il ne doit
458
+ // jamais être tué en cours de route, sinon les onglets se vident) et on met le
459
+ // résultat en cache un court instant : ré-ouvrir un onglet, basculer legal ↔
460
+ // officiel ou paginer ne relance pas la commande lente. Le cache n'est actif
461
+ // que pour le vrai `captureCommand` — les tests injectent leur runCommand et
462
+ // doivent voir chaque appel.
463
+ const AVAILABLE_TTL_MS = 5 * 60 * 1000;
464
+ const AVAILABLE_TIMEOUT_MS = 60000;
465
+ let availableCache = null; // { at: number, data }
466
+
467
+ function invalidateMarketplaceCache() {
468
+ availableCache = null;
469
+ }
470
+
471
+ function fetchAvailablePlugins(runCommand = captureCommand) {
472
+ const useCache = runCommand === captureCommand;
473
+ if (useCache && availableCache && Date.now() - availableCache.at < AVAILABLE_TTL_MS) {
474
+ return availableCache.data;
475
+ }
476
+ const result = runCommand('claude', ['plugin', 'list', '--available', '--json'], AVAILABLE_TIMEOUT_MS);
477
+ if (!result.ok) {
478
+ return { ok: false, reason: result.output || 'Échec de l’appel « claude plugin list --available --json ».' };
479
+ }
480
+ const parsed = parseJsonOutput(result.output) || {};
481
+ const data = {
482
+ ok: true,
483
+ installed: Array.isArray(parsed.installed) ? parsed.installed : [],
484
+ available: Array.isArray(parsed.available) ? parsed.available : [],
485
+ };
486
+ if (useCache) availableCache = { at: Date.now(), data };
487
+ return data;
488
+ }
489
+
490
+ /**
491
+ * Plugins-connecteurs des marketplaces déjà enregistrées (hors piecemaker),
492
+ * fusion de `plugin list --json` (installés, avec enabled/scope) et
493
+ * `plugin list --available --json` (catalogue non installé, avec
494
+ * description/popularité) — seule source de données énumérable, voir le
495
+ * commentaire au-dessus de OFFICIAL_MARKETPLACE_NAME. `runCommand`
496
+ * injectable pour les tests.
497
+ */
498
+ function listMarketplaceConnectors(runCommand = captureCommand, options = {}) {
499
+ // `marketplaceName` restreint le catalogue à un seul marketplace (onglet
500
+ // legal ↔ claude-for-legal, onglet officiel ↔ claude-plugins-official) ;
501
+ // absent, on garde le comportement historique (tous les marketplaces hors
502
+ // piecemaker). Un plugin d'un autre marketplace n'apparaît alors jamais dans
503
+ // l'onglet, et `applyMarketplaceSelection` n'y touche pas.
504
+ const scopeName = options.marketplaceName || null;
505
+ const inScope = (marketplace) => !scopeName || marketplace === scopeName;
506
+ const marketplaces = listRegisteredMarketplaces(runCommand);
507
+ // `registered` : le marketplace de l'onglet est-il déjà déclaré sur ce poste ?
508
+ // (scopé sur son nom, sinon sur l'officiel — comme avant.)
509
+ const registered = marketplaces.some((entry) => entry.name === (scopeName || OFFICIAL_MARKETPLACE_NAME));
510
+ const fetched = fetchAvailablePlugins(runCommand);
511
+ if (!fetched.ok) {
512
+ return {
513
+ marketplaces,
514
+ officialRegistered: registered,
515
+ registered,
516
+ plugins: [],
517
+ reason: fetched.reason,
518
+ };
519
+ }
520
+ const installed = fetched.installed;
521
+ const available = fetched.available;
522
+
523
+ const plugins = [];
524
+ const seen = new Set();
525
+ for (const entry of available) {
526
+ if (!entry?.pluginId || entry.marketplaceName === LEGACY_PIECEMAKER_MARKETPLACE_NAME || seen.has(entry.pluginId)) continue;
527
+ if (!inScope(entry.marketplaceName)) continue;
528
+ seen.add(entry.pluginId);
529
+ plugins.push({
530
+ id: entry.pluginId,
531
+ name: entry.displayName || entry.name || entry.pluginId,
532
+ description: entry.description || '',
533
+ marketplace: entry.marketplaceName || '',
534
+ installCount: Number.isFinite(entry.installCount) ? entry.installCount : null,
535
+ installed: false,
536
+ enabled: false,
537
+ });
538
+ }
539
+ // `plugin list --json` ne fournit pas de description — seul le catalogue
540
+ // « available » en a une. Un plugin déjà installé apparaît donc ici avec
541
+ // une description vide (limite de la CLI, pas une omission de notre part).
542
+ for (const entry of installed) {
543
+ if (!entry?.id || entry.id.endsWith(`@${LEGACY_PIECEMAKER_MARKETPLACE_NAME}`) || seen.has(entry.id)) continue;
544
+ const [name, marketplace] = entry.id.split('@');
545
+ if (!inScope(marketplace)) continue;
546
+ seen.add(entry.id);
547
+ plugins.push({
548
+ id: entry.id,
549
+ name,
550
+ description: '',
551
+ marketplace: marketplace || '',
552
+ installCount: null,
553
+ installed: true,
554
+ enabled: Boolean(entry.enabled),
555
+ });
556
+ }
557
+
558
+ return { marketplaces, officialRegistered: registered, registered, plugins };
559
+ }
560
+
561
+ /**
562
+ * Enregistre (ou rafraîchit s'il l'est déjà) un marketplace Anthropic —
563
+ * bouton « Découvrir le marketplace… » des onglets du pop-up. `marketplace`
564
+ * ({ name, slug }) désigne le catalogue voulu : par défaut le marketplace
565
+ * officiel généraliste, ou celui du scope (legal → claude-for-legal), pour un
566
+ * poste qui n'a encore que le marketplace piecemaker.
567
+ */
568
+ function registerOfficialMarketplace(runCommand = captureCommand, marketplace = MARKETPLACE_SCOPES.official) {
569
+ const alreadyRegistered = listRegisteredMarketplaces(runCommand)
570
+ .some((entry) => entry.name === marketplace.name);
571
+ const action = alreadyRegistered
572
+ ? runCommand('claude', ['plugin', 'marketplace', 'update', marketplace.name], 8000)
573
+ : runCommand('claude', ['plugin', 'marketplace', 'add', marketplace.slug], 15000);
574
+ if (!action.ok) {
575
+ return { ok: false, alreadyRegistered, reason: action.output || `Échec de l’enregistrement du marketplace « ${marketplace.name} ».` };
576
+ }
577
+ // Un nouveau marketplace change le catalogue « available » : purger le cache.
578
+ invalidateMarketplaceCache();
579
+ return { ok: true, alreadyRegistered };
580
+ }
581
+
582
+ /**
583
+ * Applique une sélection de connecteurs marketplace « voulus actifs » —
584
+ * même sémantique que `applyPluginComponentSelection` (coché = actif),
585
+ * étendue à trois actions CLI selon l'état courant : `install` (jamais
586
+ * installé), `enable` (installé mais désactivé), `disable` (actif et
587
+ * décoché). Jamais `uninstall` : décocher désactive, ne supprime rien —
588
+ * choix délibéré, réversible, cohérent avec le reste de PieceMaker qui ne
589
+ * retire jamais un composant tiers de façon destructive. Idempotent : un
590
+ * connecteur déjà dans l'état voulu n'appelle aucune commande. Chaque appel
591
+ * est best-effort — un échec individuel n'empêche pas les suivants.
592
+ */
593
+ function applyMarketplaceSelection(selection, runCommand = captureCommand, options = {}) {
594
+ const wanted = new Set((Array.isArray(selection) ? selection : []).map(String));
595
+ // Scopé au marketplace de l'onglet : décocher un plugin ne peut jamais
596
+ // désactiver un connecteur d'un autre marketplace (l'univers considéré est
597
+ // exactement celui listé dans l'onglet).
598
+ const { plugins } = listMarketplaceConnectors(runCommand, options);
599
+ const results = plugins.map((plugin) => {
600
+ const shouldBeActive = wanted.has(plugin.id);
601
+ const isActive = plugin.installed && plugin.enabled;
602
+ if (shouldBeActive === isActive) return { id: plugin.id, action: 'none', ok: true };
603
+ if (shouldBeActive) {
604
+ const command = plugin.installed
605
+ ? runCommand('claude', ['plugin', 'enable', plugin.id], 5000)
606
+ : runCommand('claude', ['plugin', 'install', plugin.id, '-y'], 20000);
607
+ return {
608
+ id: plugin.id,
609
+ action: plugin.installed ? 'enable' : 'install',
610
+ ok: command.ok,
611
+ reason: command.ok ? '' : command.output || 'Échec de la commande claude.',
612
+ };
613
+ }
614
+ const command = runCommand('claude', ['plugin', 'disable', plugin.id], 5000);
615
+ return { id: plugin.id, action: 'disable', ok: command.ok, reason: command.ok ? '' : command.output || 'Échec de la commande claude.' };
616
+ });
617
+ // L'état installé/activé a changé : le prochain chargement doit repartir de zéro.
618
+ if (results.some((entry) => entry.action !== 'none')) invalidateMarketplaceCache();
619
+ return {
620
+ results,
621
+ installed: results.filter((entry) => entry.action === 'install' && entry.ok).length,
622
+ enabled: results.filter((entry) => entry.action === 'enable' && entry.ok).length,
623
+ disabled: results.filter((entry) => entry.action === 'disable' && entry.ok).length,
624
+ failed: results.filter((entry) => !entry.ok),
625
+ };
626
+ }
627
+
628
+ async function fetchWithTimeout(fetchImpl, url, options = {}, timeout = 3000) {
629
+ const controller = new AbortController();
630
+ const timer = setTimeout(() => controller.abort(), timeout);
631
+ try {
632
+ return await fetchImpl(url, { ...options, signal: controller.signal });
633
+ } finally {
634
+ clearTimeout(timer);
635
+ }
636
+ }
637
+
638
+ async function ollamaState(fetchImpl = global.fetch) {
639
+ if (typeof fetchImpl !== 'function') return { installed: false, version: '', models: [] };
640
+ try {
641
+ const [versionResponse, tagsResponse] = await Promise.all([
642
+ fetchWithTimeout(fetchImpl, 'http://127.0.0.1:11434/api/version', {}, 1800).catch(() => null),
643
+ fetchWithTimeout(fetchImpl, 'http://127.0.0.1:11434/api/tags', {}, 2500),
644
+ ]);
645
+ if (!tagsResponse?.ok) return { installed: false, version: '', models: [] };
646
+ const data = await tagsResponse.json();
647
+ const versionData = versionResponse?.ok ? await versionResponse.json().catch(() => ({})) : {};
648
+ const models = Array.isArray(data.models) ? data.models.map((model) => ({
649
+ name: String(model.name || model.model || ''),
650
+ digest: String(model.digest || ''),
651
+ size: Number(model.size) || 0,
652
+ modifiedAt: model.modified_at || '',
653
+ family: model.details?.family || '',
654
+ parameterSize: model.details?.parameter_size || '',
655
+ quantization: model.details?.quantization_level || '',
656
+ })).filter((model) => model.name) : [];
657
+ return { installed: true, version: String(versionData.version || ''), models };
658
+ } catch {
659
+ return { installed: false, version: '', models: [] };
660
+ }
661
+ }
662
+
663
+ function ollamaRegistryReference(modelName) {
664
+ const value = String(modelName || '').trim();
665
+ if (!value || value.length > 240 || !/^[A-Za-z0-9._/-]+(?::[A-Za-z0-9._-]+)?$/.test(value)) {
666
+ throw new Error('Nom de modèle Ollama invalide.');
667
+ }
668
+ const lastSlash = value.lastIndexOf('/');
669
+ const tagSeparator = value.indexOf(':', lastSlash + 1);
670
+ const repositoryName = tagSeparator >= 0 ? value.slice(0, tagSeparator) : value;
671
+ const tag = tagSeparator >= 0 ? value.slice(tagSeparator + 1) : 'latest';
672
+ // Names beginning with a registry host (notably hf.co/...) are imports, not
673
+ // models published in Ollama's registry, so their update cannot be inferred.
674
+ if (/^[^/]+\.[^/]+\//.test(repositoryName)) return null;
675
+ const repository = repositoryName.includes('/') ? repositoryName : `library/${repositoryName}`;
676
+ return { repository, tag };
677
+ }
678
+
679
+ async function checkOllamaModelUpdate(modelName, fetchImpl = global.fetch) {
680
+ const reference = ollamaRegistryReference(modelName);
681
+ if (!reference) {
682
+ return { status: 'unknown', message: 'Ce modèle provient d’un registre externe ; vérification automatique indisponible.' };
683
+ }
684
+ const local = await ollamaState(fetchImpl);
685
+ const model = local.models.find((entry) => entry.name === modelName);
686
+ if (!model) throw new Error('Ce modèle local n’est plus disponible dans Ollama.');
687
+ const repositoryPath = reference.repository.split('/').map(encodeURIComponent).join('/');
688
+ const url = `https://registry.ollama.ai/v2/${repositoryPath}/manifests/${encodeURIComponent(reference.tag)}`;
689
+ let response;
690
+ try {
691
+ response = await fetchWithTimeout(fetchImpl, url, {
692
+ headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' },
693
+ }, 8000);
694
+ } catch (error) {
695
+ return { status: 'unknown', message: `Registre Ollama injoignable (${error.message}).` };
696
+ }
697
+ if (response.status === 404) {
698
+ return { status: 'unknown', message: 'Modèle local ou privé : aucune version publique correspondante.' };
699
+ }
700
+ if (!response.ok) {
701
+ return { status: 'unknown', message: `Vérification impossible (registre HTTP ${response.status}).` };
702
+ }
703
+ const manifest = Buffer.from(await response.arrayBuffer());
704
+ const remoteDigest = crypto.createHash('sha256').update(manifest).digest('hex');
705
+ const updateAvailable = Boolean(model.digest) && remoteDigest !== model.digest.replace(/^sha256:/, '');
706
+ return updateAvailable
707
+ ? { status: 'update', message: 'Une mise à jour est disponible dans le registre Ollama.' }
708
+ : { status: 'current', message: 'Le modèle local est à jour.' };
709
+ }
710
+
711
+ async function configurationOverview({ repoRoot, homeDir, userHome, getRuntimeStatus, fetchImpl }) {
712
+ const env = readEnvFile(path.join(repoRoot, '.env'));
713
+ const installer = readJson(path.join(homeDir, 'state.json'), { steps: {} });
714
+ const steps = installer.steps || {};
715
+ const runtime = getRuntimeStatus();
716
+ const claude = captureCommand('claude', ['--version']);
717
+ const codex = captureCommand('codex', ['--version']);
718
+ const clients = [
719
+ claude.ok && { name: 'Claude Code', version: claude.output.replace(/\s*\(Claude Code\)\s*$/i, '') },
720
+ codex.ok && { name: 'Codex', version: codex.output.replace(/^codex(?:-cli)?\s*/i, '') },
721
+ ].filter(Boolean);
722
+ const hookFiles = [
723
+ 'protect-originals.mjs',
724
+ 'commit-track.mjs',
725
+ 'billing-track.mjs',
726
+ ].map((name) => path.join(repoRoot, 'piecemaker-plugin', 'scripts', name));
727
+ const hooksReady = hookFiles.every((file) => fs.existsSync(file))
728
+ && fs.existsSync(path.join(repoRoot, 'piecemaker-plugin', 'hooks', 'hooks.json'));
729
+ const claudeAssets = repositoryAssets(repoRoot);
730
+ const claudeAssetsReady = claudeAssets.length > 0 && claudeAssets.every((asset) =>
731
+ ['linked', 'copied'].includes(claudeAssetStatus(repoRoot, userHome, asset)?.state));
732
+ const hooksRegistered = claudeHooksStatus(repoRoot, userHome).ok;
733
+ const claudeSettings = readJson(path.join(userHome, '.claude', 'settings.json'), {});
734
+ const legifrancePluginEnabled = claudeSettings.enabledPlugins?.['piecemaker@mcp-legifrance'] === true;
735
+ let nodePtyReady = false;
736
+ try { nodePtyReady = Boolean(require.resolve('node-pty')); } catch { /* dépendance optionnelle */ }
737
+
738
+ const mcpItems = [
739
+ {
740
+ name: 'Légifrance',
741
+ installed: legifrancePluginEnabled,
742
+ configured: Boolean(env.LEGIFRANCE_CLIENT_ID && env.LEGIFRANCE_CLIENT_SECRET),
743
+ detail: 'Plugin autonome PieceMaker-Legal/mcp-legifrance via les API officielles PISTE.',
744
+ },
745
+ ];
746
+ const models = await ollamaState(fetchImpl);
747
+ const telegram = getTelegramState({ repoRoot, homeDir, userHome });
748
+
749
+ // Moteurs Python locaux du venv — détection par présence de paquet, sans import
750
+ // (importer gliner2/mineru chargerait des centaines de Mo à chaque appel).
751
+ const config = readJson(path.join(homeDir, 'config.json'), {});
752
+ const venvDir = config.venvPath || path.join(homeDir, 'venv');
753
+ const sitePackages = (() => {
754
+ if (process.platform === 'win32') return path.join(venvDir, 'Lib', 'site-packages');
755
+ const libDir = path.join(venvDir, 'lib');
756
+ try {
757
+ const py = fs.readdirSync(libDir).find((name) => name.startsWith('python'));
758
+ return py ? path.join(libDir, py, 'site-packages') : '';
759
+ } catch { return ''; }
760
+ })();
761
+ const hasPackage = (name) => Boolean(sitePackages) && fs.existsSync(path.join(sitePackages, name));
762
+ const glinerDir = path.join(repoRoot, 'websocket-server', 'scripts', 'presidio-gliner');
763
+ const glinerModelId = 'fastino/gliner2.5-multi-v1';
764
+ const huggingFaceHubCache = process.env.HF_HUB_CACHE
765
+ || path.join(process.env.HF_HOME || path.join(userHome, '.cache', 'huggingface'), 'hub');
766
+ const glinerModelCache = path.join(
767
+ huggingFaceHubCache,
768
+ `models--${glinerModelId.replace('/', '--')}`,
769
+ 'snapshots',
770
+ );
771
+ const glinerModelReady = (() => {
772
+ try {
773
+ return fs.readdirSync(glinerModelCache)
774
+ .some((revision) => ['config.json', 'model.safetensors']
775
+ .every((name) => fs.existsSync(path.join(glinerModelCache, revision, name))));
776
+ } catch { return false; }
777
+ })();
778
+ const glinerReady = fs.existsSync(path.join(glinerDir, 'presidio-gliner.py'))
779
+ && hasPackage('gliner2')
780
+ && glinerModelReady;
781
+ const coremlDefault = path.join(
782
+ glinerDir, 'models', 'gliner2.5-multi-v1-encoder_b1_832.mlmodelc',
783
+ );
784
+ const coreml = [process.env.PIECEMAKER_COREML_MODEL, coremlDefault]
785
+ .filter(Boolean)
786
+ .some((candidate) => fs.existsSync(candidate));
787
+ const mineruReady = hasPackage('mineru') || hasPackage('magic_pdf')
788
+ || fs.existsSync(path.join(venvDir, 'bin', 'mineru'));
789
+ const folders = listConfiguredCases(readRegistryConfig(path.join(homeDir, 'config.json'))).map((entry) => {
790
+ const bot = telegram.dossiers.find((candidate) => {
791
+ try { return path.resolve(candidate.workdir) === path.resolve(entry.root); } catch { return false; }
792
+ });
793
+ return {
794
+ id: entry.id,
795
+ name: entry.name,
796
+ path: entry.root,
797
+ bot: bot ? {
798
+ id: bot.id,
799
+ name: bot.name,
800
+ configured: Boolean(bot.linked),
801
+ running: Boolean(bot.running),
802
+ } : null,
803
+ };
804
+ });
805
+
806
+ return {
807
+ components: {
808
+ client: {
809
+ name: clients.map((client) => client.name).join(' + ') || 'Client IA',
810
+ installed: clients.length > 0,
811
+ summary: clients.length
812
+ ? clients.map((client) => `${client.name}${client.version ? ` ${client.version}` : ''}`).join(' · ')
813
+ : 'Aucun client en ligne de commande détecté',
814
+ clients,
815
+ pluginInstalled: claude.ok && claudeAssetsReady,
816
+ pluginVersion: '',
817
+ },
818
+ terminal: {
819
+ name: 'Terminal intégré',
820
+ installed: runtime.terminalReady ?? nodePtyReady,
821
+ summary: runtime.terminalReady ?? nodePtyReady
822
+ ? `PTY local · ${os.userInfo().shell || process.env.COMSPEC || 'shell système'}`
823
+ : 'Le pont PTY optionnel est absent',
824
+ shell: os.userInfo().shell || process.env.COMSPEC || '',
825
+ },
826
+ hooks: {
827
+ name: 'Hooks PieceMaker',
828
+ installed: hooksReady && hooksRegistered,
829
+ summary: `${hookFiles.filter((file) => fs.existsSync(file)).length}/${hookFiles.length} garde-fous disponibles`,
830
+ count: hookFiles.filter((file) => fs.existsSync(file)).length,
831
+ installerStatus: steps['06-hooks']?.status || '',
832
+ },
833
+ mcp: {
834
+ name: 'MCP locaux',
835
+ installed: mcpItems.every((item) => item.installed),
836
+ configured: mcpItems.every((item) => item.installed && item.configured),
837
+ summary: `${mcpItems.filter((item) => item.installed).length}/${mcpItems.length} serveurs présents`,
838
+ items: mcpItems,
839
+ },
840
+ gliner: {
841
+ name: 'GLiNER2.5 · PII',
842
+ installed: glinerReady,
843
+ summary: glinerReady
844
+ ? coreml ? 'Détection locale · GPU CoreML' : 'Détection locale · CPU'
845
+ : 'Modèle d’anonymisation absent',
846
+ coreml,
847
+ model: glinerModelId,
848
+ engine: coreml ? 'CoreML (GPU)' : 'torch (CPU)',
849
+ },
850
+ mineru: {
851
+ name: 'MinerU · OCR',
852
+ installed: mineruReady,
853
+ optional: true,
854
+ summary: mineruReady
855
+ ? 'OCR local · PDF scannés et images'
856
+ : 'Optionnel · non installé',
857
+ },
858
+ telegram: {
859
+ name: 'Telegram',
860
+ installed: true,
861
+ optional: true,
862
+ configured: Boolean(telegram.assistant.token.configured || telegram.monitor.token.configured),
863
+ summary: (() => {
864
+ const running = [telegram.assistant.running && 'Assistant', telegram.monitor.running && 'Surveillance'].filter(Boolean);
865
+ if (running.length) return `${running.join(' + ')} en ligne`;
866
+ if (telegram.assistant.token.configured || telegram.monitor.token.configured) return 'Configuré · arrêté';
867
+ return 'Aucun bot configuré';
868
+ })(),
869
+ bots: [
870
+ { name: telegram.assistant.name, role: 'Assistant', configured: Boolean(telegram.assistant.token.configured), running: Boolean(telegram.assistant.running) },
871
+ { name: telegram.monitor.name, role: 'Surveillance', configured: Boolean(telegram.monitor.token.configured), running: Boolean(telegram.monitor.running) },
872
+ ],
873
+ },
874
+ },
875
+ models: {
876
+ provider: 'Ollama',
877
+ installed: models.installed,
878
+ version: models.version,
879
+ items: models.models,
880
+ },
881
+ folders,
882
+ };
883
+ }
884
+
885
+ function normalizeManagedPath(relativePath) {
886
+ return String(relativePath || '').replaceAll('\\', '/').replace(/^\.\//, '');
887
+ }
888
+
889
+ function managedFileKind(relativePath) {
890
+ const normalized = normalizeManagedPath(relativePath);
891
+ if (normalized === 'AGENTS.md' || normalized === 'CLAUDE.md') return 'instructions';
892
+ if (/^piecemaker-plugin\/agents\/[^/]+\.md$/.test(normalized)) return 'agent';
893
+ if (/^piecemaker-plugin\/skills\/[^/]+\/SKILL\.md$/.test(normalized)) return 'skill';
894
+ if (/^billing\/synthese\/[^/]+\.md$/.test(normalized)) return 'billing';
895
+ if (/^billing\/\d{4}-\d{2}\.jsonl$/.test(normalized)) return 'billing';
896
+ return null;
897
+ }
898
+
899
+ function nearestExistingPath(value) {
900
+ let current = value;
901
+ while (!fs.existsSync(current) && path.dirname(current) !== current) current = path.dirname(current);
902
+ return current;
903
+ }
904
+
905
+ function managedAbsolutePath(repoRoot, relativePath, homeDir = path.join(os.homedir(), '.piecemaker')) {
906
+ const normalized = normalizeManagedPath(relativePath);
907
+ const kind = managedFileKind(normalized);
908
+ if (!kind) throw new Error('Ce fichier ne fait pas partie des fichiers administrables.');
909
+ const synthesis = normalized.startsWith('billing/synthese/');
910
+ const root = kind === 'billing'
911
+ ? path.resolve(homeDir, 'billing', ...(synthesis ? ['synthese'] : []))
912
+ : path.resolve(repoRoot);
913
+ const relative = kind === 'billing'
914
+ ? normalized.replace(/^billing\/(?:synthese\/)?/, '')
915
+ : normalized;
916
+ const absolute = path.resolve(root, ...relative.split('/'));
917
+ if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) {
918
+ throw new Error('Chemin de fichier invalide.');
919
+ }
920
+ const existing = nearestExistingPath(absolute);
921
+ const realRoot = fs.realpathSync(nearestExistingPath(root));
922
+ const realExisting = fs.realpathSync(existing);
923
+ if (realExisting !== realRoot && !realExisting.startsWith(`${realRoot}${path.sep}`)) {
924
+ throw new Error('Le fichier résout vers un emplacement extérieur au dépôt.');
925
+ }
926
+ if (kind === 'billing' && !fs.existsSync(absolute)) throw new Error('Rapport de facturation introuvable.');
927
+ return { normalized, absolute };
928
+ }
929
+
930
+ // Préfixe des chemins synthétiques d'un skill fourni par un plugin de
931
+ // marketplace installé (onglet « Skills et agents », groupe « Skills
932
+ // officiels »). Ce ne sont pas des fichiers du dépôt : ils vivent dans le cache
933
+ // Claude Code (~/.claude/plugins/cache/…), en lecture seule. Le chemin encode
934
+ // l'id du plugin et le slug du skill pour être re-résolu à la lecture sans
935
+ // stocker de chemin de version volatil.
936
+ const OFFICIAL_SKILL_PREFIX = 'official-skill:';
937
+
938
+ // Skills exposés par les plugins de marketplace installés ET activés (hors
939
+ // notre propre plugin piecemaker, dont les skills sont déjà listés depuis le
940
+ // dépôt). Source : `claude plugin list --json`, qui donne l'`installPath` exact
941
+ // (dossier de version inclus) et l'état `enabled` de chaque plugin ; on y lit
942
+ // alors les `skills/<slug>/SKILL.md`. `runCommand` injectable pour les tests.
943
+ function installedPluginSkills(runCommand = captureCommand) {
944
+ const result = runCommand('claude', ['plugin', 'list', '--json'], 5000);
945
+ if (!result.ok) return [];
946
+ const plugins = parseJsonOutput(result.output);
947
+ if (!Array.isArray(plugins)) return [];
948
+ const skills = [];
949
+ const seen = new Set();
950
+ for (const plugin of plugins) {
951
+ if (!plugin?.id || plugin.enabled === false) continue;
952
+ // Nos propres skills sont déjà listés depuis le dépôt — ne pas les répéter.
953
+ if (plugin.id.endsWith(`@${LEGACY_PIECEMAKER_MARKETPLACE_NAME}`)) continue;
954
+ const installPath = typeof plugin.installPath === 'string' ? plugin.installPath : '';
955
+ if (!installPath) continue;
956
+ const skillsDir = path.join(installPath, 'skills');
957
+ let entries;
958
+ try {
959
+ entries = fs.readdirSync(skillsDir);
960
+ } catch {
961
+ continue; // pas de dossier skills/ (plugin d'agents seuls, MCP, etc.)
962
+ }
963
+ const [pluginName, marketplace] = plugin.id.split('@');
964
+ for (const name of entries.sort()) {
965
+ const skillFile = path.join(skillsDir, name, 'SKILL.md');
966
+ if (!fs.existsSync(skillFile)) continue;
967
+ const key = `${plugin.id}/${name}`;
968
+ if (seen.has(key)) continue; // même plugin installé en scope user ET project
969
+ seen.add(key);
970
+ const metadata = pluginComponentFrontMatter(skillFile);
971
+ skills.push({
972
+ path: `${OFFICIAL_SKILL_PREFIX}${plugin.id}/${name}`,
973
+ pluginId: plugin.id,
974
+ plugin: pluginName,
975
+ marketplace: marketplace || '',
976
+ slug: name,
977
+ name: metadata.name || name,
978
+ description: metadata.description || '',
979
+ absolute: skillFile,
980
+ });
981
+ }
982
+ }
983
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
984
+ }
985
+
986
+ // Registre persistant des skills officiels supprimés par l'utilisateur. Un
987
+ // skill de plugin de marketplace vit dans le cache Claude Code, que
988
+ // `claude plugin install`/`update` restaure : sans ce registre, la suppression
989
+ // serait annulée à la première mise à jour. On mémorise donc le chemin
990
+ // synthétique retiré et on réapplique le retrait au démarrage du serveur et à
991
+ // chaque listing (voir `reapplyDeletedOfficialSkills`).
992
+ function deletedOfficialSkillsFile(homeDir) {
993
+ return path.join(homeDir, 'deleted-official-skills.json');
994
+ }
995
+
996
+ function readDeletedOfficialSkills(homeDir) {
997
+ const data = readJson(deletedOfficialSkillsFile(homeDir), null);
998
+ const paths = Array.isArray(data?.paths) ? data.paths : [];
999
+ return new Set(paths.filter((value) => typeof value === 'string'));
1000
+ }
1001
+
1002
+ function recordDeletedOfficialSkill(homeDir, pathValue) {
1003
+ const set = readDeletedOfficialSkills(homeDir);
1004
+ set.add(pathValue);
1005
+ atomicWrite(
1006
+ deletedOfficialSkillsFile(homeDir),
1007
+ `${JSON.stringify({ version: 1, paths: [...set].sort() }, null, 2)}\n`,
1008
+ );
1009
+ }
1010
+
1011
+ // Réapplique le registre : retire du cache Claude Code tout dossier de skill
1012
+ // officiel réapparu après une mise à jour/réinstallation de plugin. Best-effort
1013
+ // et idempotent — un skill déjà absent n'est pas une erreur. Renvoie le nombre
1014
+ // de dossiers effectivement retirés, pour la journalisation au démarrage.
1015
+ function reapplyDeletedOfficialSkills(homeDir = path.join(os.homedir(), '.piecemaker'), runCommand = captureCommand) {
1016
+ const deleted = readDeletedOfficialSkills(homeDir);
1017
+ if (!deleted.size) return 0;
1018
+ let removed = 0;
1019
+ for (const skill of installedPluginSkills(runCommand)) {
1020
+ if (!deleted.has(skill.path) || !skill.absolute || !fs.existsSync(skill.absolute)) continue;
1021
+ try {
1022
+ fs.rmSync(path.dirname(skill.absolute), { recursive: true, force: true });
1023
+ removed += 1;
1024
+ } catch {
1025
+ // Cache verrouillé ou déjà retiré : on n'interrompt pas le balayage.
1026
+ }
1027
+ }
1028
+ return removed;
1029
+ }
1030
+
1031
+ // Nombre maximal d'annexes listées pour un skill (panneau « Skills et
1032
+ // agents ») — borne défensive contre un dossier de skill pathologique.
1033
+ const MAX_SKILL_ASSETS = 200;
1034
+
1035
+ // Les fichiers annexes (assets/scripts) d'un skill, pour l'affichage
1036
+ // indenté sous son bouton dans le panneau « Skills et agents ». `skillRelPath`
1037
+ // est le chemin relatif de son SKILL.md ; on recurse dans les sous-dossiers
1038
+ // (ex. `scripts/`) mais `name` reste le seul basename — jamais le chemin — et
1039
+ // `path` est le chemin relatif au dépôt utilisé pour la suppression. Les
1040
+ // fichiers cachés et les dossiers cachés sont ignorés, SKILL.md n'est jamais
1041
+ // listé comme une annexe, et la liste est triée par nom puis plafonnée.
1042
+ function listSkillAssets(repoRoot, skillRelPath) {
1043
+ const normalized = normalizeManagedPath(skillRelPath);
1044
+ if (!/^piecemaker-plugin\/skills\/[^/]+\/SKILL\.md$/.test(normalized)) return [];
1045
+ const skillDirRel = normalized.replace(/\/SKILL\.md$/, '');
1046
+ const skillDir = path.resolve(repoRoot, ...skillDirRel.split('/'));
1047
+ const assets = [];
1048
+ const walk = (dir, relDir) => {
1049
+ if (assets.length >= MAX_SKILL_ASSETS) return;
1050
+ let entries;
1051
+ try {
1052
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1053
+ } catch {
1054
+ return;
1055
+ }
1056
+ for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
1057
+ if (assets.length >= MAX_SKILL_ASSETS) return;
1058
+ if (entry.name.startsWith('.')) continue;
1059
+ const entryRel = relDir ? `${relDir}/${entry.name}` : entry.name;
1060
+ if (entry.isDirectory()) {
1061
+ walk(path.join(dir, entry.name), entryRel);
1062
+ } else if (entry.isFile()) {
1063
+ if (relDir === '' && entry.name === 'SKILL.md') continue;
1064
+ assets.push({ name: entry.name, path: `${skillDirRel}/${entryRel}` });
1065
+ }
1066
+ }
1067
+ };
1068
+ walk(skillDir, '');
1069
+ assets.sort((a, b) => a.name.localeCompare(b.name));
1070
+ return assets.slice(0, MAX_SKILL_ASSETS);
1071
+ }
1072
+
1073
+ // Les instructions, skills et agents du dépôt, plus les skills des plugins de
1074
+ // marketplace installés (groupe « Skills officiels », lecture seule) : les
1075
+ // aperçus de facturation vivent dans ~/.piecemaker/billing, une hiérarchie
1076
+ // distincte du dépôt, et ne sont pas listés dans l'éditeur « Skills et agents ».
1077
+ // `options.installedSkills` permet aux tests d'injecter la liste des skills de
1078
+ // plugins (sinon elle est lue via le CLI claude, non hermétique).
1079
+ function listManagedFiles(repoRoot, homeDir = path.join(os.homedir(), '.piecemaker'), userHome = os.homedir(), options = {}) {
1080
+ const candidates = ['AGENTS.md', 'CLAUDE.md'];
1081
+ const agentsDir = path.join(repoRoot, 'piecemaker-plugin', 'agents');
1082
+ if (fs.existsSync(agentsDir)) {
1083
+ for (const name of fs.readdirSync(agentsDir).filter((entry) => entry.endsWith('.md')).sort()) {
1084
+ candidates.push(`piecemaker-plugin/agents/${name}`);
1085
+ }
1086
+ }
1087
+ const skillsDir = path.join(repoRoot, 'piecemaker-plugin', 'skills');
1088
+ if (fs.existsSync(skillsDir)) {
1089
+ for (const name of fs.readdirSync(skillsDir).sort()) {
1090
+ const relative = `piecemaker-plugin/skills/${name}/SKILL.md`;
1091
+ if (fs.existsSync(path.join(repoRoot, ...relative.split('/')))) candidates.push(relative);
1092
+ }
1093
+ }
1094
+
1095
+ const files = candidates.map((relativePath) => {
1096
+ const { absolute, normalized } = managedAbsolutePath(repoRoot, relativePath, homeDir);
1097
+ const kind = managedFileKind(normalized);
1098
+ return {
1099
+ path: normalized,
1100
+ name: normalized === 'AGENTS.md' || normalized === 'CLAUDE.md'
1101
+ ? normalized
1102
+ : path.basename(path.dirname(normalized)) === 'agents'
1103
+ ? path.basename(normalized, '.md')
1104
+ : path.basename(path.dirname(normalized)),
1105
+ kind,
1106
+ exists: fs.existsSync(absolute),
1107
+ readonly: false,
1108
+ // Visibilité côté Claude Code (voir claude-assets.cjs) — null pour les
1109
+ // fichiers qui ne sont pas des composants de plugin.
1110
+ claudeCode: claudeAssetStatus(repoRoot, userHome, normalized),
1111
+ // Fichiers annexes (assets/scripts) — seuls les skills en ont un
1112
+ // dossier propre ; un agent n'a jamais d'annexes.
1113
+ assets: kind === 'skill' ? listSkillAssets(repoRoot, normalized) : [],
1114
+ };
1115
+ });
1116
+
1117
+ // Skills des plugins de marketplace installés — lecture seule, groupe
1118
+ // « Skills officiels ». Un plugin installé depuis les onglets marketplace du
1119
+ // pop-up apparaît ainsi dans la liste dès qu'il expose des skills.
1120
+ const installedSkills = options.installedSkills || installedPluginSkills();
1121
+ const deletedOfficial = readDeletedOfficialSkills(homeDir);
1122
+ for (const skill of installedSkills) {
1123
+ // Skill officiel supprimé mais réapparu (mise à jour/réinstallation du
1124
+ // plugin) : on le re-retire du cache et on ne le liste pas — la suppression
1125
+ // reste effective par-delà les mises à jour.
1126
+ if (deletedOfficial.has(skill.path)) {
1127
+ if (skill.absolute && fs.existsSync(skill.absolute)) {
1128
+ try { fs.rmSync(path.dirname(skill.absolute), { recursive: true, force: true }); } catch { /* best-effort */ }
1129
+ }
1130
+ continue;
1131
+ }
1132
+ files.push({
1133
+ path: skill.path,
1134
+ name: skill.name,
1135
+ kind: 'official-skill',
1136
+ exists: true,
1137
+ readonly: true,
1138
+ plugin: skill.plugin,
1139
+ marketplace: skill.marketplace,
1140
+ description: skill.description,
1141
+ claudeCode: null,
1142
+ });
1143
+ }
1144
+ return files;
1145
+ }
1146
+
1147
+ // Lecture minimale du front matter (name/description) d'un skill/agent, pour
1148
+ // le pop-up « Ajouter le plugin legal Claude ». Même grammaire que
1149
+ // admin/markdown.mjs#parseMetadata (module navigateur, non réutilisable
1150
+ // ici — server.cjs ne charge pas de modules ES du dossier admin/), réduite
1151
+ // aux deux clés affichées.
1152
+ function parseFrontMatter(raw) {
1153
+ const match = String(raw || '').match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
1154
+ if (!match) return {};
1155
+ const values = {};
1156
+ for (const line of match[1].split('\n')) {
1157
+ const lineMatch = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
1158
+ if (!lineMatch) continue;
1159
+ let value = lineMatch[2];
1160
+ if (value.startsWith('"')) {
1161
+ try { value = JSON.parse(value); } catch { value = value.replace(/^"|"$/g, ''); }
1162
+ } else if (value.startsWith("'") && value.endsWith("'")) {
1163
+ value = value.slice(1, -1).replaceAll("''", "'");
1164
+ }
1165
+ values[lineMatch[1]] = value;
1166
+ }
1167
+ return values;
1168
+ }
1169
+
1170
+ function pluginComponentFrontMatter(absolutePath) {
1171
+ try {
1172
+ return parseFrontMatter(fs.readFileSync(absolutePath, 'utf8'));
1173
+ } catch {
1174
+ return {};
1175
+ }
1176
+ }
1177
+
1178
+ /**
1179
+ * Les skills et agents PieceMaker, avec leur état d'enregistrement
1180
+ * auprès de Claude Code — support du pop-up de sélection de
1181
+ * « Ajouter le plugin legal Claude ». S'appuie sur `repositoryAssets` /
1182
+ * `claudeAssetStatus` (claude-assets.cjs), la même source que la liste de
1183
+ * fichiers et `files/sync`, pour que les deux vues restent en accord.
1184
+ */
1185
+ function listPluginComponents(repoRoot, userHome = os.homedir()) {
1186
+ return repositoryAssets(repoRoot).map((relativePath) => {
1187
+ const asset = claudeAssetOf(repoRoot, userHome, relativePath);
1188
+ const status = claudeAssetStatus(repoRoot, userHome, relativePath);
1189
+ const sourceFile = asset.type === 'dir' ? path.join(asset.source, 'SKILL.md') : asset.source;
1190
+ const metadata = pluginComponentFrontMatter(sourceFile);
1191
+ return {
1192
+ path: relativePath,
1193
+ kind: asset.kind,
1194
+ slug: asset.slug,
1195
+ name: metadata.name || asset.slug,
1196
+ description: metadata.description || '',
1197
+ state: status.state,
1198
+ // Coché par défaut dans le pop-up : un enregistrement déjà présent,
1199
+ // même périmé (« stale » — autre clone du dépôt), compte comme actif.
1200
+ registered: ['linked', 'copied', 'stale'].includes(status.state),
1201
+ note: status.state === 'conflict'
1202
+ ? `Un ${asset.kind === 'skill' ? 'skill' : 'agent'} personnel du même nom existe déjà dans ~/.claude — non modifiable ici.`
1203
+ : '',
1204
+ };
1205
+ });
1206
+ }
1207
+
1208
+ /**
1209
+ * Applique une sélection de composants (chemins relatifs voulus « actifs »)
1210
+ * en enregistrant ceux cochés et en retirant ceux décochés, via
1211
+ * `registerClaudeAsset`/`unregisterClaudeAsset` — jamais de substitution
1212
+ * maison. Un composant en conflit (fichier personnel homonyme) n'est jamais
1213
+ * touché, coché ou non. Idempotent : ré-appliquer la même sélection ne
1214
+ * change rien à un état déjà atteint.
1215
+ */
1216
+ function applyPluginComponentSelection(repoRoot, userHome, selection) {
1217
+ const wanted = new Set((Array.isArray(selection) ? selection : []).map(String));
1218
+ const assets = repositoryAssets(repoRoot).map((relativePath) => {
1219
+ const current = claudeAssetStatus(repoRoot, userHome, relativePath);
1220
+ if (current.state === 'conflict') return { path: relativePath, ...current };
1221
+ const result = wanted.has(relativePath)
1222
+ ? registerClaudeAsset(repoRoot, userHome, relativePath)
1223
+ : unregisterClaudeAsset(repoRoot, userHome, relativePath);
1224
+ return { path: relativePath, ...result };
1225
+ });
1226
+ return {
1227
+ assets,
1228
+ registered: assets.filter((asset) => asset.state === 'linked' || asset.state === 'copied').length,
1229
+ removed: assets.filter((asset) => asset.state === 'missing' && !wanted.has(asset.path)).length,
1230
+ conflicts: assets.filter((asset) => asset.state === 'conflict'),
1231
+ };
1232
+ }
1233
+
1234
+ function billingLedgerToMarkdown(file) {
1235
+ const month = path.basename(file, '.jsonl');
1236
+ const entries = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean).flatMap((line) => {
1237
+ try { return [JSON.parse(line)]; } catch { return []; }
1238
+ });
1239
+ const clean = (value) => String(value ?? '—').replace(/[\r\n|]+/g, ' ').trim() || '—';
1240
+ const duration = (ms) => Number.isFinite(ms)
1241
+ ? `${Math.floor(ms / 60000)} min ${Math.round((ms % 60000) / 1000)} s`
1242
+ : '—';
1243
+ const rows = entries.map((entry) => {
1244
+ const date = entry.timestamp ? new Date(entry.timestamp).toLocaleString('fr-FR') : '—';
1245
+ const tools = Object.entries(entry.tool_counts || {}).map(([name, count]) => `${name} (${count})`).join(', ') || '—';
1246
+ return `| ${clean(date)} | ${clean(entry.event)} | ${clean(entry.task_label)} | ${clean(entry.dossier)} | ${duration(entry.duration_ms)} | ${clean(tools)} |`;
1247
+ });
1248
+ return [
1249
+ `# Suivi de facturation — ${month}`,
1250
+ '',
1251
+ `${entries.length} événement(s) enregistré(s). Aperçu généré depuis le journal mensuel en lecture seule.`,
1252
+ '',
1253
+ '| Date | Événement | Tâche | Dossier | Durée | Outils |',
1254
+ '| --- | --- | --- | --- | --- | --- |',
1255
+ ...rows,
1256
+ '',
1257
+ ].join('\n');
1258
+ }
1259
+
1260
+ // Lecture (seule) d'un skill de plugin de marketplace : re-résolution du chemin
1261
+ // synthétique `official-skill:<pluginId>/<slug>` vers son SKILL.md dans le cache
1262
+ // Claude Code, via `installedPluginSkills`. Jamais d'écriture — saveManagedFile
1263
+ // refuse ce préfixe (managedAbsolutePath ne le reconnaît pas comme administrable).
1264
+ function readOfficialSkill(pathValue, runCommand = captureCommand) {
1265
+ const skill = installedPluginSkills(runCommand).find((entry) => entry.path === pathValue);
1266
+ if (!skill || !fs.existsSync(skill.absolute)) throw new Error('Skill de plugin introuvable ou plugin désactivé.');
1267
+ return {
1268
+ path: pathValue,
1269
+ kind: 'official-skill',
1270
+ exists: true,
1271
+ content: fs.readFileSync(skill.absolute, 'utf8'),
1272
+ readonly: true,
1273
+ sourceType: 'markdown',
1274
+ };
1275
+ }
1276
+
1277
+ function readManagedFile(repoRoot, relativePath, homeDir = path.join(os.homedir(), '.piecemaker')) {
1278
+ if (typeof relativePath === 'string' && relativePath.startsWith(OFFICIAL_SKILL_PREFIX)) {
1279
+ return readOfficialSkill(relativePath);
1280
+ }
1281
+ const { absolute, normalized } = managedAbsolutePath(repoRoot, relativePath, homeDir);
1282
+ const exists = fs.existsSync(absolute);
1283
+ const kind = managedFileKind(normalized);
1284
+ return {
1285
+ path: normalized,
1286
+ kind,
1287
+ exists,
1288
+ content: exists
1289
+ ? normalized.endsWith('.jsonl') ? billingLedgerToMarkdown(absolute) : fs.readFileSync(absolute, 'utf8')
1290
+ : '',
1291
+ readonly: kind === 'billing',
1292
+ sourceType: normalized.endsWith('.jsonl') ? 'billing-ledger' : 'markdown',
1293
+ };
1294
+ }
1295
+
1296
+ function backupFile(source, relativePath, homeDir) {
1297
+ if (!fs.existsSync(source)) return null;
1298
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1299
+ const target = path.join(homeDir, 'backups', stamp, ...normalizeManagedPath(relativePath).split('/'));
1300
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1301
+ fs.copyFileSync(source, target);
1302
+ return target;
1303
+ }
1304
+
1305
+ function saveManagedFile(repoRoot, homeDir, relativePath, content) {
1306
+ if (typeof content !== 'string') throw new Error('Le contenu Markdown est requis.');
1307
+ if (Buffer.byteLength(content, 'utf8') > MAX_MARKDOWN_BYTES) {
1308
+ throw new Error('Le fichier dépasse la limite de 1 Mo.');
1309
+ }
1310
+ if (managedFileKind(relativePath) === 'billing') throw new Error('Les rapports de facturation sont en lecture seule.');
1311
+ const { absolute, normalized } = managedAbsolutePath(repoRoot, relativePath, homeDir);
1312
+ const backup = backupFile(absolute, normalized, homeDir);
1313
+ atomicWrite(absolute, content.endsWith('\n') ? content : `${content}\n`);
1314
+ return { path: normalized, backup, savedAt: new Date().toISOString() };
1315
+ }
1316
+
1317
+ // Modèles acceptés dans le front matter d'un agent Claude Code. « inherit »
1318
+ // reprend le modèle de la session appelante.
1319
+ const AGENT_MODELS = ['inherit', 'haiku', 'sonnet', 'opus'];
1320
+ const DEFAULT_AGENT_TOOLS = 'Read, Grep, Glob';
1321
+
1322
+ /**
1323
+ * Un agent n'a pas le même front matter qu'un skill : il déclare en plus les
1324
+ * outils auxquels il a droit et le modèle qui l'exécute. On valide donc ces
1325
+ * deux champs ici plutôt que de recopier le gabarit d'un skill.
1326
+ * Une liste d'outils vide signifie « hérite de tous les outils » : la clé est
1327
+ * alors omise, ce que Claude Code interprète ainsi.
1328
+ */
1329
+ function normalizeAgentTools(value) {
1330
+ if (value === undefined || value === null) return DEFAULT_AGENT_TOOLS;
1331
+ const tools = String(value)
1332
+ .split(',')
1333
+ .map((tool) => tool.trim())
1334
+ .filter(Boolean);
1335
+ const seen = [];
1336
+ for (const tool of tools) {
1337
+ // « Bash(git status:*) » : nom d'outil, éventuellement suivi d'un motif.
1338
+ if (!/^[A-Za-z][A-Za-z0-9_-]*(\([^()\n]*\))?$/.test(tool)) {
1339
+ throw new Error(`Outil invalide : « ${tool} ». Utilisez des noms comme Read, Grep, Glob ou Bash(git *).`);
1340
+ }
1341
+ if (!seen.includes(tool)) seen.push(tool);
1342
+ }
1343
+ if (seen.length > 30) throw new Error('Trop d\u2019outils déclarés pour un agent (30 maximum).');
1344
+ return seen.join(', ');
1345
+ }
1346
+
1347
+ function normalizeAgentModel(value) {
1348
+ const model = String(value ?? 'sonnet').trim().toLowerCase() || 'sonnet';
1349
+ if (!AGENT_MODELS.includes(model)) {
1350
+ throw new Error(`Modèle inconnu : « ${model} ». Choisissez ${AGENT_MODELS.join(', ')}.`);
1351
+ }
1352
+ return model;
1353
+ }
1354
+
1355
+ function agentTemplate(slug, title, summary, tools, model) {
1356
+ const metadata = [
1357
+ `name: ${slug}`,
1358
+ `description: ${JSON.stringify(summary)}`,
1359
+ ...(tools ? [`tools: ${tools}`] : []),
1360
+ `model: ${model}`,
1361
+ ].join('\n');
1362
+ const toolLine = tools
1363
+ ? `Outils autorisés : ${tools}.`
1364
+ : 'Aucune restriction d\u2019outils : l\u2019agent hérite de ceux de la session.';
1365
+ return `---\n${metadata}\n---\n\n# ${title}\n\n`
1366
+ + `Vous êtes un sous-agent PieceMaker lancé pour une tâche précise, dans sa\n`
1367
+ + `propre fenêtre de contexte. Décrivez ci-dessous votre rôle et vos règles.\n\n`
1368
+ + `## Mission\n\n${summary}\n\n`
1369
+ + `## Déroulé attendu\n\n`
1370
+ + `1. Rassemblez le contexte nécessaire (documents, mapping, dossier).\n`
1371
+ + `2. Effectuez l\u2019analyse ou la production demandée.\n`
1372
+ + `3. Renvoyez un rapport final autonome : l\u2019agent appelant ne voit pas vos étapes intermédiaires.\n\n`
1373
+ + `## Contraintes\n\n`
1374
+ + `- ${toolLine}\n`
1375
+ + `- Ne levez jamais une anonymisation existante : travaillez sur les codes tels quels.\n`
1376
+ + `- Ne citez aucun texte de loi ni jurisprudence de mémoire.\n`;
1377
+ }
1378
+
1379
+ function skillTemplate(slug, title, summary) {
1380
+ const metadata = `name: ${slug}\ndescription: ${JSON.stringify(summary)}`;
1381
+ return `---\n${metadata}\n---\n\n# ${title}\n\n`
1382
+ + `${summary}\n\n`
1383
+ + `## Quand utiliser ce skill\n\nDécrivez les situations qui doivent déclencher ce skill.\n\n`
1384
+ + `## Déroulé\n\n1. Première étape.\n2. Deuxième étape.\n\n`
1385
+ + `## Points de vigilance\n\n- Ce qu\u2019il ne faut jamais faire.\n`;
1386
+ }
1387
+
1388
+ function createManagedFile(repoRoot, homeDir, { kind, slug, name, description, tools, model } = {}) {
1389
+ if (!['skill', 'agent'].includes(kind)) throw new Error('Choisissez « skill » ou « agent ».');
1390
+ const safeSlug = String(slug || '').trim().toLowerCase();
1391
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(safeSlug)) {
1392
+ throw new Error('L\u2019identifiant doit contenir uniquement des minuscules, chiffres et tirets.');
1393
+ }
1394
+ const title = String(name || safeSlug).replace(/[\r\n]+/g, ' ').trim().slice(0, 80) || safeSlug;
1395
+ const summary = String(description || '').replace(/[\r\n]+/g, ' ').trim().slice(0, 1000);
1396
+ if (!summary) throw new Error('Une description est requise pour que l\u2019agent sache quand utiliser ce fichier.');
1397
+ const relativePath = kind === 'skill'
1398
+ ? `piecemaker-plugin/skills/${safeSlug}/SKILL.md`
1399
+ : `piecemaker-plugin/agents/${safeSlug}.md`;
1400
+ const { absolute } = managedAbsolutePath(repoRoot, relativePath, homeDir);
1401
+ if (fs.existsSync(absolute)) throw new Error('Un fichier portant cet identifiant existe déjà.');
1402
+ const content = kind === 'skill'
1403
+ ? skillTemplate(safeSlug, title, summary)
1404
+ : agentTemplate(safeSlug, title, summary, normalizeAgentTools(tools), normalizeAgentModel(model));
1405
+ saveManagedFile(repoRoot, homeDir, relativePath, content);
1406
+ return readManagedFile(repoRoot, relativePath, homeDir);
1407
+ }
1408
+
1409
+ /**
1410
+ * Écrit un fichier annexe (asset ou script) dans le dossier d'un skill —
1411
+ * `piecemaker-plugin/skills/<slug>/`. Seuls les skills ont un dossier propre :
1412
+ * un agent est un fichier `.md` isolé, sans annexes. Le nom est réduit à son
1413
+ * basename (aucune remontée de dossier), `SKILL.md` ne peut pas être écrasé, et
1414
+ * la cible est revérifiée à l'intérieur du dossier du skill avant écriture.
1415
+ * Renvoie le nom retenu et son chemin relatif au dépôt.
1416
+ */
1417
+ function saveManagedAsset(repoRoot, homeDir, skillRelPath, filename, buffer) {
1418
+ const { absolute, normalized } = managedAbsolutePath(repoRoot, skillRelPath, homeDir);
1419
+ if (managedFileKind(normalized) !== 'skill') {
1420
+ throw new Error('Un fichier annexe ne peut être ajouté qu’à un skill.');
1421
+ }
1422
+ const safeName = path.basename(String(filename || '').trim());
1423
+ if (!safeName || safeName.startsWith('.')) throw new Error('Nom de fichier invalide.');
1424
+ if (safeName === 'SKILL.md') throw new Error('SKILL.md ne peut pas être remplacé ainsi.');
1425
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new Error('Fichier vide.');
1426
+ const skillDir = path.dirname(absolute);
1427
+ const target = path.resolve(skillDir, safeName);
1428
+ if (target !== path.join(skillDir, safeName)) throw new Error('Chemin de fichier invalide.');
1429
+ fs.mkdirSync(skillDir, { recursive: true });
1430
+ const temp = path.join(skillDir, `.${safeName}.${process.pid}.${Date.now()}.tmp`);
1431
+ fs.writeFileSync(temp, buffer);
1432
+ fs.renameSync(temp, target);
1433
+ const slugDir = normalized.replace(/\/SKILL\.md$/, '');
1434
+ return { name: safeName, path: `${slugDir}/${safeName}` };
1435
+ }
1436
+
1437
+ /**
1438
+ * Supprime un unique fichier annexe (asset ou script) d'un skill, sans toucher
1439
+ * au reste du dossier. `managedAbsolutePath` ne reconnaît pas un chemin
1440
+ * d'annexe (`managedFileKind` renvoie `null` dessus), donc on ne peut pas le
1441
+ * réutiliser tel quel : cette fonction reprend la même vérification de
1442
+ * confinement (chemin résolu sous le dépôt, puis realpath contre un lien
1443
+ * symbolique) directement contre `piecemaker-plugin/skills/<slug>/`.
1444
+ * `SKILL.md` n'est jamais une « annexe » — sa suppression passe par
1445
+ * `deleteManagedFile` (suppression du skill entier).
1446
+ */
1447
+ function deleteManagedAsset(repoRoot, homeDir, assetRelPath) {
1448
+ const normalized = normalizeManagedPath(assetRelPath);
1449
+ if (!/^piecemaker-plugin\/skills\/[^/]+\/.+/.test(normalized) || normalized.endsWith('/SKILL.md')) {
1450
+ throw new Error('Ce chemin ne correspond pas à un fichier annexe de skill.');
1451
+ }
1452
+ const root = path.resolve(repoRoot);
1453
+ const absolute = path.resolve(root, ...normalized.split('/'));
1454
+ if (absolute !== root && !absolute.startsWith(`${root}${path.sep}`)) {
1455
+ throw new Error('Chemin de fichier invalide.');
1456
+ }
1457
+ const existing = nearestExistingPath(absolute);
1458
+ const realRoot = fs.realpathSync(nearestExistingPath(root));
1459
+ const realExisting = fs.realpathSync(existing);
1460
+ if (realExisting !== realRoot && !realExisting.startsWith(`${realRoot}${path.sep}`)) {
1461
+ throw new Error('Le fichier résout vers un emplacement extérieur au dépôt.');
1462
+ }
1463
+ if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) {
1464
+ throw new Error('Fichier annexe introuvable.');
1465
+ }
1466
+ backupFile(absolute, normalized, homeDir);
1467
+ fs.rmSync(absolute);
1468
+ return { path: normalized, deletedAt: new Date().toISOString() };
1469
+ }
1470
+
1471
+ /**
1472
+ * Supprime un skill ou un agent du dépôt : seuls ces deux types sont
1473
+ * effaçables ici — instructions (CLAUDE.md/AGENTS.md), skills officiels et
1474
+ * aperçus de facturation ne le sont jamais. Le contenu est sauvegardé avant
1475
+ * retrait, puis l'enregistrement Claude Code (lien ou copie) est nettoyé par
1476
+ * l'appelant via `unregisterClaudeAsset`. Pour un skill on retire le dossier
1477
+ * `piecemaker-plugin/skills/<slug>/` entier (SKILL.md et ses annexes) ; pour un
1478
+ * agent, le seul fichier `.md`.
1479
+ */
1480
+ function deleteManagedFile(repoRoot, homeDir, relativePath) {
1481
+ // Skill de plugin de marketplace : on retire son dossier directement dans le
1482
+ // cache Claude Code — voir `deleteOfficialSkill`.
1483
+ if (typeof relativePath === 'string' && relativePath.startsWith(OFFICIAL_SKILL_PREFIX)) {
1484
+ return deleteOfficialSkill(relativePath, homeDir);
1485
+ }
1486
+ const { absolute, normalized } = managedAbsolutePath(repoRoot, relativePath, homeDir);
1487
+ const kind = managedFileKind(normalized);
1488
+ if (!['skill', 'agent'].includes(kind)) {
1489
+ throw new Error('Seuls un skill ou un agent peuvent être supprimés.');
1490
+ }
1491
+ if (!fs.existsSync(absolute)) throw new Error('Fichier introuvable.');
1492
+ backupFile(absolute, normalized, homeDir);
1493
+ // Un skill vit dans son propre dossier — on supprime le dossier, pas juste
1494
+ // le SKILL.md, pour ne pas laisser un dossier vide derrière.
1495
+ const target = kind === 'skill' ? path.dirname(absolute) : absolute;
1496
+ fs.rmSync(target, { recursive: true, force: true });
1497
+ return { path: normalized, kind, deletedAt: new Date().toISOString() };
1498
+ }
1499
+
1500
+ /**
1501
+ * Supprime le dossier d'un skill de plugin de marketplace directement dans le
1502
+ * cache Claude Code (~/.claude/plugins/cache/…) : le chemin synthétique
1503
+ * `official-skill:<pluginId>/<slug>` est re-résolu vers son SKILL.md via
1504
+ * `installedPluginSkills`, puis le dossier `skills/<slug>/` entier est retiré.
1505
+ * Le retrait est mémorisé dans le registre `deleted-official-skills.json` puis
1506
+ * réappliqué (`reapplyDeletedOfficialSkills`) au démarrage du serveur et à
1507
+ * chaque listing : la suppression survit donc à une réinstallation ou mise à
1508
+ * jour du plugin, qui restaurerait sinon le dossier. C'est une suppression
1509
+ * chirurgicale dans un cache géré, pas une désinstallation, et sans sauvegarde.
1510
+ */
1511
+ function deleteOfficialSkill(pathValue, homeDir = path.join(os.homedir(), '.piecemaker'), runCommand = captureCommand) {
1512
+ const skill = installedPluginSkills(runCommand).find((entry) => entry.path === pathValue);
1513
+ if (!skill || !fs.existsSync(skill.absolute)) throw new Error('Skill de plugin introuvable ou plugin désactivé.');
1514
+ // Enregistrer avant de retirer : même si le rm échoue, une mise à jour
1515
+ // ultérieure ne doit pas ressusciter un skill que l'utilisateur a supprimé.
1516
+ recordDeletedOfficialSkill(homeDir, pathValue);
1517
+ fs.rmSync(path.dirname(skill.absolute), { recursive: true, force: true });
1518
+ return { path: pathValue, kind: 'official-skill', deletedAt: new Date().toISOString() };
1519
+ }
1520
+
1521
+ // Le slug qui identifie un skill/agent sur le disque : le dossier pour un
1522
+ // skill (`piecemaker-plugin/skills/<slug>/SKILL.md`), le basename du `.md` pour
1523
+ // un agent (`piecemaker-plugin/agents/<slug>.md`).
1524
+ function managedSlug(normalized) {
1525
+ const kind = managedFileKind(normalized);
1526
+ if (kind === 'skill') return normalized.split('/')[2];
1527
+ if (kind === 'agent') return path.basename(normalized, '.md');
1528
+ return null;
1529
+ }
1530
+
1531
+ /**
1532
+ * Renomme un skill/agent quand le champ `name:` de son front matter ne
1533
+ * correspond plus au slug de son emplacement. Ce nom sert de nom de dossier
1534
+ * (skill) ou de fichier (agent) et doit rester en accord avec ce que Claude
1535
+ * Code découvre. Pour un skill on déplace le dossier
1536
+ * `piecemaker-plugin/skills/<slug>/` entier (SKILL.md et ses annexes suivent) ;
1537
+ * pour un agent, le seul fichier `.md`. Renvoie `{ renamed, path, previous }`,
1538
+ * ou `{ renamed: false }` si le nom est inchangé.
1539
+ */
1540
+ function renameManagedFile(repoRoot, homeDir, relativePath, newSlug) {
1541
+ const { absolute, normalized } = managedAbsolutePath(repoRoot, relativePath, homeDir);
1542
+ const kind = managedFileKind(normalized);
1543
+ if (!['skill', 'agent'].includes(kind)) {
1544
+ throw new Error('Seuls un skill ou un agent peuvent être renommés.');
1545
+ }
1546
+ const slug = String(newSlug || '').trim();
1547
+ if (!slug || slug === managedSlug(normalized)) return { renamed: false, path: normalized };
1548
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
1549
+ throw new Error('Le nom sert de nom de dossier : uniquement minuscules, chiffres et tirets.');
1550
+ }
1551
+ const targetRelative = kind === 'skill'
1552
+ ? `piecemaker-plugin/skills/${slug}/SKILL.md`
1553
+ : `piecemaker-plugin/agents/${slug}.md`;
1554
+ const { absolute: targetAbsolute, normalized: targetNormalized } = managedAbsolutePath(repoRoot, targetRelative, homeDir);
1555
+ const source = kind === 'skill' ? path.dirname(absolute) : absolute;
1556
+ const destination = kind === 'skill' ? path.dirname(targetAbsolute) : targetAbsolute;
1557
+ if (!fs.existsSync(source)) throw new Error('Fichier introuvable — enregistrez-le avant de le renommer.');
1558
+ if (fs.existsSync(destination)) {
1559
+ throw new Error(`Un ${kind === 'skill' ? 'skill' : 'agent'} nommé « ${slug} » existe déjà.`);
1560
+ }
1561
+ fs.renameSync(source, destination);
1562
+ return { renamed: true, path: targetNormalized, previous: normalized };
1563
+ }
1564
+
1565
+ function isLocalOrigin(origin) {
1566
+ if (!origin) return true;
1567
+ try {
1568
+ const hostname = new URL(origin).hostname;
1569
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';
1570
+ } catch {
1571
+ return false;
1572
+ }
1573
+ }
1574
+
1575
+ function finishAdminTiming(res, metric, startedAt, details = {}) {
1576
+ const durationMs = Number((performance.now() - startedAt).toFixed(1));
1577
+ res.set('Server-Timing', `${metric};dur=${durationMs}`);
1578
+ if (process.env.PIECEMAKER_PERF_LOG === '1' || durationMs >= 250) {
1579
+ const write = durationMs >= 250 ? console.warn : console.log;
1580
+ write(`[PM-PERF] admin.${metric}`, { durationMs, ...details });
1581
+ }
1582
+ }
1583
+
1584
+ /**
1585
+ * Dossiers tamponnables : un par `compilation_dossier_<documentId>.json` écrit
1586
+ * dans le dossier de sortie (voir server.cjs). `folder` est le dossier de
1587
+ * travail (celui du document Word), où les pièces tamponnées sont écrites dans
1588
+ * le sous-dossier « Pièces tamponnées ». Seules les métadonnées utiles au bordereau sont
1589
+ * renvoyées — jamais `texte_integral`, qui contient les pièces en clair.
1590
+ */
1591
+ // Les pièces à tamponner ne viennent plus d'un `compilation_dossier_*.json`
1592
+ // (produit par l'ancien chargement de dossier depuis Word) : la logique s'appuie
1593
+ // désormais sur les dossiers juridiques enregistrés, quelle que soit leur place
1594
+ // dans l'arborescence. Chaque dossier expose ses pièces originales — tout fichier
1595
+ // qui n'est ni `.md` ni `.json` —, identifiées par leur chemin relatif au dossier.
1596
+ async function listDossiers(repoRoot, homeDir) {
1597
+ const config = readRegistryConfig(path.join(homeDir, 'config.json'));
1598
+ const dossiers = [];
1599
+ for (const entry of listConfiguredCases(config)) {
1600
+ let originals;
1601
+ try {
1602
+ originals = await listOriginals(entry.root);
1603
+ } catch {
1604
+ // Dossier déplacé/illisible pendant l'inventaire : on l'omet simplement.
1605
+ continue;
1606
+ }
1607
+ if (!originals.length) continue;
1608
+ dossiers.push({
1609
+ documentId: entry.id,
1610
+ informations: { intitule: entry.name },
1611
+ folder: entry.root,
1612
+ stampedDir: stampedPiecesDirectory(entry.root),
1613
+ documents: originals.map((file) => ({
1614
+ id: file.path,
1615
+ filename: file.path,
1616
+ type_document: (file.extension || '').replace(/^\./, '').toUpperCase(),
1617
+ date_document: '',
1618
+ })),
1619
+ });
1620
+ }
1621
+ return dossiers;
1622
+ }
1623
+
1624
+ const REVEAL_TARGETS = new Set(['files', 'terminal']);
1625
+
1626
+ /**
1627
+ * Résout un chemin reçu du navigateur (relatif à la racine d'un dossier
1628
+ * juridique) vers un chemin absolu, sans jamais remonter hors de cette
1629
+ * racine. Même garde que `startOriginalsJob` (originals-pipeline.cjs) pour
1630
+ * les pièces sélectionnées : `path.resolve` puis vérification du préfixe.
1631
+ */
1632
+ function resolveCasePath(caseRoot, relativePath) {
1633
+ const relative = String(relativePath || '').replaceAll('\\', '/').replace(/^\.\//, '');
1634
+ if (!relative) throw new Error('Chemin de pièce manquant.');
1635
+ const absolute = path.resolve(caseRoot, ...relative.split('/'));
1636
+ if (absolute !== caseRoot && !absolute.startsWith(`${caseRoot}${path.sep}`)) {
1637
+ throw new Error('Pièce hors du dossier juridique.');
1638
+ }
1639
+ if (!fs.existsSync(absolute)) throw new Error('Pièce introuvable.');
1640
+ return absolute;
1641
+ }
1642
+
1643
+ /**
1644
+ * Le Markdown converti d'une pièce originale (chemin relatif à la racine du
1645
+ * dossier). La chronologie ne connaît une pièce que par le chemin de son
1646
+ * ORIGINAL (`document-index.cjs`, sur `originalFilesOverview`) ; retrouver son
1647
+ * `.md` reprend la même clé de correspondance que `originalFilesOverview`
1648
+ * (`documentKey` du basename), sans supposer d'emplacement fixe — le
1649
+ * converti vit sous `Fichiers convertis PieceMaker/`, mais un `.md` migré
1650
+ * depuis une version antérieure peut encore traîner à la racine.
1651
+ */
1652
+ async function convertedMarkdownRelativePath(caseRoot, originalRelativePath) {
1653
+ const key = documentKey(path.basename(String(originalRelativePath || '')));
1654
+ const files = await safeCaseFiles(caseRoot);
1655
+ return files.find((file) => path.extname(file).toLowerCase() === '.md' && documentKey(file) === key) || null;
1656
+ }
1657
+
1658
+ /**
1659
+ * Commandes candidates pour montrer un dossier du poste de travail, essayées
1660
+ * dans l'ordre jusqu'à ce que l'une démarre. Fonction pure : `platform` suit la
1661
+ * convention de `process.platform`, aucun chemin n'est concaténé dans une
1662
+ * chaîne de shell (les arguments partent tels quels à `spawn`, sans `shell`).
1663
+ */
1664
+ function revealCommands(platform, target, absolutePath) {
1665
+ if (!REVEAL_TARGETS.has(target)) throw new Error('Action de dossier inconnue.');
1666
+ const isAbsolute = platform === 'win32' ? path.win32.isAbsolute : path.posix.isAbsolute;
1667
+ if (!isAbsolute(String(absolutePath || ''))) throw new Error('Chemin de dossier invalide.');
1668
+ if (platform === 'darwin') {
1669
+ return target === 'terminal'
1670
+ ? [{ command: 'open', args: ['-a', 'Terminal', absolutePath] }]
1671
+ // `-R` révèle le dossier dans sa fenêtre parente, comme « Afficher dans le Finder ».
1672
+ : [{ command: 'open', args: ['-R', absolutePath] }];
1673
+ }
1674
+ if (platform === 'win32') {
1675
+ return target === 'terminal'
1676
+ ? [
1677
+ { command: 'wt.exe', args: ['-d', absolutePath] },
1678
+ // Repli sans Windows Terminal : PowerShell ouvre l'invite de commandes
1679
+ // dans le dossier, `-WorkingDirectory` évite tout `cd` à échapper.
1680
+ { command: 'powershell.exe', args: ['-NoProfile', '-Command', `Start-Process -FilePath cmd.exe -WorkingDirectory '${absolutePath.replace(/'/g, "''")}'`] },
1681
+ ]
1682
+ : [{ command: 'explorer.exe', args: [`/select,${absolutePath}`] }];
1683
+ }
1684
+ return target === 'terminal'
1685
+ ? [
1686
+ { command: 'x-terminal-emulator', args: [`--working-directory=${absolutePath}`] },
1687
+ { command: 'gnome-terminal', args: [`--working-directory=${absolutePath}`] },
1688
+ { command: 'konsole', args: ['--workdir', absolutePath] },
1689
+ { command: 'xterm', args: ['-e', 'sh', '-c', 'cd "$0" && exec "${SHELL:-/bin/sh}"', absolutePath] },
1690
+ ]
1691
+ : [{ command: 'xdg-open', args: [absolutePath] }];
1692
+ }
1693
+
1694
+ function spawnDetached({ command, args }) {
1695
+ return new Promise((resolve, reject) => {
1696
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
1697
+ child.once('error', reject);
1698
+ // `spawn` confirme le démarrage : le processus survit ensuite à PieceMaker.
1699
+ child.once('spawn', () => {
1700
+ child.unref();
1701
+ resolve(command);
1702
+ });
1703
+ });
1704
+ }
1705
+
1706
+ async function revealLocalFolder(platform, target, absolutePath) {
1707
+ const candidates = revealCommands(platform, target, absolutePath);
1708
+ let lastError = null;
1709
+ for (const candidate of candidates) {
1710
+ try {
1711
+ return await spawnDetached(candidate);
1712
+ } catch (error) {
1713
+ lastError = error;
1714
+ }
1715
+ }
1716
+ throw new Error(target === 'terminal'
1717
+ ? `Aucun terminal n’a pu être ouvert sur ce poste (${lastError?.message || 'commande introuvable'}).`
1718
+ : `Le gestionnaire de fichiers n’a pas pu être ouvert (${lastError?.message || 'commande introuvable'}).`);
1719
+ }
1720
+
1721
+ // Installation à la demande des moteurs locaux depuis le tiroir de la carte de
1722
+ // configuration : un clic sur « Installer » relance l'étape d'installateur
1723
+ // correspondante en arrière-plan (non interactif), au lieu d'obliger l'utilisateur
1724
+ // à ouvrir un terminal. Chaque composant est mappé sur son étape.
1725
+ const INSTALL_STEP_IDS = {
1726
+ gliner: '03-python-gliner',
1727
+ mineru: '04-conversion-md',
1728
+ };
1729
+ const installJobs = new Map();
1730
+ let activeInstallComponent = null;
1731
+
1732
+ // Nettoie une ligne de sortie de l'installateur (codes ANSI, spinner) pour n'en
1733
+ // garder qu'un texte de progression lisible ; les lignes JSON de warmup.py
1734
+ // portent un champ `message` plus parlant que la ligne brute.
1735
+ function cleanInstallProgressLine(raw) {
1736
+ // eslint-disable-next-line no-control-regex
1737
+ const stripped = String(raw).replace(/\u001b\[[0-9;]*[A-Za-z]/g, '').replace(/[\r\t]+/g, ' ').trim();
1738
+ if (!stripped) return '';
1739
+ try {
1740
+ const parsed = JSON.parse(stripped);
1741
+ if (parsed && typeof parsed.message === 'string') return parsed.message.trim();
1742
+ } catch { /* ligne texte ordinaire */ }
1743
+ return stripped;
1744
+ }
1745
+
1746
+ function startInstallJob(repoRoot, component) {
1747
+ const stepId = INSTALL_STEP_IDS[component];
1748
+ if (!stepId) throw new Error(`Composant non installable : ${component}`);
1749
+ if (activeInstallComponent) {
1750
+ throw Object.assign(new Error(`Une installation est déjà en cours (${activeInstallComponent}).`), { conflict: true });
1751
+ }
1752
+ const installer = path.join(repoRoot, 'installer', 'bin', 'piecemaker.mjs');
1753
+ if (!fs.existsSync(installer)) throw new Error('Installateur PieceMaker introuvable.');
1754
+ const id = crypto.randomUUID();
1755
+ const job = { id, component, state: 'running', progress: 'Démarrage de l’installation…', startedAt: Date.now(), finishedAt: null, error: '' };
1756
+ installJobs.set(id, job);
1757
+ activeInstallComponent = component;
1758
+
1759
+ const child = spawn(process.execPath, [installer, '--step', stepId], {
1760
+ cwd: repoRoot,
1761
+ windowsHide: true,
1762
+ stdio: ['ignore', 'pipe', 'pipe'],
1763
+ // PIECEMAKER_YES=1 : accepte les valeurs par défaut (MinerU inclus) sans TTY.
1764
+ env: { ...process.env, PIECEMAKER_YES: '1', NO_COLOR: '1' },
1765
+ });
1766
+ const tailStderr = [];
1767
+ const onLine = (chunk) => {
1768
+ for (const part of String(chunk).split('\n')) {
1769
+ const line = cleanInstallProgressLine(part);
1770
+ if (line) job.progress = line;
1771
+ }
1772
+ };
1773
+ child.stdout.on('data', onLine);
1774
+ child.stderr.on('data', (chunk) => {
1775
+ onLine(chunk);
1776
+ tailStderr.push(String(chunk));
1777
+ while (tailStderr.join('').length > 4000) tailStderr.shift();
1778
+ });
1779
+ child.once('error', (error) => {
1780
+ job.state = 'failed';
1781
+ job.error = error.message;
1782
+ job.finishedAt = Date.now();
1783
+ activeInstallComponent = null;
1784
+ });
1785
+ child.once('close', (code) => {
1786
+ if (job.state === 'running') {
1787
+ job.state = code === 0 ? 'done' : 'failed';
1788
+ if (code !== 0) job.error = cleanInstallProgressLine(tailStderr.join('')) || `L’installateur s’est arrêté avec le code ${code}.`;
1789
+ if (code === 0) job.progress = 'Installation terminée.';
1790
+ job.finishedAt = Date.now();
1791
+ }
1792
+ activeInstallComponent = null;
1793
+ // Purge tardive : garde le job interrogeable un moment après la fin.
1794
+ setTimeout(() => installJobs.delete(id), 5 * 60 * 1000).unref?.();
1795
+ });
1796
+ return job;
1797
+ }
1798
+
1799
+ function publicInstallJob(job) {
1800
+ if (!job) return null;
1801
+ return { id: job.id, component: job.component, state: job.state, progress: job.progress, error: job.error };
1802
+ }
1803
+
1804
+ // `AAAA-MM` : seule forme acceptée pour le mois de l'export d'historique. Cette
1805
+ // valeur part ensuite dans des arguments `git log` (`--since`/`--until`) et
1806
+ // dans un nom de fichier — on la valide donc avant tout usage.
1807
+ const HISTORY_MONTH_RE = /^\d{4}-(0[1-9]|1[0-2])$/;
1808
+
1809
+ // Formats acceptés par les routes d'export « papier » (chronologie, historique).
1810
+ const EXPORT_FORMATS = new Set(['pdf', 'docx']);
1811
+ const EXPORT_CONTENT_TYPES = {
1812
+ pdf: 'application/pdf',
1813
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
1814
+ };
1815
+
1816
+ function validateExportFormat(format) {
1817
+ if (!EXPORT_FORMATS.has(format)) throw new Error('Format d’export invalide (pdf ou docx).');
1818
+ return format;
1819
+ }
1820
+
1821
+ /**
1822
+ * Repli ASCII d'un nom de fichier pour l'en-tête `Content-Disposition` : les
1823
+ * vieux clients HTTP ignorent le paramètre `filename*` et n'affichent que
1824
+ * `filename`, qui doit rester dans le jeu de caractères historique de l'en-tête
1825
+ * (RFC 6266 §4.3). Nos noms contiennent accents, tiret cadratin et guillemets
1826
+ * français (« Chronologie — Dossier Martin ») : on translittère les accents,
1827
+ * puis on réduit le reste du hors-ASCII (tirets, guillemets…) à un tiret
1828
+ * simple, et on retire pour finir tout guillemet droit ou caractère de
1829
+ * contrôle — les deux interdits dans la valeur de l'en-tête.
1830
+ */
1831
+ function asciiFallbackFilename(name) {
1832
+ const withoutAccents = String(name).normalize('NFKD').replace(/[\u0300-\u036f]/g, '');
1833
+ return withoutAccents
1834
+ .replace(/[^\x20-\x7E]/g, '-')
1835
+ .replace(/["\x00-\x1F\x7F]/g, '')
1836
+ .trim() || 'export';
1837
+ }
1838
+
1839
+ /**
1840
+ * Génère un document imprimable (PDF/DOCX) à partir d'un HTML déjà rendu, via
1841
+ * pandoc (`doc-generate.cjs`) — avec repli intégral sur LibreOffice
1842
+ * (`office-to-pdf.cjs`) si pandoc est absent du poste —, et l'envoie en
1843
+ * téléchargement. Factorisée entre les deux routes d'export : seule leur
1844
+ * mise en forme HTML diffère.
1845
+ *
1846
+ * Le dossier temporaire contient le HTML source PUIS le document produit —
1847
+ * potentiellement en clair, avec les vrais noms des clients (vue cabinet
1848
+ * ré-identifiée). Il est donc systématiquement détruit après l'envoi, y
1849
+ * compris si la conversion échoue : aucun fichier ne doit survivre dans /tmp.
1850
+ *
1851
+ * Les en-têtes ne sont posés qu'une fois le document produit : tant que
1852
+ * `generateDocument` n'a pas résolu, l'appelant peut encore répondre en JSON
1853
+ * en cas d'erreur (voir les routes `/repository/chronology/export` et
1854
+ * `/history/export`).
1855
+ */
1856
+ async function sendGeneratedDocument(res, { html, filename, format }) {
1857
+ validateExportFormat(format);
1858
+
1859
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'piecemaker-export-'));
1860
+ const cleanup = () => {
1861
+ try {
1862
+ fs.rmSync(dir, { recursive: true, force: true });
1863
+ } catch {
1864
+ // Dossier temporaire : un échec de nettoyage n'a aucune conséquence
1865
+ // fonctionnelle, seulement un déchet dans /tmp — on ne le remonte pas.
1866
+ }
1867
+ };
1868
+
1869
+ let producedPath;
1870
+ try {
1871
+ const { path: generatedPath, engine } = await generateDocument(html, dir, { format });
1872
+ producedPath = generatedPath;
1873
+ // Trace serveur uniquement (jamais en en-tête ni en corps de réponse) :
1874
+ // permet de diagnostiquer, a posteriori, quel moteur a réellement produit
1875
+ // le document sur ce poste (pandoc+typst, pandoc+libreoffice, ou le
1876
+ // repli libreoffice pur si pandoc est absent).
1877
+ console.log(`[export] ${filename}.${outputExtension(format)} généré via ${engine}`);
1878
+ } catch (error) {
1879
+ cleanup();
1880
+ throw error;
1881
+ }
1882
+
1883
+ const ext = outputExtension(format);
1884
+ const fullName = `${filename}.${ext}`;
1885
+ res.setHeader('Content-Type', EXPORT_CONTENT_TYPES[format]);
1886
+ res.setHeader(
1887
+ 'Content-Disposition',
1888
+ `attachment; filename="${asciiFallbackFilename(fullName)}"; filename*=UTF-8''${encodeURIComponent(fullName)}`,
1889
+ );
1890
+
1891
+ const stream = fs.createReadStream(producedPath);
1892
+ // Le nettoyage doit survenir quoi qu'il arrive une fois l'envoi entamé : fin
1893
+ // normale (`close`, émis par un ReadStream après `end` comme après un
1894
+ // `destroy` sur erreur) ou coupure côté client (`close` sur la réponse).
1895
+ // `fs.rmSync` ci-dessus est idempotent : peu importe lequel des deux
1896
+ // déclenche `cleanup()` en premier.
1897
+ stream.once('close', cleanup);
1898
+ res.once('close', cleanup);
1899
+ // Sans écouteur, un `error` sur le flux remonterait en exception non
1900
+ // interceptée et tuerait le serveur — or il sert aussi l'administration et les
1901
+ // WebSocket. Les en-têtes sont déjà partis : on ne peut que couper.
1902
+ stream.once('error', (error) => {
1903
+ console.warn(`[export] Lecture du document généré interrompue: ${error.message}`);
1904
+ cleanup();
1905
+ res.destroy();
1906
+ });
1907
+ stream.pipe(res);
1908
+ }
1909
+
1910
+ const documentMetaMutationQueues = new Map();
1911
+
1912
+ async function serializeDocumentMetaMutation(caseRoot, callback) {
1913
+ const key = path.resolve(caseRoot);
1914
+ const previous = documentMetaMutationQueues.get(key) || Promise.resolve();
1915
+ const current = previous.catch(() => {}).then(callback);
1916
+ documentMetaMutationQueues.set(key, current);
1917
+ try {
1918
+ return await current;
1919
+ } finally {
1920
+ if (documentMetaMutationQueues.get(key) === current) documentMetaMutationQueues.delete(key);
1921
+ }
1922
+ }
1923
+
1924
+ async function applyDocumentMetaMutation({
1925
+ legalCase,
1926
+ relativePath,
1927
+ correction,
1928
+ homeDir,
1929
+ envFile,
1930
+ applyCorrection = applyDocumentIndexCorrection,
1931
+ createHistoryCommit = createCommit,
1932
+ }) {
1933
+ return serializeDocumentMetaMutation(legalCase.root, async () => {
1934
+ const mutation = applyCorrection(legalCase.root, relativePath, correction);
1935
+ let history;
1936
+ try {
1937
+ history = await createHistoryCommit({
1938
+ casesRoot: legalCase.casesRoot,
1939
+ caseName: legalCase.caseName,
1940
+ homeDir,
1941
+ envFile,
1942
+ label: 'Correction manuelle de la chronologie',
1943
+ description: `Mise à jour déterministe de la pièce ${mutation.documentKey.slice(0, 12).toUpperCase()}.`,
1944
+ event: 'admin-document-meta-edit',
1945
+ paths: [path.relative(legalCase.root, documentIndexFile(legalCase.root)).split(path.sep).join('/')],
1946
+ waitForLockMs: 10_000,
1947
+ });
1948
+ } catch (error) {
1949
+ history = { created: false, error: error.message };
1950
+ }
1951
+ return {
1952
+ mutation,
1953
+ history: {
1954
+ created: Boolean(history?.created),
1955
+ hash: history?.commit || null,
1956
+ skipped: history?.skipped || null,
1957
+ error: history?.error || null,
1958
+ },
1959
+ };
1960
+ });
1961
+ }
1962
+
1963
+ async function loadAdminChronology({
1964
+ caseRoot,
1965
+ buildLocalChronology = buildChronology,
1966
+ }) {
1967
+ return buildLocalChronology(caseRoot, {
1968
+ deanonymizeLabels: true,
1969
+ includeManualDecisions: true,
1970
+ });
1971
+ }
1972
+
1973
+ function normalizeChronologyScope(caseRoot, value) {
1974
+ const scope = String(value || '').trim().replaceAll('\\', '/').replace(/^\/|\/$/g, '');
1975
+ if (!scope) return null;
1976
+ const parts = scope.split('/');
1977
+ if (parts.some((part) => !part || part === '.' || part === '..')) throw new Error('Sous-dossier invalide.');
1978
+ const root = fs.realpathSync(caseRoot);
1979
+ const absolute = path.resolve(caseRoot, ...parts);
1980
+ const resolved = fs.realpathSync(absolute);
1981
+ if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new Error('Sous-dossier hors du dossier juridique.');
1982
+ if (!fs.statSync(resolved).isDirectory()) throw new Error('La cible choisie n’est pas un sous-dossier.');
1983
+ return parts.join('/');
1984
+ }
1985
+
1986
+ function chronologyScopeFromFolder(caseRoot, selectedFolder) {
1987
+ const root = fs.realpathSync(caseRoot);
1988
+ const selected = fs.realpathSync(selectedFolder);
1989
+ if (selected !== root && !selected.startsWith(`${root}${path.sep}`)) throw new Error('Sous-dossier hors du dossier juridique.');
1990
+ const relative = path.relative(root, selected).split(path.sep).join('/');
1991
+ return relative ? normalizeChronologyScope(root, relative) : null;
1992
+ }
1993
+
1994
+ function chronologyFolders(documents) {
1995
+ const folders = new Set();
1996
+ for (const document of documents || []) {
1997
+ const parts = String(document.path || '').replaceAll('\\', '/').split('/');
1998
+ parts.pop();
1999
+ for (let index = 1; index <= parts.length; index += 1) folders.add(parts.slice(0, index).join('/'));
2000
+ }
2001
+ return [...folders].sort((left, right) => left.localeCompare(right, 'fr'));
2002
+ }
2003
+
2004
+ function scopeChronology(chronology, scope) {
2005
+ const folders = chronology.folders || chronologyFolders(chronology.documents);
2006
+ const documents = scope
2007
+ ? chronology.documents.filter((document) => {
2008
+ const documentPath = String(document.path || '');
2009
+ return documentPath === scope || documentPath.startsWith(`${scope}/`);
2010
+ })
2011
+ : chronology.documents;
2012
+ const datedDocuments = documents.filter((document) => document.dateIso);
2013
+ const undatedDocuments = documents.filter((document) => !document.dateIso);
2014
+ const documentIds = new Set(documents.map((document) => document.id));
2015
+ const entities = (chronology.entities || [])
2016
+ .map((entity) => ({ ...entity, documents: (entity.documents || []).filter((id) => documentIds.has(id)) }))
2017
+ .filter((entity) => entity.documents.length > 0);
2018
+ const dated = datedDocuments.map((document) => document.dateIso).sort();
2019
+ return {
2020
+ ...chronology,
2021
+ scope: scope || null,
2022
+ folders,
2023
+ documents,
2024
+ datedDocuments,
2025
+ undatedDocuments,
2026
+ entities,
2027
+ stats: {
2028
+ ...chronology.stats,
2029
+ documents: documents.length,
2030
+ indexed: documents.filter((document) => document.indexed).length,
2031
+ dated: datedDocuments.length,
2032
+ entities: entities.length,
2033
+ span: dated.length ? { from: dated[0], to: dated[dated.length - 1] } : null,
2034
+ },
2035
+ };
2036
+ }
2037
+
2038
+ function createAdminRouter({
2039
+ repoRoot = path.resolve(__dirname, '..'),
2040
+ homeDir = path.join(os.homedir(), '.piecemaker'),
2041
+ userHome = os.homedir(),
2042
+ getRuntimeStatus = () => ({}),
2043
+ fetchImpl = global.fetch,
2044
+ pickFolder = selectLocalFolder,
2045
+ // Adaptation PieceMaker/CloudCLI : monté sur le serveur CloudCLI, le routeur
2046
+ // est déjà protégé par `authenticateToken`, et l'origine peut être une app
2047
+ // Electron (`file://`) ou un hôte LAN. L'appelant peut donc fournir sa propre
2048
+ // règle ; par défaut on garde la restriction locale d'origine.
2049
+ isOriginAllowed = isLocalOrigin,
2050
+ } = {}) {
2051
+ // Lazy import keeps the pure filesystem/Git helpers testable before npm
2052
+ // dependencies are installed. The running server already depends on Express.
2053
+ const express = require('express');
2054
+ const router = express.Router();
2055
+ const configFile = path.join(homeDir, 'config.json');
2056
+ const envFile = path.join(repoRoot, '.env');
2057
+ const registryConfig = () => readRegistryConfig(configFile);
2058
+ const selectedCase = (reference) => {
2059
+ const config = registryConfig();
2060
+ const legalCase = resolveCaseReference(config, reference);
2061
+ // Migration idempotente des dossiers enregistrés avant l'introduction de
2062
+ // l'arborescence métier. Le manifeste fige les noms utilisés par ce dossier.
2063
+ ensureCaseFolderStructure(legalCase.root, config);
2064
+ return legalCase;
2065
+ };
2066
+
2067
+ // Migration : l'identité de commit ne vivait que dans le `.env` du clone
2068
+ // runtime, invisible au hook d'édition qui tourne depuis le cache du plugin.
2069
+ // On la recopie une fois dans config.json, lu par resolveCommitIdentity.
2070
+ try {
2071
+ const bootConfig = readJson(configFile, {});
2072
+ const bootName = String(readEnvFile(envFile)[COMMIT_USER_NAME_KEY] || '').trim();
2073
+ if (bootName && !bootConfig?.commits?.userName) {
2074
+ bootConfig.commits = { ...(bootConfig.commits || {}), userName: bootName };
2075
+ atomicWrite(configFile, `${JSON.stringify(bootConfig, null, 2)}\n`);
2076
+ }
2077
+ } catch {
2078
+ // La migration est opportuniste ; son échec ne doit pas empêcher l'admin.
2079
+ }
2080
+
2081
+ // Réapplique au démarrage les suppressions de skills officiels : une mise à
2082
+ // jour de plugin (`claude plugin update`) survenue serveur éteint aurait pu
2083
+ // restaurer leur dossier dans le cache Claude Code.
2084
+ try {
2085
+ reapplyDeletedOfficialSkills(homeDir);
2086
+ } catch {
2087
+ // Best-effort : le balayage ne doit jamais empêcher l'admin de démarrer.
2088
+ }
2089
+
2090
+ router.use((req, res, next) => {
2091
+ res.set('Cache-Control', 'no-store');
2092
+ if (!isOriginAllowed(req.get('Origin'))) {
2093
+ return res.status(403).json({ error: 'L’administration PieceMaker est réservée à une page locale.' });
2094
+ }
2095
+ next();
2096
+ });
2097
+
2098
+ router.get('/status', (req, res) => {
2099
+ const pkg = readJson(path.join(repoRoot, 'package.json'), {});
2100
+ const files = listManagedFiles(repoRoot, homeDir, userHome);
2101
+ res.json({
2102
+ ok: true,
2103
+ version: pkg.version || 'inconnue',
2104
+ repoRoot,
2105
+ certificatesReady:
2106
+ fs.existsSync(path.join(repoRoot, 'websocket-server', 'localhost.crt')) &&
2107
+ fs.existsSync(path.join(repoRoot, 'websocket-server', 'localhost.key')),
2108
+ files: {
2109
+ skills: files.filter((file) => file.kind === 'skill').length,
2110
+ agents: files.filter((file) => file.kind === 'agent').length,
2111
+ registered: files.filter((file) => ['linked', 'copied'].includes(file.claudeCode?.state)).length,
2112
+ },
2113
+ ...getRuntimeStatus(),
2114
+ });
2115
+ });
2116
+
2117
+ router.get('/configuration', async (req, res) => {
2118
+ try {
2119
+ res.json(await configurationOverview({
2120
+ repoRoot,
2121
+ homeDir,
2122
+ userHome,
2123
+ getRuntimeStatus,
2124
+ fetchImpl,
2125
+ }));
2126
+ } catch (error) {
2127
+ res.status(500).json({ error: error.message });
2128
+ }
2129
+ });
2130
+
2131
+ router.post('/configuration/models/check', async (req, res) => {
2132
+ try {
2133
+ res.json({
2134
+ ok: true,
2135
+ model: String(req.body?.model || ''),
2136
+ checkedAt: new Date().toISOString(),
2137
+ ...await checkOllamaModelUpdate(req.body?.model, fetchImpl),
2138
+ });
2139
+ } catch (error) {
2140
+ res.status(400).json({ error: error.message });
2141
+ }
2142
+ });
2143
+
2144
+ // Installe un composant absent (GLiNER, MinerU) en relançant son étape
2145
+ // d'installateur en arrière-plan ; le tiroir suit ensuite l'avancement en
2146
+ // interrogeant GET /configuration/install?id=.
2147
+ router.post('/configuration/install', (req, res) => {
2148
+ try {
2149
+ const component = String(req.body?.component || '');
2150
+ const job = startInstallJob(repoRoot, component);
2151
+ res.json({ ok: true, job: publicInstallJob(job) });
2152
+ } catch (error) {
2153
+ res.status(error.conflict ? 409 : 400).json({ error: error.message });
2154
+ }
2155
+ });
2156
+
2157
+ router.get('/configuration/install', (req, res) => {
2158
+ const job = installJobs.get(String(req.query?.id || ''));
2159
+ if (!job) return res.status(404).json({ error: 'Installation inconnue ou expirée.' });
2160
+ res.json({ ok: true, job: publicInstallJob(job) });
2161
+ });
2162
+
2163
+ router.get('/settings', (req, res) => {
2164
+ const config = { ...defaultConfig(repoRoot, homeDir), ...readJson(configFile, {}) };
2165
+ config.caseFolderStructure = configuredCaseFolderStructure(config);
2166
+ const env = readEnvFile(envFile);
2167
+ const publicEnv = {};
2168
+ const secrets = {};
2169
+ for (const key of ENV_KEYS) {
2170
+ if (SECRET_KEYS.has(key)) secrets[key] = maskSecret(env[key]);
2171
+ else publicEnv[key] = env[key] || '';
2172
+ }
2173
+ res.json({ config, env: publicEnv, secrets });
2174
+ });
2175
+
2176
+ router.put('/settings', (req, res) => {
2177
+ try {
2178
+ const current = { ...defaultConfig(repoRoot, homeDir), ...readJson(configFile, {}) };
2179
+ current.caseFolderStructure = configuredCaseFolderStructure(current);
2180
+ const patch = req.body?.config || {};
2181
+ const next = { ...current };
2182
+
2183
+ if (patch.port !== undefined) {
2184
+ const port = Number(patch.port);
2185
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('Le port doit être compris entre 1024 et 65535.');
2186
+ next.port = port;
2187
+ }
2188
+ if (patch.pythonPath !== undefined) next.pythonPath = String(patch.pythonPath || '').trim() || null;
2189
+ if (patch.adminTheme !== undefined) next.adminTheme = validateAdminTheme(patch.adminTheme);
2190
+ if (patch.caseFolderStructure !== undefined) {
2191
+ next.caseFolderStructure = normalizeCaseFolderStructure({
2192
+ ...current.caseFolderStructure,
2193
+ ...(patch.caseFolderStructure && typeof patch.caseFolderStructure === 'object'
2194
+ ? patch.caseFolderStructure
2195
+ : {}),
2196
+ });
2197
+ }
2198
+
2199
+ // L'identité de commit est aussi mémorisée dans config.json : le hook
2200
+ // d'édition (lancé depuis le cache du plugin) ne peut pas lire le `.env`
2201
+ // du clone runtime, mais lit config.json.
2202
+ const rawName = req.body?.env?.[COMMIT_USER_NAME_KEY];
2203
+ if (rawName !== undefined) {
2204
+ const requested = String(rawName || '').trim();
2205
+ if (requested && requested !== '********') {
2206
+ const { name } = resolveCommitIdentity({ identity: { name: requested } });
2207
+ next.commits = { ...(next.commits || {}), userName: name };
2208
+ }
2209
+ }
2210
+
2211
+ atomicWrite(configFile, `${JSON.stringify(next, null, 2)}\n`);
2212
+ updateEnvFile(envFile, req.body?.env || {}, req.body?.clearSecrets || []);
2213
+ res.json({
2214
+ ok: true,
2215
+ restartRequired: patch.port !== undefined || patch.pythonPath !== undefined,
2216
+ });
2217
+ } catch (error) {
2218
+ res.status(400).json({ error: error.message });
2219
+ }
2220
+ });
2221
+
2222
+ // Entités institutionnelles à ne jamais anonymiser — liste globale, éditable.
2223
+ // La détection GLiNER n'est pas touchée : ces termes sont écartés au moment de
2224
+ // bâtir le mapping (voir mapping.cjs / institutional-terms.cjs).
2225
+ router.get('/institutional-terms', (req, res) => {
2226
+ try {
2227
+ res.json(readInstitutionalTerms());
2228
+ } catch (error) {
2229
+ res.status(500).json({ error: error.message });
2230
+ }
2231
+ });
2232
+
2233
+ router.put('/institutional-terms', (req, res) => {
2234
+ try {
2235
+ const terms = req.body?.terms;
2236
+ if (!Array.isArray(terms)) throw new Error('Le corps doit contenir un tableau « terms ».');
2237
+ if (terms.length > 1000) throw new Error('Liste trop longue (1000 termes maximum).');
2238
+ res.json({ ok: true, ...writeInstitutionalTerms(terms) });
2239
+ } catch (error) {
2240
+ res.status(400).json({ error: error.message });
2241
+ }
2242
+ });
2243
+
2244
+ router.get('/files', (req, res) => {
2245
+ res.json({ files: listManagedFiles(repoRoot, homeDir, userHome) });
2246
+ });
2247
+
2248
+ router.post('/files', (req, res) => {
2249
+ try {
2250
+ const file = createManagedFile(repoRoot, homeDir, req.body);
2251
+ // Enregistrement immédiat auprès de Claude Code, sans manifest ni cache.
2252
+ const claudeCode = registerClaudeAsset(repoRoot, userHome, file.path);
2253
+ res.status(201).json({ ok: true, file: { ...file, claudeCode } });
2254
+ } catch (error) {
2255
+ res.status(400).json({ error: error.message });
2256
+ }
2257
+ });
2258
+
2259
+ router.post('/files/sync', (req, res) => {
2260
+ try {
2261
+ res.json({ ok: true, ...syncClaudeAssets(repoRoot, userHome) });
2262
+ } catch (error) {
2263
+ res.status(500).json({ error: error.message });
2264
+ }
2265
+ });
2266
+
2267
+ // Support du bouton « Ajouter le plugin legal Claude » (onglet Skills et
2268
+ // agents) : liste les composants PieceMaker avec leur état
2269
+ // d'enregistrement, pour le pop-up de sélection.
2270
+ router.get('/plugin/components', (req, res) => {
2271
+ try {
2272
+ const components = listPluginComponents(repoRoot, userHome);
2273
+ res.json({
2274
+ plugin: { installed: components.some((component) => component.registered), version: '' },
2275
+ components,
2276
+ });
2277
+ } catch (error) {
2278
+ res.status(500).json({ error: error.message });
2279
+ }
2280
+ });
2281
+
2282
+ // Enregistre directement dans ~/.claude
2283
+ // uniquement les composants cochés dans le pop-up — désenregistre ceux
2284
+ // décochés. `components` : chemins relatifs (piecemaker-plugin/agents/...
2285
+ // ou piecemaker-plugin/skills/.../SKILL.md) tels que renvoyés par
2286
+ // GET /plugin/components.
2287
+ router.post('/plugin/install', async (req, res) => {
2288
+ try {
2289
+ const selection = Array.isArray(req.body?.components) ? req.body.components : [];
2290
+ const plugin = await ensureClaudePluginActive({ repoRoot, userHome });
2291
+ const result = applyPluginComponentSelection(repoRoot, userHome, selection);
2292
+ res.json({ ok: true, plugin, ...result });
2293
+ } catch (error) {
2294
+ res.status(400).json({ error: error.message });
2295
+ }
2296
+ });
2297
+
2298
+ // Onglets marketplace du pop-up : liste les plugins-connecteurs d'un
2299
+ // marketplace Anthropic. `scope=legal` cible claude-for-legal (onglet
2300
+ // « Plugin legal Claude »), `scope=official` (défaut) claude-plugins-official
2301
+ // (onglet « Marketplace officiel »). Voir le commentaire de
2302
+ // listMarketplaceConnectors pour ce que la CLI expose (aucune recherche
2303
+ // distante).
2304
+ router.get('/plugin/marketplace', (req, res) => {
2305
+ try {
2306
+ const marketplace = marketplaceForScope(String(req.query?.scope || 'official'));
2307
+ res.json({ scope: req.query?.scope || 'official', marketplaceName: marketplace.name, ...listMarketplaceConnectors(captureCommand, { marketplaceName: marketplace.name }) });
2308
+ } catch (error) {
2309
+ res.status(500).json({ error: error.message });
2310
+ }
2311
+ });
2312
+
2313
+ // Enregistre (ou rafraîchit) le marketplace du scope quand le poste ne
2314
+ // l'a pas encore — bouton « Découvrir… » dédié dans chaque onglet.
2315
+ router.post('/plugin/marketplace/register', (req, res) => {
2316
+ try {
2317
+ const marketplace = marketplaceForScope(String(req.body?.scope || 'official'));
2318
+ const result = registerOfficialMarketplace(captureCommand, marketplace);
2319
+ res.json({ ok: result.ok, ...result });
2320
+ } catch (error) {
2321
+ res.status(400).json({ error: error.message });
2322
+ }
2323
+ });
2324
+
2325
+ // Installe/active/désactive les connecteurs cochés — `plugins` : ids
2326
+ // `nom@marketplace` tels que renvoyés par GET /plugin/marketplace ; `scope`
2327
+ // borne l'action au seul marketplace de l'onglet.
2328
+ router.post('/plugin/marketplace/install', (req, res) => {
2329
+ try {
2330
+ const selection = Array.isArray(req.body?.plugins) ? req.body.plugins : [];
2331
+ const marketplace = marketplaceForScope(String(req.body?.scope || 'official'));
2332
+ res.json({ ok: true, ...applyMarketplaceSelection(selection, captureCommand, { marketplaceName: marketplace.name }) });
2333
+ } catch (error) {
2334
+ res.status(400).json({ error: error.message });
2335
+ }
2336
+ });
2337
+
2338
+ router.get('/file', (req, res) => {
2339
+ try {
2340
+ res.json(readManagedFile(repoRoot, req.query.path, homeDir));
2341
+ } catch (error) {
2342
+ res.status(400).json({ error: error.message });
2343
+ }
2344
+ });
2345
+
2346
+ router.put('/file', (req, res) => {
2347
+ try {
2348
+ const relativePath = req.body?.path;
2349
+ const content = req.body?.content;
2350
+ const normalized = normalizeManagedPath(relativePath || '');
2351
+ const kind = managedFileKind(normalized);
2352
+ let effectivePath = relativePath;
2353
+ let previousPath = null;
2354
+ // Le champ `name:` du front matter sert de nom de dossier (skill) ou de
2355
+ // fichier (agent) : s'il change sur un fichier déjà présent, on renomme
2356
+ // sur le disque pour que Claude Code retrouve l'asset sous son nouveau
2357
+ // nom (un fichier pas encore créé garde le chemin de son slug initial).
2358
+ if (['skill', 'agent'].includes(kind) && typeof content === 'string') {
2359
+ const { absolute } = managedAbsolutePath(repoRoot, normalized, homeDir);
2360
+ if (fs.existsSync(absolute)) {
2361
+ const rename = renameManagedFile(repoRoot, homeDir, normalized, parseFrontMatter(content).name);
2362
+ if (rename.renamed) {
2363
+ effectivePath = rename.path;
2364
+ previousPath = rename.previous;
2365
+ }
2366
+ }
2367
+ }
2368
+ const saved = saveManagedFile(repoRoot, homeDir, effectivePath, content);
2369
+ // Un renommage laisse l'ancien lien Claude Code orphelin : on le retire
2370
+ // avant d'enregistrer le nouveau.
2371
+ if (previousPath) unregisterClaudeAsset(repoRoot, userHome, previousPath);
2372
+ res.json({
2373
+ ok: true,
2374
+ ...saved,
2375
+ renamedFrom: previousPath || undefined,
2376
+ claudeCode: registerClaudeAsset(repoRoot, userHome, saved.path),
2377
+ });
2378
+ } catch (error) {
2379
+ res.status(400).json({ error: error.message });
2380
+ }
2381
+ });
2382
+
2383
+ // Supprime un skill/agent du dépôt et retire son enregistrement Claude Code.
2384
+ // Le chemin arrive en query (`?path=`) ou dans le corps.
2385
+ router.delete('/file', (req, res) => {
2386
+ try {
2387
+ const relativePath = req.query?.path || req.body?.path;
2388
+ const deleted = deleteManagedFile(repoRoot, homeDir, relativePath);
2389
+ const claudeCode = unregisterClaudeAsset(repoRoot, userHome, deleted.path);
2390
+ res.json({ ok: true, ...deleted, claudeCode });
2391
+ } catch (error) {
2392
+ res.status(400).json({ error: error.message });
2393
+ }
2394
+ });
2395
+
2396
+ // Ajoute un fichier annexe (asset ou script) au dossier du skill sélectionné
2397
+ // et renvoie son nom, que l'éditeur insère comme lien Markdown relatif. Corps
2398
+ // brut (octet-stream) ; nom dans l'en-tête X-Filename (encodeURIComponent),
2399
+ // SKILL.md cible dans X-Skill-Path.
2400
+ router.post('/asset', express.raw({ type: 'application/octet-stream', limit: '100mb' }), (req, res) => {
2401
+ try {
2402
+ const skillPath = req.get('X-Skill-Path');
2403
+ const filename = decodeURIComponent(req.get('X-Filename') || '');
2404
+ const saved = saveManagedAsset(repoRoot, homeDir, skillPath, filename, req.body);
2405
+ res.status(201).json({ ok: true, ...saved });
2406
+ } catch (error) {
2407
+ res.status(400).json({ error: error.message });
2408
+ }
2409
+ });
2410
+
2411
+ // Supprime un unique fichier annexe d'un skill (pas le SKILL.md lui-même —
2412
+ // voir DELETE /file pour supprimer le skill entier). Chemin en query
2413
+ // (`?path=`) ou dans le corps.
2414
+ router.delete('/asset', (req, res) => {
2415
+ try {
2416
+ const relativePath = req.query?.path || req.body?.path;
2417
+ const deleted = deleteManagedAsset(repoRoot, homeDir, relativePath);
2418
+ res.json({ ok: true, ...deleted });
2419
+ } catch (error) {
2420
+ res.status(400).json({ error: error.message });
2421
+ }
2422
+ });
2423
+
2424
+ router.get('/dossiers', async (req, res) => {
2425
+ try {
2426
+ res.json({
2427
+ dossiers: await listDossiers(repoRoot, homeDir),
2428
+ tamponConfigured: fs.existsSync(path.join(homeDir, 'tampon.png')),
2429
+ });
2430
+ } catch (error) {
2431
+ res.status(500).json({ error: error.message });
2432
+ }
2433
+ });
2434
+
2435
+ router.get('/repository', async (req, res) => {
2436
+ const startedAt = performance.now();
2437
+ try {
2438
+ const config = registryConfig();
2439
+ const entries = listConfiguredCases(config);
2440
+ const overview = {
2441
+ name: 'PieceMaker',
2442
+ root: '',
2443
+ branch: 'Dossiers enregistrés',
2444
+ head: '',
2445
+ shortHead: '',
2446
+ folders: entries.map((entry) => ({
2447
+ name: entry.name,
2448
+ path: entry.id,
2449
+ location: entry.root,
2450
+ registered: entry.registered,
2451
+ })),
2452
+ changes: [],
2453
+ };
2454
+ finishAdminTiming(res, 'repository', startedAt, { folders: overview.folders.length });
2455
+ res.json(overview);
2456
+ } catch (error) {
2457
+ res.status(503).json({ error: error.message });
2458
+ }
2459
+ });
2460
+
2461
+ router.post('/repository/cases', async (req, res) => {
2462
+ try {
2463
+ const selected = await pickFolder(process.platform, userHome);
2464
+ if (!selected) return res.json({ ok: true, cancelled: true });
2465
+ const result = await registerLegalCase({
2466
+ folder: selected,
2467
+ configFile,
2468
+ homeDir,
2469
+ });
2470
+ res.status(201).json({ ok: true, ...result });
2471
+ } catch (error) {
2472
+ res.status(400).json({ error: error.message });
2473
+ }
2474
+ });
2475
+
2476
+ router.get('/repository/case', async (req, res) => {
2477
+ const startedAt = performance.now();
2478
+ try {
2479
+ const legalCase = selectedCase(req.query.case);
2480
+ const folder = await caseOverview(legalCase.casesRoot, homeDir, legalCase.caseName);
2481
+ folder.path = legalCase.id;
2482
+ folder.location = legalCase.root;
2483
+ folder.registered = legalCase.registered;
2484
+ // Les Markdown convertis vivent déjà dans l'historique ; ce cadre ne
2485
+ // présente que les pièces originales et un résumé non sensible du mapping.
2486
+ const mapping = readCaseMapping(legalCase.root);
2487
+ const [originals, branches] = await Promise.all([
2488
+ listOriginalsCached(legalCase.root),
2489
+ historyBranches(legalCase.casesRoot, homeDir, legalCase.caseName),
2490
+ ]);
2491
+ folder.originals = originals;
2492
+ folder.mapping = {
2493
+ exists: mapping.exists,
2494
+ name: path.basename(mapping.file),
2495
+ entries: Object.keys(mapping.mapping).length,
2496
+ };
2497
+ folder.branches = branches;
2498
+ finishAdminTiming(res, 'case', startedAt, {
2499
+ changes: folder.changes,
2500
+ originals: folder.originals.length,
2501
+ });
2502
+ res.json({ folder });
2503
+ } catch (error) {
2504
+ res.status(400).json({ error: error.message });
2505
+ }
2506
+ });
2507
+
2508
+ router.get('/repository/chronology', async (req, res) => {
2509
+ const startedAt = performance.now();
2510
+ try {
2511
+ const legalCase = selectedCase(req.query.case);
2512
+ const scope = normalizeChronologyScope(legalCase.root, req.query.scope);
2513
+ // Cette route est une vue cabinet et n'expose plus de branche publique
2514
+ // pseudonymisée. `deanonymize=0` est volontairement ignoré : les données
2515
+ // destinées au modèle empruntent les chemins internes dédiés.
2516
+ const chronology = await loadAdminChronology({ caseRoot: legalCase.root });
2517
+ const scopedChronology = scopeChronology(chronology, scope);
2518
+ scopedChronology.case = { path: legalCase.id, name: legalCase.caseName, location: legalCase.root };
2519
+ finishAdminTiming(res, 'chronology', startedAt, {
2520
+ documents: scopedChronology.stats.documents,
2521
+ entities: scopedChronology.stats.entities,
2522
+ });
2523
+ res.json(scopedChronology);
2524
+ } catch (error) {
2525
+ res.status(400).json({ error: error.message });
2526
+ }
2527
+ });
2528
+
2529
+ router.post('/repository/chronology/scope', async (req, res) => {
2530
+ try {
2531
+ const legalCase = selectedCase(req.body?.case);
2532
+ const selectedFolder = await pickFolder(process.platform, legalCase.root);
2533
+ if (!selectedFolder) return res.json({ ok: true, cancelled: true, scope: null });
2534
+ res.json({ ok: true, scope: chronologyScopeFromFolder(legalCase.root, selectedFolder) });
2535
+ } catch (error) {
2536
+ res.status(400).json({ error: error.message });
2537
+ }
2538
+ });
2539
+
2540
+ router.get('/repository/chronology/export', async (req, res) => {
2541
+ const startedAt = performance.now();
2542
+ try {
2543
+ const format = validateExportFormat(String(req.query.format || ''));
2544
+ const legalCase = selectedCase(req.query.case);
2545
+ const scope = normalizeChronologyScope(legalCase.root, req.query.scope);
2546
+ const chronology = await loadAdminChronology({ caseRoot: legalCase.root });
2547
+ const scopedChronology = scopeChronology(chronology, scope);
2548
+ const html = renderChronologyHtml(scopedChronology, { caseName: legalCase.caseName });
2549
+ finishAdminTiming(res, 'chronology-export', startedAt, {
2550
+ documents: scopedChronology.stats.documents,
2551
+ format,
2552
+ });
2553
+ await sendGeneratedDocument(res, {
2554
+ html,
2555
+ filename: `Chronologie — ${legalCase.caseName}`,
2556
+ format,
2557
+ });
2558
+ } catch (error) {
2559
+ // Une fois l'envoi du document entamé, les en-têtes sont partis : on ne
2560
+ // peut plus répondre en JSON, seulement couper la connexion.
2561
+ if (res.headersSent) return res.destroy();
2562
+ res.status(400).json({ error: error.message });
2563
+ }
2564
+ });
2565
+
2566
+ // Correction manuelle des métadonnées d'une pièce dans la chronologie —
2567
+ // nature (type), date, lieu (juridiction) et champs libres ajoutés/retirés par
2568
+ // le cabinet. Stocké avec sa provenance dans l'index documentaire que le
2569
+ // pipeline relit en préservant décisions et révisions : un re-scan rafraîchit
2570
+ // la détection sans écraser la correction.
2571
+ // `path` désigne la pièce ORIGINALE (comme le reste de la chronologie) et sert
2572
+ // seulement à calculer la clé de hachage — il n'est jamais lu. Envoyer une
2573
+ // correction entièrement vide efface l'entrée (retour aux valeurs détectées).
2574
+ router.put('/repository/document-meta', async (req, res) => {
2575
+ try {
2576
+ const legalCase = selectedCase(req.body?.case);
2577
+ const relativePath = String(req.body?.path || '').trim();
2578
+ if (!relativePath) throw new Error('Chemin de pièce manquant.');
2579
+ // Validation d'appartenance au dossier (même règle que le reste) ; on ne
2580
+ // lit pas la pièce, on refuse seulement un chemin hors racine.
2581
+ resolveCasePath(legalCase.root, relativePath);
2582
+ const body = req.body || {};
2583
+ const result = await applyDocumentMetaMutation({
2584
+ legalCase,
2585
+ relativePath,
2586
+ homeDir,
2587
+ envFile,
2588
+ correction: {
2589
+ nature: body.nature,
2590
+ dateIso: body.dateIso,
2591
+ localisation: body.localisation,
2592
+ fields: body.fields,
2593
+ reason: body.reason,
2594
+ ...(Object.hasOwn(body, 'entityDecisions')
2595
+ ? { entityDecisions: body.entityDecisions }
2596
+ : {}),
2597
+ },
2598
+ });
2599
+ res.json({
2600
+ ok: true,
2601
+ override: result.mutation.override,
2602
+ entityDecisions: result.mutation.entityDecisions,
2603
+ editRevision: result.mutation.editRevision,
2604
+ revisions: result.mutation.revisions,
2605
+ commit: result.history,
2606
+ });
2607
+ } catch (error) {
2608
+ res.status(400).json({ error: error.message });
2609
+ }
2610
+ });
2611
+
2612
+ // Décisions d'entité propres à une pièce. Contrairement à l'éditeur de
2613
+ // mapping global, cette route préserve l'override de métadonnées existant et
2614
+ // ne supprime ni ne renomme aucun code dans le reste du dossier.
2615
+ router.put('/repository/document-entities', async (req, res) => {
2616
+ try {
2617
+ const legalCase = selectedCase(req.body?.case);
2618
+ const relativePath = String(req.body?.path || '').trim();
2619
+ if (!relativePath) throw new Error('Chemin de pièce manquant.');
2620
+ resolveCasePath(legalCase.root, relativePath);
2621
+ const index = readDocumentIndex(legalCase.root);
2622
+ const key = stateKey(relativePath);
2623
+ const existingOverride = index.overrides?.[key] || {};
2624
+ const result = await applyDocumentMetaMutation({
2625
+ legalCase,
2626
+ relativePath,
2627
+ homeDir,
2628
+ envFile,
2629
+ correction: {
2630
+ ...existingOverride,
2631
+ reason: req.body?.reason,
2632
+ entityDecisions: req.body?.entityDecisions,
2633
+ },
2634
+ });
2635
+ res.json({
2636
+ ok: true,
2637
+ entityDecisions: result.mutation.entityDecisions,
2638
+ editRevision: result.mutation.editRevision,
2639
+ revisions: result.mutation.revisions,
2640
+ commit: result.history,
2641
+ });
2642
+ } catch (error) {
2643
+ res.status(400).json({ error: error.message });
2644
+ }
2645
+ });
2646
+
2647
+ // Contenu en lecture seule du Markdown converti d'une pièce — la seule
2648
+ // surface sûre : `path` est le chemin de la pièce ORIGINALE (comme partout
2649
+ // ailleurs dans la chronologie/l'aperçu), jamais lu directement. Sert
2650
+ // l'aperçu de la chronologie et le panneau droit de la correction d'entités ;
2651
+ // le fichier original (PDF/DOCX/scan) n'est jamais renvoyé par cette route.
2652
+ router.get('/repository/document', async (req, res) => {
2653
+ try {
2654
+ const legalCase = selectedCase(req.query.case);
2655
+ const mdPath = await convertedMarkdownRelativePath(legalCase.root, req.query.path);
2656
+ if (!mdPath) throw new Error('Cette pièce n’a pas encore de Markdown converti.');
2657
+ const absolute = resolveCasePath(legalCase.root, mdPath);
2658
+ const content = fs.readFileSync(absolute, 'utf8');
2659
+ res.json({
2660
+ path: mdPath,
2661
+ content: Buffer.byteLength(content, 'utf8') > MAX_MARKDOWN_BYTES
2662
+ ? `${content.slice(0, MAX_MARKDOWN_BYTES)}\n\n… (aperçu tronqué à 1 Mo)`
2663
+ : content,
2664
+ });
2665
+ } catch (error) {
2666
+ res.status(400).json({ error: error.message });
2667
+ }
2668
+ });
2669
+
2670
+ router.post('/branches', async (req, res) => {
2671
+ try {
2672
+ const legalCase = selectedCase(req.body?.case);
2673
+ const branches = await createHistoryBranch({
2674
+ casesRoot: legalCase.casesRoot,
2675
+ caseName: legalCase.caseName,
2676
+ homeDir,
2677
+ name: req.body?.name,
2678
+ });
2679
+ if (branches.skipped === 'busy') throw new Error('L’historique est occupé. Réessayez dans quelques secondes.');
2680
+ res.status(201).json({ ok: true, ...branches });
2681
+ } catch (error) {
2682
+ res.status(400).json({ error: error.message });
2683
+ }
2684
+ });
2685
+
2686
+ router.put('/branches/current', async (req, res) => {
2687
+ try {
2688
+ const legalCase = selectedCase(req.body?.case);
2689
+ const branches = await checkoutHistoryBranch({
2690
+ casesRoot: legalCase.casesRoot,
2691
+ caseName: legalCase.caseName,
2692
+ homeDir,
2693
+ name: req.body?.name,
2694
+ });
2695
+ if (branches.skipped === 'busy') throw new Error('L’historique est occupé. Réessayez dans quelques secondes.');
2696
+ res.json({ ok: true, ...branches });
2697
+ } catch (error) {
2698
+ res.status(400).json({ error: error.message });
2699
+ }
2700
+ });
2701
+
2702
+ // Ouvre le dossier juridique sélectionné (ou, à défaut, le dossier personnel)
2703
+ // dans le gestionnaire de fichiers ou le terminal du poste. Le chemin est
2704
+ // toujours résolu localement, jamais reçu tel quel du navigateur : un `path`
2705
+ // optionnel (target: 'files' seulement) désigne une pièce précise, relative à
2706
+ // la racine du dossier — `resolveCasePath` refuse toute sortie de racine.
2707
+ // Révéler dans le Finder n'affiche jamais de contenu dans le navigateur : sûr
2708
+ // même pour une pièce protégée.
2709
+ router.post('/reveal', async (req, res) => {
2710
+ try {
2711
+ const target = String(req.body?.target || '');
2712
+ if (!REVEAL_TARGETS.has(target)) throw new Error('Action de dossier inconnue.');
2713
+ const caseName = String(req.body?.case || '').trim();
2714
+ const relativePath = target === 'files' ? String(req.body?.path || '').trim() : '';
2715
+ let absolute;
2716
+ if (caseName && relativePath) {
2717
+ absolute = resolveCasePath(selectedCase(caseName).root, relativePath);
2718
+ } else {
2719
+ absolute = caseName ? selectedCase(caseName).root : userHome;
2720
+ }
2721
+ await revealLocalFolder(process.platform, target, absolute);
2722
+ res.json({ ok: true, target, path: absolute });
2723
+ } catch (error) {
2724
+ res.status(400).json({ error: error.message });
2725
+ }
2726
+ });
2727
+
2728
+ // ── Pièces originales : conversion Markdown et pipeline d'anonymisation ──
2729
+ // Les deux traitements sont longs (OCR, modèles NER) : la route rend la main
2730
+ // avec un identifiant de travail que l'administration interroge ensuite.
2731
+
2732
+ router.post('/originals/pipeline', async (req, res) => {
2733
+ try {
2734
+ const legalCase = selectedCase(req.body?.case);
2735
+ const job = await startOriginalsJob({
2736
+ casesRoot: legalCase.casesRoot,
2737
+ caseName: legalCase.caseName,
2738
+ homeDir,
2739
+ action: String(req.body?.action || ''),
2740
+ files: Array.isArray(req.body?.files) ? req.body.files : [],
2741
+ options: {
2742
+ // Sans `force`, un travail sans sélection ne refait que les pièces
2743
+ // dont le Markdown ou le scan PII manque.
2744
+ force: req.body?.force === true,
2745
+ engine: String(req.body?.engine || '').trim() || undefined,
2746
+ mode: String(req.body?.mode || '').trim() || undefined,
2747
+ lang: String(req.body?.lang || '').trim() || undefined,
2748
+ },
2749
+ });
2750
+ job.reference = legalCase.id;
2751
+ res.status(202).json({ ok: true, job });
2752
+ } catch (error) {
2753
+ res.status(400).json({ error: error.message });
2754
+ }
2755
+ });
2756
+
2757
+ router.get('/originals/job', (req, res) => {
2758
+ const job = getJob(req.query.id);
2759
+ if (!job) return res.status(404).json({ error: 'Travail inconnu ou expiré.' });
2760
+ res.json({ job });
2761
+ });
2762
+
2763
+ router.delete('/originals/job', (req, res) => {
2764
+ const job = cancelOriginalsJob(req.query.id);
2765
+ if (!job) return res.status(404).json({ error: 'Aucun traitement en cours pour cet identifiant.' });
2766
+ res.json({ ok: true, job });
2767
+ });
2768
+
2769
+ // ── Protection des pièces ────────────────────────────────────────────────
2770
+ // La protection est une propriété du fichier, pas de son emplacement : c'est
2771
+ // ici qu'on la décide, et les hooks Claude Code l'appliquent
2772
+ // (`piecemaker-plugin/scripts/protect-originals.mjs`). Tout est protégé par
2773
+ // défaut ; `protection.json` n'enregistre que les exceptions.
2774
+
2775
+ router.get('/protection', async (req, res) => {
2776
+ const startedAt = performance.now();
2777
+ try {
2778
+ const legalCase = selectedCase(req.query.case);
2779
+ const files = await listOriginalsCached(legalCase.root);
2780
+ finishAdminTiming(res, 'protection', startedAt, { files: files.length });
2781
+ res.json({
2782
+ case: legalCase.id,
2783
+ files,
2784
+ protectedCount: files.filter((file) => file.protected).length,
2785
+ resourceCount: files.filter((file) => file.resource).length,
2786
+ truncated: Boolean(files.truncated),
2787
+ });
2788
+ } catch (error) {
2789
+ res.status(400).json({ error: error.message });
2790
+ }
2791
+ });
2792
+
2793
+ router.put('/protection', (req, res) => {
2794
+ try {
2795
+ if (!Array.isArray(req.body?.unprotected)) {
2796
+ throw new Error('« unprotected » doit être la liste des pièces laissées accessibles à l’IA.');
2797
+ }
2798
+ if (req.body?.resources !== undefined && !Array.isArray(req.body.resources)) {
2799
+ throw new Error('« resources » doit être la liste des pièces marquées comme ressources.');
2800
+ }
2801
+ const legalCase = selectedCase(req.body?.case);
2802
+ const saved = writeProtection(legalCase.root, {
2803
+ unprotected: req.body.unprotected,
2804
+ resources: req.body.resources,
2805
+ });
2806
+ invalidateOriginals(legalCase.root);
2807
+ res.json({
2808
+ ok: true,
2809
+ case: legalCase.id,
2810
+ unprotected: [...saved.unprotected],
2811
+ resources: [...saved.resources],
2812
+ });
2813
+ } catch (error) {
2814
+ res.status(400).json({ error: error.message });
2815
+ }
2816
+ });
2817
+
2818
+ // Le mapping du dossier est le seul fichier de l'administration qui contient
2819
+ // des données personnelles en clair : il n'est jamais journalisé, seulement
2820
+ // rendu au navigateur local qui l'a demandé.
2821
+ router.get('/mapping', (req, res) => {
2822
+ try {
2823
+ const legalCase = selectedCase(req.query.case);
2824
+ const mapping = readCaseMapping(legalCase.root);
2825
+ res.json({
2826
+ case: legalCase.id,
2827
+ name: path.basename(mapping.file),
2828
+ exists: mapping.exists,
2829
+ mapping: mapping.mapping,
2830
+ reverse_mapping: mapping.reverse_mapping,
2831
+ informations_dossier: mapping.informations_dossier,
2832
+ });
2833
+ } catch (error) {
2834
+ res.status(400).json({ error: error.message });
2835
+ }
2836
+ });
2837
+
2838
+ router.put('/mapping', async (req, res) => {
2839
+ try {
2840
+ if (!req.body?.mapping || typeof req.body.mapping !== 'object' || Array.isArray(req.body.mapping)) {
2841
+ throw new Error('Le mapping doit être un objet { entité: code }.');
2842
+ }
2843
+ const legalCase = selectedCase(req.body?.case);
2844
+ const saved = saveCaseMapping(legalCase.root, {
2845
+ mapping: req.body.mapping,
2846
+ reverse_mapping: req.body.reverse_mapping,
2847
+ ...(req.body.informations_dossier !== undefined
2848
+ ? { informations_dossier: req.body.informations_dossier }
2849
+ : {}),
2850
+ });
2851
+ const commit = await createCommit({
2852
+ casesRoot: legalCase.casesRoot,
2853
+ caseName: legalCase.caseName,
2854
+ homeDir,
2855
+ label: 'Modification du mapping d’anonymisation',
2856
+ event: 'admin-mapping-edit',
2857
+ paths: [path.relative(legalCase.root, saved.file).split(path.sep).join('/')],
2858
+ waitForLockMs: 10_000,
2859
+ });
2860
+ if (commit.skipped === 'busy') throw new Error('Mapping enregistré, mais historique occupé : commit automatique non créé.');
2861
+ res.json({
2862
+ ok: true,
2863
+ case: legalCase.id,
2864
+ name: path.basename(saved.file),
2865
+ exists: true,
2866
+ mapping: saved.mapping,
2867
+ reverse_mapping: saved.reverse_mapping,
2868
+ informations_dossier: saved.informations_dossier,
2869
+ commit: { created: commit.created, hash: commit.commit || null },
2870
+ });
2871
+ } catch (error) {
2872
+ res.status(400).json({ error: error.message });
2873
+ }
2874
+ });
2875
+
2876
+ router.post('/mapping/rebuild', async (req, res) => {
2877
+ try {
2878
+ const legalCase = selectedCase(req.body?.case);
2879
+ const rebuilt = await rebuildCaseMapping(legalCase.root);
2880
+ const commit = await createCommit({
2881
+ casesRoot: legalCase.casesRoot,
2882
+ caseName: legalCase.caseName,
2883
+ homeDir,
2884
+ label: 'Régénération du mapping d’anonymisation',
2885
+ event: 'admin-mapping-rebuild',
2886
+ paths: [path.relative(legalCase.root, rebuilt.file).split(path.sep).join('/')],
2887
+ waitForLockMs: 10_000,
2888
+ });
2889
+ if (commit.skipped === 'busy') throw new Error('Mapping régénéré, mais historique occupé : commit automatique non créé.');
2890
+ res.json({
2891
+ ok: true,
2892
+ case: legalCase.id,
2893
+ name: path.basename(rebuilt.file),
2894
+ added: rebuilt.added,
2895
+ total: rebuilt.total,
2896
+ mapping: rebuilt.mapping,
2897
+ reverse_mapping: rebuilt.reverse_mapping,
2898
+ informations_dossier: rebuilt.informations_dossier,
2899
+ commit: { created: commit.created, hash: commit.commit || null },
2900
+ });
2901
+ } catch (error) {
2902
+ res.status(400).json({ error: error.message });
2903
+ }
2904
+ });
2905
+
2906
+ router.get('/history', async (req, res) => {
2907
+ const startedAt = performance.now();
2908
+ try {
2909
+ const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 250);
2910
+ const legalCase = selectedCase(req.query.case);
2911
+ const history = await listHistory(legalCase.casesRoot, homeDir, { limit, caseName: legalCase.caseName });
2912
+ finishAdminTiming(res, 'history', startedAt, { commits: history.length, limit });
2913
+ res.json({ history });
2914
+ } catch (error) {
2915
+ res.status(400).json({ error: error.message });
2916
+ }
2917
+ });
2918
+
2919
+ // Export « papier » (PDF/DOCX) de la feuille de temps d'un mois donné.
2920
+ router.get('/history/export', async (req, res) => {
2921
+ const startedAt = performance.now();
2922
+ try {
2923
+ const format = validateExportFormat(String(req.query.format || ''));
2924
+ const month = String(req.query.month || '');
2925
+ if (!HISTORY_MONTH_RE.test(month)) throw new Error('Mois invalide (attendu AAAA-MM).');
2926
+ const legalCase = selectedCase(req.query.case);
2927
+
2928
+ // Fenêtre du mois, bornes incluses. `--until` de git est INCLUSIF :
2929
+ // prendre le 1er du mois suivant compterait deux fois un commit tombant
2930
+ // exactement à minuit, à cheval entre les exports de deux mois
2931
+ // consécutifs. On borne donc au dernier instant du mois lui-même, le
2932
+ // dernier jour se déduisant du jour 0 du mois suivant.
2933
+ const [year, monthNum] = month.split('-').map(Number);
2934
+ const lastDay = new Date(Date.UTC(year, monthNum, 0)).getUTCDate();
2935
+ const since = `${month}-01`;
2936
+ const until = `${month}-${String(lastDay).padStart(2, '0')}T23:59:59`;
2937
+
2938
+ const entries = await listHistoryPeriod(legalCase.casesRoot, homeDir, {
2939
+ caseName: legalCase.caseName,
2940
+ since,
2941
+ until,
2942
+ });
2943
+ const html = renderHistoryHtml(entries, { caseName: legalCase.caseName, month });
2944
+ finishAdminTiming(res, 'history-export', startedAt, { commits: entries.length, format });
2945
+ await sendGeneratedDocument(res, {
2946
+ html,
2947
+ filename: `Feuille de temps ${month} — ${legalCase.caseName}`,
2948
+ format,
2949
+ });
2950
+ } catch (error) {
2951
+ if (res.headersSent) return res.destroy();
2952
+ res.status(400).json({ error: error.message });
2953
+ }
2954
+ });
2955
+
2956
+ // Mois disposant d'au moins un commit, du plus récent au plus ancien —
2957
+ // alimente le menu déroulant de l'export mensuel côté admin.
2958
+ router.get('/history/months', async (req, res) => {
2959
+ try {
2960
+ const legalCase = selectedCase(req.query.case);
2961
+ const months = await historyMonths(legalCase.casesRoot, homeDir, { caseName: legalCase.caseName });
2962
+ res.json({ months });
2963
+ } catch (error) {
2964
+ res.status(400).json({ error: error.message });
2965
+ }
2966
+ });
2967
+
2968
+ router.get('/revision', async (req, res) => {
2969
+ const startedAt = performance.now();
2970
+ try {
2971
+ const hash = String(req.query.hash || '');
2972
+ const filePath = String(req.query.path || '');
2973
+ const snapshot = String(req.query.snapshot || '');
2974
+ const legalCase = selectedCase(req.query.case);
2975
+ const revision = hash === 'WORKTREE'
2976
+ ? await worktreeDetails(legalCase.casesRoot, homeDir, legalCase.caseName, filePath, snapshot)
2977
+ : await revisionDetails(legalCase.casesRoot, homeDir, legalCase.caseName, hash, filePath);
2978
+ finishAdminTiming(res, 'revision', startedAt, {
2979
+ files: revision.filesCount,
2980
+ patchBytes: Buffer.byteLength(revision.patch || '', 'utf8'),
2981
+ truncated: revision.truncated,
2982
+ });
2983
+ res.json(revision);
2984
+ } catch (error) {
2985
+ res.status(400).json({ error: error.message });
2986
+ }
2987
+ });
2988
+
2989
+ router.post('/commits', async (req, res) => {
2990
+ try {
2991
+ const legalCase = selectedCase(req.body?.case);
2992
+ const label = String(req.body?.label || 'Commit manuel').trim().slice(0, 140);
2993
+ const description = String(req.body?.description || '').trim().slice(0, 4000);
2994
+ // Sélection de fichiers optionnelle : `paths` absent, vide ou non-tableau
2995
+ // laisse `createCommit` sans cette clé, donc le comportement inchangé
2996
+ // (commit du dossier entier). Quand une sélection est fournie, chaque
2997
+ // chemin est validé par `validateRelativeSafePath` (déjà appelée en
2998
+ // interne par `createCommit`/`buildSelectedTree`, commits.cjs) : toute
2999
+ // évasion du dossier ou fichier protégé y lève une erreur explicite,
3000
+ // remontée en 400 par le catch ci-dessous — pas de revalidation ici.
3001
+ const rawPaths = req.body?.paths;
3002
+ const paths = Array.isArray(rawPaths) && rawPaths.length ? rawPaths : null;
3003
+ const result = await createCommit({
3004
+ casesRoot: legalCase.casesRoot,
3005
+ caseName: legalCase.caseName,
3006
+ homeDir,
3007
+ label,
3008
+ description,
3009
+ event: 'manual',
3010
+ waitForLockMs: 10_000,
3011
+ ...(paths ? { paths } : {}),
3012
+ });
3013
+ if (result.skipped === 'busy') throw new Error('L’historique est occupé. Réessayez dans quelques secondes.');
3014
+ res.status(result.created ? 201 : 200).json({ ok: true, ...result });
3015
+ } catch (error) {
3016
+ res.status(400).json({ error: error.message });
3017
+ }
3018
+ });
3019
+
3020
+ router.post('/restore', async (req, res) => {
3021
+ try {
3022
+ if (req.body?.confirm !== true) {
3023
+ return res.status(400).json({ error: 'La confirmation explicite de la restauration est requise.' });
3024
+ }
3025
+ const legalCase = selectedCase(req.body?.case);
3026
+ const result = await restoreRevision({
3027
+ casesRoot: legalCase.casesRoot,
3028
+ caseName: legalCase.caseName,
3029
+ homeDir,
3030
+ hash: req.body?.hash,
3031
+ });
3032
+ res.json({ ok: true, ...result });
3033
+ } catch (error) {
3034
+ res.status(400).json({ error: error.message });
3035
+ }
3036
+ });
3037
+
3038
+ router.get('/telegram', (req, res) => {
3039
+ res.json(getTelegramState({ repoRoot, homeDir, userHome }));
3040
+ });
3041
+
3042
+ router.put('/telegram', (req, res) => {
3043
+ try {
3044
+ res.json({ ok: true, ...saveTelegramConfig({ repoRoot, homeDir, userHome }, req.body) });
3045
+ } catch (error) {
3046
+ res.status(400).json({ error: error.message });
3047
+ }
3048
+ });
3049
+
3050
+ router.post('/telegram/:role/:action', (req, res) => {
3051
+ try {
3052
+ res.json(controlTelegram({ repoRoot, homeDir, userHome }, req.params.role, req.params.action));
3053
+ } catch (error) {
3054
+ res.status(400).json({ error: error.message });
3055
+ }
3056
+ });
3057
+
3058
+ router.put('/telegram/dossiers/:id', (req, res) => {
3059
+ try {
3060
+ res.json({ ok: true, dossier: saveDossierBot({ repoRoot, homeDir, userHome }, req.params.id, req.body) });
3061
+ } catch (error) {
3062
+ res.status(400).json({ error: error.message });
3063
+ }
3064
+ });
3065
+
3066
+ router.post('/telegram/dossiers/:id/:action', (req, res) => {
3067
+ try {
3068
+ res.json(controlDossierBot({ repoRoot, homeDir, userHome }, req.params.id, req.params.action));
3069
+ } catch (error) {
3070
+ res.status(400).json({ error: error.message });
3071
+ }
3072
+ });
3073
+
3074
+ return router;
3075
+ }
3076
+
3077
+ module.exports = {
3078
+ loadAdminChronology,
3079
+ normalizeChronologyScope,
3080
+ chronologyScopeFromFolder,
3081
+ scopeChronology,
3082
+ applyDocumentMetaMutation,
3083
+ applyMarketplaceSelection,
3084
+ applyPluginComponentSelection,
3085
+ checkOllamaModelUpdate,
3086
+ configurationOverview,
3087
+ createLegalCase,
3088
+ createAdminRouter,
3089
+ createManagedFile,
3090
+ deleteManagedAsset,
3091
+ deleteManagedFile,
3092
+ ensureClaudePluginActive,
3093
+ folderPickerCommands,
3094
+ installedPluginSkills,
3095
+ reapplyDeletedOfficialSkills,
3096
+ isLocalOrigin,
3097
+ listDossiers,
3098
+ listManagedFiles,
3099
+ listMarketplaceConnectors,
3100
+ listPluginComponents,
3101
+ listRegisteredMarketplaces,
3102
+ listSkillAssets,
3103
+ managedFileKind,
3104
+ normalizeAgentModel,
3105
+ normalizeAgentTools,
3106
+ readManagedFile,
3107
+ registerLegalCase,
3108
+ registerOfficialMarketplace,
3109
+ renameManagedFile,
3110
+ revealCommands,
3111
+ saveManagedAsset,
3112
+ saveManagedFile,
3113
+ selectLocalFolder,
3114
+ updateEnvFile,
3115
+ validateAdminTheme,
3116
+ };