hammoc 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/README.md +422 -403
  2. package/bin/hammoc.js +0 -6
  3. package/package.json +100 -93
  4. package/packages/client/dist/assets/agentExampleHighlight-BgwTm15v.js +1 -0
  5. package/packages/client/dist/assets/commandTokenHighlight-BljHwnrK.js +1 -0
  6. package/packages/client/dist/assets/index-CjyjnXB8.css +32 -0
  7. package/packages/client/dist/assets/index-D3LxqW3f.js +2 -0
  8. package/packages/client/dist/assets/index-NqJdhlek.js +1498 -0
  9. package/packages/client/dist/assets/snippetTokenHighlight-DWsaQXX0.js +1 -0
  10. package/packages/client/dist/index.html +2 -2
  11. package/packages/client/dist/sw.js +1 -1
  12. package/packages/server/dist/app.d.ts.map +1 -1
  13. package/packages/server/dist/app.js +24 -24
  14. package/packages/server/dist/app.js.map +1 -1
  15. package/packages/server/dist/controllers/boardController.d.ts.map +1 -1
  16. package/packages/server/dist/controllers/boardController.js +0 -5
  17. package/packages/server/dist/controllers/boardController.js.map +1 -1
  18. package/packages/server/dist/controllers/claudeMdController.d.ts +26 -0
  19. package/packages/server/dist/controllers/claudeMdController.d.ts.map +1 -0
  20. package/packages/server/dist/controllers/claudeMdController.js +158 -0
  21. package/packages/server/dist/controllers/claudeMdController.js.map +1 -0
  22. package/packages/server/dist/controllers/fileSystemController.d.ts +4 -0
  23. package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
  24. package/packages/server/dist/controllers/fileSystemController.js +20 -2
  25. package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
  26. package/packages/server/dist/controllers/harnessAgentController.d.ts +28 -0
  27. package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -0
  28. package/packages/server/dist/controllers/harnessAgentController.js +339 -0
  29. package/packages/server/dist/controllers/harnessAgentController.js.map +1 -0
  30. package/packages/server/dist/controllers/harnessCommandController.d.ts +28 -0
  31. package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -0
  32. package/packages/server/dist/controllers/harnessCommandController.js +382 -0
  33. package/packages/server/dist/controllers/harnessCommandController.js.map +1 -0
  34. package/packages/server/dist/controllers/harnessController.d.ts +21 -0
  35. package/packages/server/dist/controllers/harnessController.d.ts.map +1 -0
  36. package/packages/server/dist/controllers/harnessController.js +176 -0
  37. package/packages/server/dist/controllers/harnessController.js.map +1 -0
  38. package/packages/server/dist/controllers/harnessHookController.d.ts +32 -0
  39. package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -0
  40. package/packages/server/dist/controllers/harnessHookController.js +363 -0
  41. package/packages/server/dist/controllers/harnessHookController.js.map +1 -0
  42. package/packages/server/dist/controllers/harnessLintController.d.ts +18 -0
  43. package/packages/server/dist/controllers/harnessLintController.d.ts.map +1 -0
  44. package/packages/server/dist/controllers/harnessLintController.js +72 -0
  45. package/packages/server/dist/controllers/harnessLintController.js.map +1 -0
  46. package/packages/server/dist/controllers/harnessMcpController.d.ts +28 -0
  47. package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -0
  48. package/packages/server/dist/controllers/harnessMcpController.js +310 -0
  49. package/packages/server/dist/controllers/harnessMcpController.js.map +1 -0
  50. package/packages/server/dist/controllers/harnessPluginController.d.ts +17 -0
  51. package/packages/server/dist/controllers/harnessPluginController.d.ts.map +1 -0
  52. package/packages/server/dist/controllers/harnessPluginController.js +115 -0
  53. package/packages/server/dist/controllers/harnessPluginController.js.map +1 -0
  54. package/packages/server/dist/controllers/harnessShareScopeController.d.ts +15 -0
  55. package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -0
  56. package/packages/server/dist/controllers/harnessShareScopeController.js +73 -0
  57. package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -0
  58. package/packages/server/dist/controllers/harnessSkillController.d.ts +32 -0
  59. package/packages/server/dist/controllers/harnessSkillController.d.ts.map +1 -0
  60. package/packages/server/dist/controllers/harnessSkillController.js +453 -0
  61. package/packages/server/dist/controllers/harnessSkillController.js.map +1 -0
  62. package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
  63. package/packages/server/dist/controllers/projectController.js +11 -0
  64. package/packages/server/dist/controllers/projectController.js.map +1 -1
  65. package/packages/server/dist/controllers/serverController.d.ts.map +1 -1
  66. package/packages/server/dist/controllers/serverController.js +84 -49
  67. package/packages/server/dist/controllers/serverController.js.map +1 -1
  68. package/packages/server/dist/controllers/snippetController.d.ts +35 -0
  69. package/packages/server/dist/controllers/snippetController.d.ts.map +1 -0
  70. package/packages/server/dist/controllers/snippetController.js +294 -0
  71. package/packages/server/dist/controllers/snippetController.js.map +1 -0
  72. package/packages/server/dist/handlers/websocket.d.ts +16 -0
  73. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  74. package/packages/server/dist/handlers/websocket.js +221 -8
  75. package/packages/server/dist/handlers/websocket.js.map +1 -1
  76. package/packages/server/dist/index.js +66 -0
  77. package/packages/server/dist/index.js.map +1 -1
  78. package/packages/server/dist/locales/en/server.json +41 -6
  79. package/packages/server/dist/locales/es/server.json +3 -5
  80. package/packages/server/dist/locales/ja/server.json +3 -5
  81. package/packages/server/dist/locales/ko/server.json +4 -6
  82. package/packages/server/dist/locales/pt/server.json +3 -5
  83. package/packages/server/dist/locales/zh-CN/server.json +3 -5
  84. package/packages/server/dist/routes/account.d.ts +7 -0
  85. package/packages/server/dist/routes/account.d.ts.map +1 -0
  86. package/packages/server/dist/routes/account.js +35 -0
  87. package/packages/server/dist/routes/account.js.map +1 -0
  88. package/packages/server/dist/routes/debug.d.ts +1 -1
  89. package/packages/server/dist/routes/debug.d.ts.map +1 -1
  90. package/packages/server/dist/routes/debug.js +60 -1
  91. package/packages/server/dist/routes/debug.js.map +1 -1
  92. package/packages/server/dist/routes/harness.d.ts +8 -0
  93. package/packages/server/dist/routes/harness.d.ts.map +1 -0
  94. package/packages/server/dist/routes/harness.js +92 -0
  95. package/packages/server/dist/routes/harness.js.map +1 -0
  96. package/packages/server/dist/routes/preferences.d.ts.map +1 -1
  97. package/packages/server/dist/routes/preferences.js +11 -2
  98. package/packages/server/dist/routes/preferences.js.map +1 -1
  99. package/packages/server/dist/routes/projects.d.ts.map +1 -1
  100. package/packages/server/dist/routes/projects.js +5 -60
  101. package/packages/server/dist/routes/projects.js.map +1 -1
  102. package/packages/server/dist/routes/snippets.d.ts +14 -0
  103. package/packages/server/dist/routes/snippets.d.ts.map +1 -0
  104. package/packages/server/dist/routes/snippets.js +27 -0
  105. package/packages/server/dist/routes/snippets.js.map +1 -0
  106. package/packages/server/dist/services/accountInfoService.d.ts +38 -0
  107. package/packages/server/dist/services/accountInfoService.d.ts.map +1 -0
  108. package/packages/server/dist/services/accountInfoService.js +118 -0
  109. package/packages/server/dist/services/accountInfoService.js.map +1 -0
  110. package/packages/server/dist/services/bmadStatusService.d.ts +6 -2
  111. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  112. package/packages/server/dist/services/bmadStatusService.js +88 -32
  113. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  114. package/packages/server/dist/services/chatService.d.ts +3 -0
  115. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  116. package/packages/server/dist/services/chatService.js +36 -8
  117. package/packages/server/dist/services/chatService.js.map +1 -1
  118. package/packages/server/dist/services/claudeMdService.d.ts +48 -0
  119. package/packages/server/dist/services/claudeMdService.d.ts.map +1 -0
  120. package/packages/server/dist/services/claudeMdService.js +240 -0
  121. package/packages/server/dist/services/claudeMdService.js.map +1 -0
  122. package/packages/server/dist/services/commandService.d.ts +10 -0
  123. package/packages/server/dist/services/commandService.d.ts.map +1 -1
  124. package/packages/server/dist/services/commandService.js +129 -4
  125. package/packages/server/dist/services/commandService.js.map +1 -1
  126. package/packages/server/dist/services/fileSystemService.d.ts +7 -1
  127. package/packages/server/dist/services/fileSystemService.d.ts.map +1 -1
  128. package/packages/server/dist/services/fileSystemService.js +67 -8
  129. package/packages/server/dist/services/fileSystemService.js.map +1 -1
  130. package/packages/server/dist/services/fileWatcherService.d.ts +59 -0
  131. package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -0
  132. package/packages/server/dist/services/fileWatcherService.js +329 -0
  133. package/packages/server/dist/services/fileWatcherService.js.map +1 -0
  134. package/packages/server/dist/services/gitService.d.ts.map +1 -1
  135. package/packages/server/dist/services/gitService.js +67 -7
  136. package/packages/server/dist/services/gitService.js.map +1 -1
  137. package/packages/server/dist/services/harnessAgentService.d.ts +79 -0
  138. package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -0
  139. package/packages/server/dist/services/harnessAgentService.js +933 -0
  140. package/packages/server/dist/services/harnessAgentService.js.map +1 -0
  141. package/packages/server/dist/services/harnessCommandService.d.ts +60 -0
  142. package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -0
  143. package/packages/server/dist/services/harnessCommandService.js +853 -0
  144. package/packages/server/dist/services/harnessCommandService.js.map +1 -0
  145. package/packages/server/dist/services/harnessHookService.d.ts +55 -0
  146. package/packages/server/dist/services/harnessHookService.d.ts.map +1 -0
  147. package/packages/server/dist/services/harnessHookService.js +1060 -0
  148. package/packages/server/dist/services/harnessHookService.js.map +1 -0
  149. package/packages/server/dist/services/harnessLintService.d.ts +49 -0
  150. package/packages/server/dist/services/harnessLintService.d.ts.map +1 -0
  151. package/packages/server/dist/services/harnessLintService.js +628 -0
  152. package/packages/server/dist/services/harnessLintService.js.map +1 -0
  153. package/packages/server/dist/services/harnessMcpService.d.ts +77 -0
  154. package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -0
  155. package/packages/server/dist/services/harnessMcpService.js +814 -0
  156. package/packages/server/dist/services/harnessMcpService.js.map +1 -0
  157. package/packages/server/dist/services/harnessPluginService.d.ts +66 -0
  158. package/packages/server/dist/services/harnessPluginService.d.ts.map +1 -0
  159. package/packages/server/dist/services/harnessPluginService.js +559 -0
  160. package/packages/server/dist/services/harnessPluginService.js.map +1 -0
  161. package/packages/server/dist/services/harnessService.d.ts +40 -0
  162. package/packages/server/dist/services/harnessService.d.ts.map +1 -0
  163. package/packages/server/dist/services/harnessService.js +222 -0
  164. package/packages/server/dist/services/harnessService.js.map +1 -0
  165. package/packages/server/dist/services/harnessShareScopeService.d.ts +31 -0
  166. package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -0
  167. package/packages/server/dist/services/harnessShareScopeService.js +93 -0
  168. package/packages/server/dist/services/harnessShareScopeService.js.map +1 -0
  169. package/packages/server/dist/services/harnessSkillService.d.ts +70 -0
  170. package/packages/server/dist/services/harnessSkillService.d.ts.map +1 -0
  171. package/packages/server/dist/services/harnessSkillService.js +636 -0
  172. package/packages/server/dist/services/harnessSkillService.js.map +1 -0
  173. package/packages/server/dist/services/historyParser.d.ts +4 -14
  174. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  175. package/packages/server/dist/services/historyParser.js +60 -5
  176. package/packages/server/dist/services/historyParser.js.map +1 -1
  177. package/packages/server/dist/services/issueService.d.ts.map +1 -1
  178. package/packages/server/dist/services/issueService.js +10 -2
  179. package/packages/server/dist/services/issueService.js.map +1 -1
  180. package/packages/server/dist/services/manualSyncService.d.ts +19 -0
  181. package/packages/server/dist/services/manualSyncService.d.ts.map +1 -0
  182. package/packages/server/dist/services/manualSyncService.js +110 -0
  183. package/packages/server/dist/services/manualSyncService.js.map +1 -0
  184. package/packages/server/dist/services/notificationService.d.ts.map +1 -1
  185. package/packages/server/dist/services/notificationService.js +34 -9
  186. package/packages/server/dist/services/notificationService.js.map +1 -1
  187. package/packages/server/dist/services/preferencesService.d.ts.map +1 -1
  188. package/packages/server/dist/services/preferencesService.js +8 -1
  189. package/packages/server/dist/services/preferencesService.js.map +1 -1
  190. package/packages/server/dist/services/projectService.d.ts +5 -0
  191. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  192. package/packages/server/dist/services/projectService.js +42 -2
  193. package/packages/server/dist/services/projectService.js.map +1 -1
  194. package/packages/server/dist/services/ptyService.d.ts +1 -0
  195. package/packages/server/dist/services/ptyService.d.ts.map +1 -1
  196. package/packages/server/dist/services/ptyService.js +36 -5
  197. package/packages/server/dist/services/ptyService.js.map +1 -1
  198. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  199. package/packages/server/dist/services/queueService.js +83 -14
  200. package/packages/server/dist/services/queueService.js.map +1 -1
  201. package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
  202. package/packages/server/dist/services/sessionBufferManager.js +26 -0
  203. package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
  204. package/packages/server/dist/services/sessionService.d.ts +4 -3
  205. package/packages/server/dist/services/sessionService.d.ts.map +1 -1
  206. package/packages/server/dist/services/sessionService.js +5 -4
  207. package/packages/server/dist/services/sessionService.js.map +1 -1
  208. package/packages/server/dist/services/snippetService.d.ts +54 -0
  209. package/packages/server/dist/services/snippetService.d.ts.map +1 -0
  210. package/packages/server/dist/services/snippetService.js +371 -0
  211. package/packages/server/dist/services/snippetService.js.map +1 -0
  212. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts +46 -0
  213. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts.map +1 -0
  214. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js +125 -0
  215. package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js.map +1 -0
  216. package/packages/server/dist/services/webPushService.d.ts.map +1 -1
  217. package/packages/server/dist/services/webPushService.js +8 -1
  218. package/packages/server/dist/services/webPushService.js.map +1 -1
  219. package/packages/server/dist/snippets/split-commit +9 -0
  220. package/packages/server/dist/utils/applySecretsPolicy.d.ts +53 -0
  221. package/packages/server/dist/utils/applySecretsPolicy.d.ts.map +1 -0
  222. package/packages/server/dist/utils/applySecretsPolicy.js +204 -0
  223. package/packages/server/dist/utils/applySecretsPolicy.js.map +1 -0
  224. package/packages/server/dist/utils/assertNoSecretOnShared.d.ts +40 -0
  225. package/packages/server/dist/utils/assertNoSecretOnShared.d.ts.map +1 -0
  226. package/packages/server/dist/utils/assertNoSecretOnShared.js +47 -0
  227. package/packages/server/dist/utils/assertNoSecretOnShared.js.map +1 -0
  228. package/packages/server/dist/utils/effortUtils.d.ts +21 -0
  229. package/packages/server/dist/utils/effortUtils.d.ts.map +1 -0
  230. package/packages/server/dist/utils/effortUtils.js +36 -0
  231. package/packages/server/dist/utils/effortUtils.js.map +1 -0
  232. package/packages/server/dist/utils/gitignoreFilter.d.ts +23 -0
  233. package/packages/server/dist/utils/gitignoreFilter.d.ts.map +1 -0
  234. package/packages/server/dist/utils/gitignoreFilter.js +42 -0
  235. package/packages/server/dist/utils/gitignoreFilter.js.map +1 -0
  236. package/packages/server/dist/utils/harnessBundleSchema.d.ts +105 -0
  237. package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -0
  238. package/packages/server/dist/utils/harnessBundleSchema.js +79 -0
  239. package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -0
  240. package/packages/server/dist/utils/harnessPaths.d.ts +34 -0
  241. package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -0
  242. package/packages/server/dist/utils/harnessPaths.js +124 -0
  243. package/packages/server/dist/utils/harnessPaths.js.map +1 -0
  244. package/packages/server/dist/utils/pathUtils.d.ts +3 -2
  245. package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
  246. package/packages/server/dist/utils/pathUtils.js +26 -2
  247. package/packages/server/dist/utils/pathUtils.js.map +1 -1
  248. package/packages/server/dist/utils/secretHeuristic.d.ts +72 -0
  249. package/packages/server/dist/utils/secretHeuristic.d.ts.map +1 -0
  250. package/packages/server/dist/utils/secretHeuristic.js +163 -0
  251. package/packages/server/dist/utils/secretHeuristic.js.map +1 -0
  252. package/packages/server/dist/utils/secretPlaceholderNamer.d.ts +41 -0
  253. package/packages/server/dist/utils/secretPlaceholderNamer.d.ts.map +1 -0
  254. package/packages/server/dist/utils/secretPlaceholderNamer.js +81 -0
  255. package/packages/server/dist/utils/secretPlaceholderNamer.js.map +1 -0
  256. package/packages/server/dist/utils/serverPathResolver.d.ts +29 -0
  257. package/packages/server/dist/utils/serverPathResolver.d.ts.map +1 -0
  258. package/packages/server/dist/utils/serverPathResolver.js +59 -0
  259. package/packages/server/dist/utils/serverPathResolver.js.map +1 -0
  260. package/packages/server/dist/utils/snippetPaths.d.ts +61 -0
  261. package/packages/server/dist/utils/snippetPaths.d.ts.map +1 -0
  262. package/packages/server/dist/utils/snippetPaths.js +123 -0
  263. package/packages/server/dist/utils/snippetPaths.js.map +1 -0
  264. package/packages/server/dist/utils/structuredEditor.d.ts +34 -0
  265. package/packages/server/dist/utils/structuredEditor.d.ts.map +1 -0
  266. package/packages/server/dist/utils/structuredEditor.js +111 -0
  267. package/packages/server/dist/utils/structuredEditor.js.map +1 -0
  268. package/packages/server/package.json +6 -2
  269. package/packages/server/resources/internals/INDEX.md +23 -0
  270. package/packages/server/resources/internals/harness-files.md +63 -0
  271. package/packages/server/resources/internals/image-storage.md +43 -0
  272. package/packages/server/resources/manual/01-getting-started.md +104 -0
  273. package/packages/server/resources/manual/02-chat.md +285 -0
  274. package/packages/server/resources/manual/03-sessions.md +48 -0
  275. package/packages/server/resources/manual/04-slash-commands-favorites.md +152 -0
  276. package/packages/server/resources/manual/05-projects.md +74 -0
  277. package/packages/server/resources/manual/06-file-explorer-editor.md +90 -0
  278. package/packages/server/resources/manual/07-git.md +94 -0
  279. package/packages/server/resources/manual/08-terminal.md +59 -0
  280. package/packages/server/resources/manual/09-queue-runner.md +262 -0
  281. package/packages/server/resources/manual/10-project-board.md +193 -0
  282. package/packages/server/resources/manual/11-bmad-method-integration.md +128 -0
  283. package/packages/server/resources/manual/12-harness-workbench.md +175 -0
  284. package/packages/server/resources/manual/13-settings.md +241 -0
  285. package/packages/server/resources/manual/14-keyboard-shortcuts.md +68 -0
  286. package/packages/server/resources/manual/15-environment-variables.md +28 -0
  287. package/packages/server/resources/manual/16-troubleshooting.md +110 -0
  288. package/packages/server/resources/manual/INDEX.md +60 -0
  289. package/packages/shared/dist/index.d.ts +3 -0
  290. package/packages/shared/dist/index.d.ts.map +1 -1
  291. package/packages/shared/dist/index.js +6 -0
  292. package/packages/shared/dist/index.js.map +1 -1
  293. package/packages/shared/dist/types/command.d.ts +3 -3
  294. package/packages/shared/dist/types/command.d.ts.map +1 -1
  295. package/packages/shared/dist/types/fileSystem.d.ts +19 -0
  296. package/packages/shared/dist/types/fileSystem.d.ts.map +1 -1
  297. package/packages/shared/dist/types/fileSystem.js +5 -0
  298. package/packages/shared/dist/types/fileSystem.js.map +1 -1
  299. package/packages/shared/dist/types/git.d.ts +6 -1
  300. package/packages/shared/dist/types/git.d.ts.map +1 -1
  301. package/packages/shared/dist/types/git.js.map +1 -1
  302. package/packages/shared/dist/types/harness.d.ts +1211 -0
  303. package/packages/shared/dist/types/harness.d.ts.map +1 -0
  304. package/packages/shared/dist/types/harness.js +107 -0
  305. package/packages/shared/dist/types/harness.js.map +1 -0
  306. package/packages/shared/dist/types/harnessBundle.d.ts +170 -0
  307. package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -0
  308. package/packages/shared/dist/types/harnessBundle.js +18 -0
  309. package/packages/shared/dist/types/harnessBundle.js.map +1 -0
  310. package/packages/shared/dist/types/history.d.ts +7 -0
  311. package/packages/shared/dist/types/history.d.ts.map +1 -1
  312. package/packages/shared/dist/types/preferences.d.ts +4 -1
  313. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  314. package/packages/shared/dist/types/preferences.js +1 -0
  315. package/packages/shared/dist/types/preferences.js.map +1 -1
  316. package/packages/shared/dist/types/queue.d.ts +9 -0
  317. package/packages/shared/dist/types/queue.d.ts.map +1 -1
  318. package/packages/shared/dist/types/sdk.d.ts +42 -1
  319. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  320. package/packages/shared/dist/types/sdk.js +26 -2
  321. package/packages/shared/dist/types/sdk.js.map +1 -1
  322. package/packages/shared/dist/types/websocket.d.ts +24 -0
  323. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  324. package/packages/shared/dist/utils/markdownSections.d.ts +50 -0
  325. package/packages/shared/dist/utils/markdownSections.d.ts.map +1 -0
  326. package/packages/shared/dist/utils/markdownSections.js +111 -0
  327. package/packages/shared/dist/utils/markdownSections.js.map +1 -0
  328. package/packages/shared/dist/utils/queueParser.d.ts.map +1 -1
  329. package/packages/shared/dist/utils/queueParser.js +104 -0
  330. package/packages/shared/dist/utils/queueParser.js.map +1 -1
  331. package/scripts/build-manual-shards.mjs +100 -0
  332. package/scripts/mock-telegram.mjs +172 -0
  333. package/scripts/run-integration-test.mjs +362 -0
  334. package/packages/client/dist/assets/index-Bf0D9oVJ.css +0 -32
  335. package/packages/client/dist/assets/index-CRmzoqHy.js +0 -2
  336. package/packages/client/dist/assets/index-CszGQ29O.js +0 -1432
@@ -0,0 +1,853 @@
1
+ /**
2
+ * Story 28.5: Harness slash-command service.
3
+ *
4
+ * Combines three sources of `.claude/commands/**\/*.md` files into a single tree:
5
+ * - <projectRoot>/.claude/commands/ (project scope)
6
+ * - ~/.claude/commands/ (user scope)
7
+ * - <pluginInstallPath>/commands/ (plugin scope, read-only)
8
+ *
9
+ * Each leaf .md file becomes one card. Frontmatter is YAML (round-trip via
10
+ * `yaml`(eemeli) — see `applyYamlFrontmatterPatch`); body is plain markdown.
11
+ * Path enumeration runs in parallel both across scopes and across files within
12
+ * each scope (Risk #4 mitigation — Promise.all, no serial for-await).
13
+ */
14
+ import path from 'path';
15
+ import fs from 'fs/promises';
16
+ import yaml from 'yaml';
17
+ import { HARNESS_ERRORS, } from '@hammoc/shared';
18
+ import { harnessService } from './harnessService.js';
19
+ import { projectService } from './projectService.js';
20
+ import { getUserHarnessRoot } from '../utils/harnessPaths.js';
21
+ import { detectSecretsInText as detectSecretsInTextCanonical } from '../utils/secretHeuristic.js';
22
+ import { assertNoSecretOnShared } from '../utils/assertNoSecretOnShared.js';
23
+ import { applyYamlFrontmatterPatch, splitFrontmatterAndBody, } from './utils/applyYamlFrontmatterPatch.js';
24
+ const SCOPE_PRIORITY = {
25
+ project: 0,
26
+ user: 1,
27
+ plugin: 2,
28
+ };
29
+ const ALLOWED_MODELS = new Set([
30
+ 'inherit',
31
+ 'sonnet',
32
+ 'opus',
33
+ 'haiku',
34
+ ]);
35
+ const COMMANDS_DIR = 'commands';
36
+ const MAX_WALK_DEPTH = 32;
37
+ // Story 30.1 (Task 1.2): SECRET_PATTERNS / ENV_REF_RE moved to
38
+ // `utils/secretHeuristic.ts`. The wrappers below adapt the canonical entry
39
+ // point to this service's existing `{ matched, lines }` shape.
40
+ const POSITIONAL_ARG_RE = /\$([1-9]\d*)\b/;
41
+ const ARGUMENTS_ALL_RE = /\$ARGUMENTS\b/;
42
+ const FILE_REF_RE = /(?:^|\s)@([\w./-]+)/;
43
+ const BASH_EXEC_RE = /!`[^`]+`/;
44
+ const PLUGIN_ROOT_RE = /\$\{CLAUDE_PLUGIN_ROOT\}/;
45
+ const BMAD_MARKER_RE = /<!--\s*Powered\s+by\s+BMAD™?\s+Core\s*-->/i;
46
+ // eslint-disable-next-line no-control-regex -- OS-reserved control chars are intentional here
47
+ const RESERVED_CHARS_RE = /[\\<>:"|?*\x00-\x1F]/;
48
+ const TRAILING_DOT_OR_SPACE_RE = /[. ]$/;
49
+ function throwMapped(code, message, extras) {
50
+ const err = new Error(message);
51
+ err.code = code;
52
+ if (extras)
53
+ Object.assign(err, extras);
54
+ throw err;
55
+ }
56
+ function isFileNotFound(err) {
57
+ const code = err?.code;
58
+ return code === 'ENOENT' || code === HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code;
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Path / slash-name conversion
62
+ // ---------------------------------------------------------------------------
63
+ /** `.md` relative path under commands root → `/A:B:foo` slash name. */
64
+ function deriveSlashName(relPathPosix) {
65
+ const noExt = relPathPosix.replace(/\.md$/i, '');
66
+ return `/${noExt.replace(/\//g, ':')}`;
67
+ }
68
+ function toPosixRelative(rel) {
69
+ return rel.replace(/\\/g, '/');
70
+ }
71
+ /**
72
+ * Validate a relative path under the commands root: forward slashes allowed,
73
+ * OS reserved chars / trailing space-or-dot / `..` traversal rejected,
74
+ * `.md` extension required.
75
+ */
76
+ function validateRelativePath(relPosix) {
77
+ if (!relPosix.endsWith('.md')) {
78
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'relativePath must end in .md');
79
+ }
80
+ if (relPosix.includes('..')) {
81
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'path traversal denied');
82
+ }
83
+ for (const segment of relPosix.split('/')) {
84
+ if (segment.length === 0) {
85
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'empty path segment not allowed');
86
+ }
87
+ if (RESERVED_CHARS_RE.test(segment)) {
88
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'OS-reserved characters not allowed in path segments');
89
+ }
90
+ if (TRAILING_DOT_OR_SPACE_RE.test(segment)) {
91
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'path segments cannot end with space or dot');
92
+ }
93
+ }
94
+ }
95
+ // ---------------------------------------------------------------------------
96
+ // Token / secret / parse helpers
97
+ // ---------------------------------------------------------------------------
98
+ function analyzeTokens(body) {
99
+ return {
100
+ usesPositionalArgs: POSITIONAL_ARG_RE.test(body),
101
+ usesArgumentsAll: ARGUMENTS_ALL_RE.test(body),
102
+ usesFileRefs: FILE_REF_RE.test(body),
103
+ usesBashExec: BASH_EXEC_RE.test(body),
104
+ usesPluginRoot: PLUGIN_ROOT_RE.test(body),
105
+ };
106
+ }
107
+ /** Detect the BMad mirror marker within the first 10 lines of the body. */
108
+ function detectBmadMirror(body) {
109
+ const head = body.split(/\r?\n/).slice(0, 10).join('\n');
110
+ return BMAD_MARKER_RE.test(head);
111
+ }
112
+ function detectSecretsInText(text) {
113
+ const { matched, lines } = detectSecretsInTextCanonical(text);
114
+ return { matched, lines };
115
+ }
116
+ function detectSecretsInRaw(raw) {
117
+ return detectSecretsInText(raw);
118
+ }
119
+ function parseFrontmatterYaml(raw) {
120
+ if (raw === null || raw.trim().length === 0)
121
+ return {};
122
+ let parsed;
123
+ try {
124
+ parsed = yaml.parse(raw);
125
+ }
126
+ catch (cause) {
127
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `failed to parse frontmatter: ${cause.message}`);
128
+ }
129
+ if (parsed === null || parsed === undefined)
130
+ return {};
131
+ if (typeof parsed !== 'object' || Array.isArray(parsed)) {
132
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'frontmatter must be a YAML mapping');
133
+ }
134
+ const obj = parsed;
135
+ const out = {};
136
+ if (typeof obj.description === 'string')
137
+ out.description = obj.description;
138
+ if (typeof obj['argument-hint'] === 'string')
139
+ out['argument-hint'] = obj['argument-hint'];
140
+ if (typeof obj['allowed-tools'] === 'string')
141
+ out['allowed-tools'] = obj['allowed-tools'];
142
+ if (typeof obj.model === 'string' && ALLOWED_MODELS.has(obj.model)) {
143
+ out.model = obj.model;
144
+ }
145
+ return out;
146
+ }
147
+ function frontmatterToPatchObject(fm) {
148
+ return {
149
+ description: fm.description,
150
+ 'argument-hint': fm['argument-hint'],
151
+ 'allowed-tools': fm['allowed-tools'],
152
+ model: fm.model,
153
+ };
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Roots / containment
157
+ // ---------------------------------------------------------------------------
158
+ function userCommandsRoot() {
159
+ return path.join(getUserHarnessRoot(), COMMANDS_DIR);
160
+ }
161
+ async function projectCommandsRoot(projectSlug) {
162
+ const projectRoot = await projectService.resolveOriginalPath(projectSlug);
163
+ return path.join(projectRoot, '.claude', COMMANDS_DIR);
164
+ }
165
+ function pluginCommandsRoots(installPath) {
166
+ return [path.join(installPath, COMMANDS_DIR)];
167
+ }
168
+ function withinRoot(absolute, root) {
169
+ const resolved = path.resolve(absolute);
170
+ const resolvedRoot = path.resolve(root);
171
+ return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
172
+ }
173
+ async function readInstalledPlugins() {
174
+ try {
175
+ const res = await harnessService.read({
176
+ scope: 'user',
177
+ relativePath: 'plugins/installed_plugins.json',
178
+ });
179
+ const trimmed = (res.content ?? '').trim();
180
+ if (!trimmed)
181
+ return {};
182
+ try {
183
+ return JSON.parse(trimmed);
184
+ }
185
+ catch {
186
+ return {};
187
+ }
188
+ }
189
+ catch (err) {
190
+ if (isFileNotFound(err))
191
+ return {};
192
+ throw err;
193
+ }
194
+ }
195
+ async function walkMdFiles(root, depth = 0) {
196
+ if (depth > MAX_WALK_DEPTH)
197
+ return [];
198
+ let entries;
199
+ try {
200
+ entries = await fs.readdir(root, { withFileTypes: true });
201
+ }
202
+ catch {
203
+ return [];
204
+ }
205
+ const promises = entries.map(async (entry) => {
206
+ const abs = path.join(root, entry.name);
207
+ if (entry.isDirectory()) {
208
+ return walkMdFiles(abs, depth + 1);
209
+ }
210
+ if (entry.isFile() && entry.name.endsWith('.md')) {
211
+ return [abs];
212
+ }
213
+ return [];
214
+ });
215
+ const nested = await Promise.all(promises);
216
+ return nested.flat();
217
+ }
218
+ async function readMdFile(absolute) {
219
+ let stat;
220
+ try {
221
+ stat = await fs.stat(absolute);
222
+ }
223
+ catch (err) {
224
+ if (err.code === 'ENOENT') {
225
+ return { malformed: 'file not found' };
226
+ }
227
+ throw err;
228
+ }
229
+ if (!stat.isFile()) {
230
+ return { malformed: 'not a regular file' };
231
+ }
232
+ let raw;
233
+ try {
234
+ raw = await fs.readFile(absolute, 'utf-8');
235
+ }
236
+ catch {
237
+ return { malformed: 'failed to read' };
238
+ }
239
+ const { frontmatterRaw, body } = splitFrontmatterAndBody(raw);
240
+ let frontmatter;
241
+ try {
242
+ frontmatter = parseFrontmatterYaml(frontmatterRaw);
243
+ }
244
+ catch (err) {
245
+ return { malformed: err.message };
246
+ }
247
+ return { raw, mtime: stat.mtime.toISOString(), frontmatter, body };
248
+ }
249
+ function makeCard(scope, rootAbs, fileAbs, frontmatter, body, mtime, extra) {
250
+ const relativePath = toPosixRelative(path.relative(rootAbs, fileAbs));
251
+ return {
252
+ scope,
253
+ absoluteFile: fileAbs,
254
+ pluginKey: extra.pluginKey,
255
+ projectSlug: extra.projectSlug,
256
+ relativePath,
257
+ slashName: deriveSlashName(relativePath),
258
+ frontmatter,
259
+ tokens: analyzeTokens(body),
260
+ mtime,
261
+ isBmadMirror: detectBmadMirror(body),
262
+ };
263
+ }
264
+ async function enumerateScopeCommands(scope, rootAbs, extra) {
265
+ const files = await walkMdFiles(rootAbs);
266
+ const reads = files.map(async (abs) => {
267
+ const result = await readMdFile(abs);
268
+ if ('malformed' in result) {
269
+ return { kind: 'malformed', entry: { abs, reason: result.malformed } };
270
+ }
271
+ return {
272
+ kind: 'card',
273
+ card: makeCard(scope, rootAbs, abs, result.frontmatter, result.body, result.mtime, extra),
274
+ };
275
+ });
276
+ const settled = await Promise.all(reads);
277
+ const cards = [];
278
+ const malformed = [];
279
+ for (const r of settled) {
280
+ if (r.kind === 'card')
281
+ cards.push(r.card);
282
+ else
283
+ malformed.push({
284
+ scope,
285
+ absoluteFile: r.entry.abs,
286
+ pluginKey: extra.pluginKey,
287
+ projectSlug: extra.projectSlug,
288
+ reason: r.entry.reason,
289
+ });
290
+ }
291
+ return { cards, malformed };
292
+ }
293
+ async function enumeratePluginCommands() {
294
+ const installed = await readInstalledPlugins();
295
+ const plugins = installed.plugins ?? {};
296
+ const tasks = [];
297
+ for (const [pluginKey, value] of Object.entries(plugins)) {
298
+ const entries = Array.isArray(value) ? value : [value];
299
+ for (const entry of entries) {
300
+ if (!entry?.installPath || typeof entry.installPath !== 'string')
301
+ continue;
302
+ for (const root of pluginCommandsRoots(entry.installPath)) {
303
+ tasks.push((async () => {
304
+ // Path containment: every walked file must remain under installPath.
305
+ try {
306
+ const stat = await fs.stat(root);
307
+ if (!stat.isDirectory())
308
+ return { cards: [], malformed: [] };
309
+ }
310
+ catch {
311
+ return { cards: [], malformed: [] };
312
+ }
313
+ const installRoot = path.resolve(entry.installPath);
314
+ if (!withinRoot(root, installRoot)) {
315
+ return { cards: [], malformed: [] };
316
+ }
317
+ const out = await enumerateScopeCommands('plugin', root, { pluginKey });
318
+ // Filter any walked file that escaped containment (defensive).
319
+ out.cards = out.cards.filter((c) => withinRoot(c.absoluteFile, installRoot));
320
+ return out;
321
+ })());
322
+ }
323
+ }
324
+ }
325
+ const settled = await Promise.all(tasks);
326
+ const cards = [];
327
+ const malformed = [];
328
+ for (const part of settled) {
329
+ cards.push(...part.cards);
330
+ malformed.push(...part.malformed);
331
+ }
332
+ return { cards, malformed };
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // Source resolution
336
+ // ---------------------------------------------------------------------------
337
+ async function resolveCommandsRoot(scope, projectSlug, pluginKey) {
338
+ if (scope === 'project') {
339
+ if (!projectSlug) {
340
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
341
+ }
342
+ return { root: await projectCommandsRoot(projectSlug) };
343
+ }
344
+ if (scope === 'user') {
345
+ return { root: userCommandsRoot() };
346
+ }
347
+ if (!pluginKey) {
348
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'pluginKey required for scope=plugin');
349
+ }
350
+ const installed = await readInstalledPlugins();
351
+ const raw = installed.plugins?.[pluginKey];
352
+ const entries = raw ? (Array.isArray(raw) ? raw : [raw]) : [];
353
+ const first = entries.find((e) => typeof e?.installPath === 'string');
354
+ if (!first?.installPath) {
355
+ throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${pluginKey}`);
356
+ }
357
+ return { root: path.join(first.installPath, COMMANDS_DIR), pluginInstallRoot: path.resolve(first.installPath) };
358
+ }
359
+ async function resolveAbsoluteFile(scope, relPosix, projectSlug, pluginKey) {
360
+ validateRelativePath(relPosix);
361
+ const { root, pluginInstallRoot } = await resolveCommandsRoot(scope, projectSlug, pluginKey);
362
+ const native = relPosix.split('/').join(path.sep);
363
+ const abs = path.resolve(root, native);
364
+ if (!withinRoot(abs, root)) {
365
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'path escapes commands root');
366
+ }
367
+ if (pluginInstallRoot && !withinRoot(abs, pluginInstallRoot)) {
368
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
369
+ }
370
+ return { abs, root, pluginInstallRoot };
371
+ }
372
+ function makeSourceLocation(scope, abs, relPosix, projectSlug, pluginKey) {
373
+ return {
374
+ scope,
375
+ absoluteFile: abs,
376
+ pluginKey,
377
+ projectSlug,
378
+ relativePath: relPosix,
379
+ slashName: deriveSlashName(relPosix),
380
+ };
381
+ }
382
+ async function scanBmadCoreSlashNames(projectSlug) {
383
+ const projectRoot = await projectService.resolveOriginalPath(projectSlug);
384
+ const bmadRoot = path.join(projectRoot, '.bmad-core');
385
+ try {
386
+ const stat = await fs.stat(bmadRoot);
387
+ if (!stat.isDirectory())
388
+ return null;
389
+ }
390
+ catch {
391
+ return null;
392
+ }
393
+ let slashPrefix = 'BMad';
394
+ try {
395
+ const cfg = await fs.readFile(path.join(bmadRoot, 'core-config.yaml'), 'utf-8');
396
+ const parsed = yaml.parse(cfg);
397
+ if (parsed && typeof parsed.slashPrefix === 'string' && parsed.slashPrefix.length > 0) {
398
+ slashPrefix = parsed.slashPrefix;
399
+ }
400
+ }
401
+ catch {
402
+ // default 'BMad'
403
+ }
404
+ const agentIds = new Set();
405
+ try {
406
+ const files = await fs.readdir(path.join(bmadRoot, 'agents'));
407
+ for (const f of files) {
408
+ if (!f.endsWith('.md'))
409
+ continue;
410
+ // Use file basename as agent id fallback — commandService uses YAML
411
+ // parse, but for de-dup we only need the slash-name string match. The
412
+ // BMad mirror file name is `<id>.md` so the basename equals the id.
413
+ agentIds.add(f.replace(/\.md$/, ''));
414
+ }
415
+ }
416
+ catch {
417
+ // no agents dir
418
+ }
419
+ const taskNames = new Set();
420
+ try {
421
+ const files = await fs.readdir(path.join(bmadRoot, 'tasks'));
422
+ for (const f of files) {
423
+ if (f.endsWith('.md'))
424
+ taskNames.add(f.replace(/\.md$/, ''));
425
+ }
426
+ }
427
+ catch {
428
+ // no tasks dir
429
+ }
430
+ return { slashPrefix, agentIds, taskNames };
431
+ }
432
+ function bmadPaletteSlashNames(scan) {
433
+ const out = new Set();
434
+ for (const id of scan.agentIds)
435
+ out.add(`/${scan.slashPrefix}:agents:${id}`);
436
+ for (const name of scan.taskNames)
437
+ out.add(`/${scan.slashPrefix}:tasks:${name}`);
438
+ return out;
439
+ }
440
+ // ---------------------------------------------------------------------------
441
+ // Service
442
+ // ---------------------------------------------------------------------------
443
+ class HarnessCommandService {
444
+ async listCards(currentProjectSlug) {
445
+ const projectTask = currentProjectSlug
446
+ ? (async () => {
447
+ try {
448
+ const root = await projectCommandsRoot(currentProjectSlug);
449
+ return enumerateScopeCommands('project', root, { projectSlug: currentProjectSlug });
450
+ }
451
+ catch (err) {
452
+ if (err?.code === HARNESS_ERRORS.HARNESS_ROOT_MISSING.code) {
453
+ return { cards: [], malformed: [] };
454
+ }
455
+ throw err;
456
+ }
457
+ })()
458
+ : Promise.resolve({ cards: [], malformed: [] });
459
+ const userTask = enumerateScopeCommands('user', userCommandsRoot(), {});
460
+ const pluginTask = enumeratePluginCommands();
461
+ const bmadTask = currentProjectSlug ? scanBmadCoreSlashNames(currentProjectSlug) : Promise.resolve(null);
462
+ const [projectPart, userPart, pluginPart, bmadScan] = await Promise.all([
463
+ projectTask,
464
+ userTask,
465
+ pluginTask,
466
+ bmadTask,
467
+ ]);
468
+ const cards = [...projectPart.cards, ...userPart.cards, ...pluginPart.cards];
469
+ const malformed = [...projectPart.malformed, ...userPart.malformed, ...pluginPart.malformed];
470
+ cards.sort((a, b) => {
471
+ const sd = SCOPE_PRIORITY[a.scope] - SCOPE_PRIORITY[b.scope];
472
+ if (sd !== 0)
473
+ return sd;
474
+ return a.relativePath.localeCompare(b.relativePath);
475
+ });
476
+ // De-dup count vs BMad palette
477
+ const bmadSlashes = bmadScan ? bmadPaletteSlashNames(bmadScan) : new Set();
478
+ const seen = new Set();
479
+ let paletteVisibleCount = 0;
480
+ for (const c of cards) {
481
+ if (bmadSlashes.has(c.slashName))
482
+ continue;
483
+ if (seen.has(c.slashName))
484
+ continue;
485
+ seen.add(c.slashName);
486
+ paletteVisibleCount += 1;
487
+ }
488
+ return { cards, malformed, paletteVisibleCount };
489
+ }
490
+ async readCommand(loc) {
491
+ const result = await readMdFile(loc.absoluteFile);
492
+ if ('malformed' in result) {
493
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, result.malformed);
494
+ }
495
+ return {
496
+ source: loc,
497
+ frontmatter: result.frontmatter,
498
+ body: result.body,
499
+ raw: result.raw,
500
+ mtime: result.mtime,
501
+ isBmadMirror: detectBmadMirror(result.body),
502
+ };
503
+ }
504
+ async createCommand(req) {
505
+ const relPosix = toPosixRelative(req.relativePath);
506
+ const { abs } = await resolveAbsoluteFile(req.scope, relPosix, req.projectSlug);
507
+ // Refuse if the target exists.
508
+ try {
509
+ await fs.stat(abs);
510
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, `command already exists at ${relPosix}`);
511
+ }
512
+ catch (err) {
513
+ if (err.code === HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code) {
514
+ throw err;
515
+ }
516
+ // ENOENT — proceed.
517
+ }
518
+ // Build initial body. Frontmatter only emitted when at least one field is set.
519
+ const fm = req.frontmatter ?? {};
520
+ const body = req.body ?? '';
521
+ const initial = applyYamlFrontmatterPatch(body, frontmatterToPatchObject(fm));
522
+ // Ensure parent directory exists (recursive) — ~/.claude/commands/ may not
523
+ // exist yet on a fresh disk per AC1(a) "global directory empty / create on
524
+ // first card".
525
+ await fs.mkdir(path.dirname(abs), { recursive: true });
526
+ // Use harnessService.write so the existing watcher self-write suppression
527
+ // applies. It requires a valid HarnessPathRef → translate from scope.
528
+ const ref = await this.buildEditableRef(req.scope, relPosix, req.projectSlug);
529
+ const written = await harnessService.write(ref, { content: initial });
530
+ return {
531
+ success: true,
532
+ source: makeSourceLocation(req.scope, abs, relPosix, req.projectSlug),
533
+ mtime: written.mtime,
534
+ };
535
+ }
536
+ async updateCommand(loc, body) {
537
+ if (loc.scope === 'plugin') {
538
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin-scope commands are read-only');
539
+ }
540
+ const editableScope = loc.scope;
541
+ const provided = [body.frontmatter, body.body, body.raw].filter((x) => x !== undefined);
542
+ if (provided.length !== 1) {
543
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'exactly one of frontmatter / body / raw is required');
544
+ }
545
+ const ref = await this.buildEditableRef(editableScope, loc.relativePath, loc.projectSlug);
546
+ const current = await harnessService.read(ref);
547
+ const sourceText = current.content ?? '';
548
+ if (body.expectedMtime !== undefined && body.expectedMtime !== current.mtime) {
549
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
550
+ currentMtime: current.mtime,
551
+ });
552
+ }
553
+ let nextText;
554
+ if (body.frontmatter !== undefined) {
555
+ nextText = applyYamlFrontmatterPatch(sourceText, frontmatterToPatchObject(body.frontmatter));
556
+ }
557
+ else if (body.body !== undefined) {
558
+ // Replace just the markdown portion. Keep frontmatter byte-for-byte.
559
+ const { frontmatterRaw } = splitFrontmatterAndBody(sourceText);
560
+ if (frontmatterRaw === null) {
561
+ nextText = body.body;
562
+ }
563
+ else {
564
+ // Find the closing `---\n?` delimiter end position in the source.
565
+ const re = /^---\s*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n)?/;
566
+ const match = re.exec(sourceText);
567
+ const head = match ? sourceText.slice(0, match[0].length) : '';
568
+ nextText = `${head}${body.body}`;
569
+ }
570
+ }
571
+ else {
572
+ // raw — replace the entire file (frontmatter + body in one pass).
573
+ nextText = body.raw;
574
+ }
575
+ // Story 30.1 (AC4.b): block writes to git-tracked command files when
576
+ // a plaintext secret is detected (matches the existing copy-flow
577
+ // detection scope — full-text scan).
578
+ if (editableScope === 'project') {
579
+ const secrets = detectSecretsInText(nextText);
580
+ await assertNoSecretOnShared({
581
+ scope: 'project',
582
+ projectSlug: loc.projectSlug,
583
+ relativePath: `.claude/commands/${loc.relativePath}`,
584
+ secretDetected: secrets.matched,
585
+ detectedAt: { lines: secrets.lines },
586
+ });
587
+ }
588
+ const written = await harnessService.write(ref, {
589
+ content: nextText,
590
+ expectedMtime: current.mtime,
591
+ });
592
+ const { body: nextBody } = splitFrontmatterAndBody(nextText);
593
+ return {
594
+ success: true,
595
+ mtime: written.mtime,
596
+ slashName: deriveSlashName(loc.relativePath),
597
+ tokens: analyzeTokens(nextBody),
598
+ };
599
+ }
600
+ async copyCommand(req) {
601
+ const sourceRel = toPosixRelative(req.sourceRelativePath);
602
+ const sourceResolved = await resolveAbsoluteFile(req.sourceScope, sourceRel, req.sourceProjectSlug, req.sourcePluginKey);
603
+ const sourceFile = await readMdFile(sourceResolved.abs);
604
+ if ('malformed' in sourceFile) {
605
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, 'source command not found');
606
+ }
607
+ const secrets = detectSecretsInRaw(sourceFile.raw);
608
+ if (secrets.matched && req.acknowledgedSecret !== true) {
609
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'client must show the secret modal and echo acknowledgedSecret', {
610
+ cause: 'secret-not-acknowledged',
611
+ details: { secretLines: secrets.lines },
612
+ });
613
+ }
614
+ const targetRel = toPosixRelative(req.targetRelativePath ?? sourceRel);
615
+ const { abs: targetAbs } = await resolveAbsoluteFile(req.targetScope, targetRel, req.targetProjectSlug);
616
+ let exists = false;
617
+ try {
618
+ const stat = await fs.stat(targetAbs);
619
+ if (stat.isFile())
620
+ exists = true;
621
+ }
622
+ catch {
623
+ // missing
624
+ }
625
+ if (exists) {
626
+ if (req.onConflict === 'skip') {
627
+ return {
628
+ success: true,
629
+ target: makeSourceLocation(req.targetScope, targetAbs, targetRel, req.targetProjectSlug),
630
+ skipped: true,
631
+ ...(this.collectCopyWarnings(req, sourceFile.raw)),
632
+ };
633
+ }
634
+ if (req.onConflict === 'rename') {
635
+ if (req.targetRelativePath === undefined) {
636
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, 'rename requires targetRelativePath');
637
+ }
638
+ // Rename → write to the new path. If the rename target also exists,
639
+ // surface the conflict so the client can re-prompt.
640
+ if (toPosixRelative(req.targetRelativePath) === sourceRel) {
641
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, 'target path equals existing conflicting path');
642
+ }
643
+ }
644
+ // overwrite → fall through
645
+ }
646
+ await fs.mkdir(path.dirname(targetAbs), { recursive: true });
647
+ const targetRef = await this.buildEditableRef(req.targetScope, targetRel, req.targetProjectSlug);
648
+ await harnessService.write(targetRef, { content: sourceFile.raw });
649
+ return {
650
+ success: true,
651
+ target: makeSourceLocation(req.targetScope, targetAbs, targetRel, req.targetProjectSlug),
652
+ skipped: false,
653
+ ...(this.collectCopyWarnings(req, sourceFile.raw)),
654
+ };
655
+ }
656
+ async copyDirectory(req) {
657
+ const sourceDirPosix = toPosixRelative(req.sourceDirectoryPath).replace(/\/+$/, '');
658
+ if (sourceDirPosix.length === 0) {
659
+ throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'sourceDirectoryPath cannot be empty');
660
+ }
661
+ const { root: sourceRoot } = await resolveCommandsRoot(req.sourceScope, req.sourceProjectSlug, req.sourcePluginKey);
662
+ const sourceDirAbs = path.resolve(sourceRoot, sourceDirPosix.split('/').join(path.sep));
663
+ if (!withinRoot(sourceDirAbs, sourceRoot)) {
664
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'source path escapes commands root');
665
+ }
666
+ const stat = await fs.stat(sourceDirAbs).catch(() => null);
667
+ if (!stat?.isDirectory()) {
668
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, 'source directory not found');
669
+ }
670
+ const files = await walkMdFiles(sourceDirAbs);
671
+ if (files.length === 0) {
672
+ return { success: true, copied: [], skipped: [] };
673
+ }
674
+ const targetDirPosix = toPosixRelative(req.targetDirectoryPath ?? sourceDirPosix).replace(/\/+$/, '');
675
+ const { root: targetRoot } = await resolveCommandsRoot(req.targetScope, req.targetProjectSlug);
676
+ // Read all source files first (for secret aggregation + body access).
677
+ const fileResults = await Promise.all(files.map(async (abs) => {
678
+ const result = await readMdFile(abs);
679
+ return { abs, result };
680
+ }));
681
+ let aggregateSecret = false;
682
+ for (const f of fileResults) {
683
+ if ('malformed' in f.result)
684
+ continue;
685
+ const sec = detectSecretsInRaw(f.result.raw);
686
+ if (sec.matched) {
687
+ aggregateSecret = true;
688
+ break;
689
+ }
690
+ }
691
+ if (aggregateSecret && req.acknowledgedSecret !== true) {
692
+ throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'client must show the secret modal and echo acknowledgedSecret', { cause: 'secret-not-acknowledged' });
693
+ }
694
+ // Build (sourceFileAbs → targetRel) mapping.
695
+ const mappings = files.map((srcAbs) => {
696
+ const relWithinSourceDir = toPosixRelative(path.relative(sourceDirAbs, srcAbs));
697
+ const targetRel = `${targetDirPosix}/${relWithinSourceDir}`;
698
+ const native = targetRel.split('/').join(path.sep);
699
+ const targetAbs = path.resolve(targetRoot, native);
700
+ return { srcAbs, sourceRelInDir: relWithinSourceDir, targetRel, targetAbs };
701
+ });
702
+ const conflicts = [];
703
+ for (const m of mappings) {
704
+ if (!withinRoot(m.targetAbs, targetRoot)) {
705
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'target path escapes commands root');
706
+ }
707
+ try {
708
+ await fs.stat(m.targetAbs);
709
+ conflicts.push(m.targetRel);
710
+ }
711
+ catch {
712
+ // missing — no conflict
713
+ }
714
+ }
715
+ if (conflicts.length > 0 && req.onConflict === 'per-file') {
716
+ const choices = req.perFileChoices ?? {};
717
+ const missing = conflicts.filter((c) => !choices[c]);
718
+ if (missing.length > 0) {
719
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, 'per-file decisions required', { details: { conflicts: missing } });
720
+ }
721
+ }
722
+ const copied = [];
723
+ const skipped = [];
724
+ let warnPluginRoot = false;
725
+ for (const m of mappings) {
726
+ const fileEntry = fileResults.find((f) => f.abs === m.srcAbs)?.result;
727
+ if (!fileEntry || 'malformed' in fileEntry) {
728
+ skipped.push(m.targetRel);
729
+ continue;
730
+ }
731
+ const conflict = conflicts.includes(m.targetRel);
732
+ let writeAbs = m.targetAbs;
733
+ let writeRel = m.targetRel;
734
+ if (conflict) {
735
+ const decision = req.onConflict === 'overwrite-all'
736
+ ? 'overwrite'
737
+ : req.onConflict === 'skip-all'
738
+ ? 'skip'
739
+ : (req.perFileChoices?.[m.targetRel] ?? 'skip');
740
+ if (decision === 'skip') {
741
+ skipped.push(m.targetRel);
742
+ continue;
743
+ }
744
+ if (decision === 'rename') {
745
+ const renamed = req.perFileRenames?.[m.targetRel];
746
+ if (!renamed) {
747
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, `rename target missing for ${m.targetRel}`);
748
+ }
749
+ const renamedPosix = toPosixRelative(renamed);
750
+ validateRelativePath(renamedPosix);
751
+ writeRel = renamedPosix;
752
+ writeAbs = path.resolve(targetRoot, renamedPosix.split('/').join(path.sep));
753
+ if (!withinRoot(writeAbs, targetRoot)) {
754
+ throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'rename escapes commands root');
755
+ }
756
+ }
757
+ }
758
+ if (req.sourceScope === 'plugin' && PLUGIN_ROOT_RE.test(fileEntry.raw)) {
759
+ warnPluginRoot = true;
760
+ }
761
+ await fs.mkdir(path.dirname(writeAbs), { recursive: true });
762
+ const targetRef = await this.buildEditableRef(req.targetScope, writeRel, req.targetProjectSlug);
763
+ await harnessService.write(targetRef, { content: fileEntry.raw });
764
+ copied.push(makeSourceLocation(req.targetScope, writeAbs, writeRel, req.targetProjectSlug));
765
+ }
766
+ const warnings = warnPluginRoot ? ['plugin-root-reference'] : [];
767
+ return {
768
+ success: true,
769
+ copied,
770
+ skipped,
771
+ ...(warnings.length > 0 ? { warnings } : {}),
772
+ };
773
+ }
774
+ async deleteCommand(req) {
775
+ // The plugin scope is rejected upstream by the Zod editableScopeSchema, so
776
+ // by the time we get here `req.scope` can only be 'project' | 'user'. No
777
+ // runtime guard is needed.
778
+ const relPosix = toPosixRelative(req.relativePath);
779
+ const { abs, root } = await resolveAbsoluteFile(req.scope, relPosix, req.projectSlug);
780
+ let stat;
781
+ try {
782
+ stat = await fs.stat(abs);
783
+ }
784
+ catch (err) {
785
+ if (err.code === 'ENOENT') {
786
+ throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, 'command not found');
787
+ }
788
+ throw err;
789
+ }
790
+ if (req.expectedMtime !== undefined && req.expectedMtime !== stat.mtime.toISOString()) {
791
+ throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
792
+ currentMtime: stat.mtime.toISOString(),
793
+ });
794
+ }
795
+ await fs.unlink(abs);
796
+ // Best-effort prune of empty parent directories up to the commands root.
797
+ let dir = path.dirname(abs);
798
+ while (withinRoot(dir, root) && path.resolve(dir) !== path.resolve(root)) {
799
+ try {
800
+ await fs.rmdir(dir);
801
+ }
802
+ catch {
803
+ break;
804
+ }
805
+ dir = path.dirname(dir);
806
+ }
807
+ return { success: true };
808
+ }
809
+ // -------------------------------------------------------------------------
810
+ // Helpers
811
+ // -------------------------------------------------------------------------
812
+ async buildEditableRef(scope, relPosix, projectSlug) {
813
+ if (scope === 'project') {
814
+ if (!projectSlug) {
815
+ throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
816
+ }
817
+ return {
818
+ scope: 'project',
819
+ projectSlug,
820
+ relativePath: `${COMMANDS_DIR}/${relPosix}`,
821
+ };
822
+ }
823
+ return {
824
+ scope: 'user',
825
+ relativePath: `${COMMANDS_DIR}/${relPosix}`,
826
+ };
827
+ }
828
+ collectCopyWarnings(req, raw) {
829
+ if (req.sourceScope === 'plugin' && PLUGIN_ROOT_RE.test(raw)) {
830
+ return { warnings: ['plugin-root-reference'] };
831
+ }
832
+ return {};
833
+ }
834
+ }
835
+ // ---------------------------------------------------------------------------
836
+ // resolveSourceLocation — used by the controller to validate path/scope tuple
837
+ // ---------------------------------------------------------------------------
838
+ export async function resolveCommandSourceLocation(input) {
839
+ const relPosix = toPosixRelative(input.relativePath);
840
+ const { abs } = await resolveAbsoluteFile(input.scope, relPosix, input.projectSlug, input.pluginKey);
841
+ return makeSourceLocation(input.scope, abs, relPosix, input.projectSlug, input.pluginKey);
842
+ }
843
+ // Exposed for unit tests + the chat slash-palette integration (Task 12).
844
+ export const harnessCommandInternals = {
845
+ analyzeTokens,
846
+ detectBmadMirror,
847
+ detectSecretsInRaw,
848
+ deriveSlashName,
849
+ validateRelativePath,
850
+ walkMdFiles,
851
+ };
852
+ export const harnessCommandService = new HarnessCommandService();
853
+ //# sourceMappingURL=harnessCommandService.js.map