@yancyyu/openhermit 1.5.8

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 (980) hide show
  1. package/LICENSE +611 -0
  2. package/README.md +220 -0
  3. package/bin/hermit.mjs +364 -0
  4. package/bin/kill-dev.js +20 -0
  5. package/dist-renderer/assets/01-BApSFlV4.png +0 -0
  6. package/dist-renderer/assets/02-CRQGs29u.png +0 -0
  7. package/dist-renderer/assets/03-BFCM2jnD.png +0 -0
  8. package/dist-renderer/assets/04-B2FThbKO.png +0 -0
  9. package/dist-renderer/assets/05-D9p0Znkd.png +0 -0
  10. package/dist-renderer/assets/06-DZAfbDlP.png +0 -0
  11. package/dist-renderer/assets/07-B_PXWGCc.png +0 -0
  12. package/dist-renderer/assets/08-DGRMZ6sl.png +0 -0
  13. package/dist-renderer/assets/09-SGCQvc7U.png +0 -0
  14. package/dist-renderer/assets/10-Cve81Q3W.png +0 -0
  15. package/dist-renderer/assets/11-DGglolDW.png +0 -0
  16. package/dist-renderer/assets/12-C3lnu79c.png +0 -0
  17. package/dist-renderer/assets/13-M59meqdw.png +0 -0
  18. package/dist-renderer/assets/ProjectEditorOverlay-BNoDw9T1.js +57 -0
  19. package/dist-renderer/assets/TeamGraphOverlay-CfGRKQIu.js +1 -0
  20. package/dist-renderer/assets/_basePickBy-Ct8Hm5_h.js +1 -0
  21. package/dist-renderer/assets/_baseUniq-BofrAFBx.js +1 -0
  22. package/dist-renderer/assets/apl-B4CMkyY2.js +1 -0
  23. package/dist-renderer/assets/arc-AbJgatzR.js +1 -0
  24. package/dist-renderer/assets/architectureDiagram-VXUJARFQ-gpniCJVk.js +36 -0
  25. package/dist-renderer/assets/asciiarmor-Df11BRmG.js +1 -0
  26. package/dist-renderer/assets/asn1-EdZsLKOL.js +1 -0
  27. package/dist-renderer/assets/asterisk-B-8jnY81.js +1 -0
  28. package/dist-renderer/assets/blockDiagram-VD42YOAC-aBbbmONC.js +122 -0
  29. package/dist-renderer/assets/brainfuck-C4LP7Hcl.js +1 -0
  30. package/dist-renderer/assets/c4Diagram-YG6GDRKO-DJio1IsU.js +10 -0
  31. package/dist-renderer/assets/channel-CZ8sd5Xf.js +1 -0
  32. package/dist-renderer/assets/chunk-4BX2VUAB-D1_HKao2.js +1 -0
  33. package/dist-renderer/assets/chunk-55IACEB6-NAmVxF4k.js +1 -0
  34. package/dist-renderer/assets/chunk-B4BG7PRW-Ce829laz.js +165 -0
  35. package/dist-renderer/assets/chunk-DI55MBZ5-Ct2Le12y.js +220 -0
  36. package/dist-renderer/assets/chunk-FMBD7UC4-Cie3DzKk.js +15 -0
  37. package/dist-renderer/assets/chunk-QN33PNHL-4f5Yb50e.js +1 -0
  38. package/dist-renderer/assets/chunk-QZHKN3VN-D9ranl9c.js +1 -0
  39. package/dist-renderer/assets/chunk-TZMSLE5B-bdGZWlEy.js +1 -0
  40. package/dist-renderer/assets/classDiagram-2ON5EDUG-CMcfSKj5.js +1 -0
  41. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CMcfSKj5.js +1 -0
  42. package/dist-renderer/assets/clike-B9uivgTg.js +1 -0
  43. package/dist-renderer/assets/clojure-BMjYHr_A.js +1 -0
  44. package/dist-renderer/assets/clone-CMuwA8RV.js +1 -0
  45. package/dist-renderer/assets/cmake-BQqOBYOt.js +1 -0
  46. package/dist-renderer/assets/cobol-CWcv1MsR.js +1 -0
  47. package/dist-renderer/assets/coffeescript-S37ZYGWr.js +1 -0
  48. package/dist-renderer/assets/commonlisp-DBKNyK5s.js +1 -0
  49. package/dist-renderer/assets/cose-bilkent-S5V4N54A-C6tvfcVi.js +1 -0
  50. package/dist-renderer/assets/crystal-SjHAIU92.js +1 -0
  51. package/dist-renderer/assets/css-BnMrqG3P.js +1 -0
  52. package/dist-renderer/assets/cypher-C_CwsFkJ.js +1 -0
  53. package/dist-renderer/assets/cytoscape.esm-DsxaTqgk.js +331 -0
  54. package/dist-renderer/assets/d-pRatUO7H.js +1 -0
  55. package/dist-renderer/assets/dagre-6UL2VRFP-B-4qcZam.js +4 -0
  56. package/dist-renderer/assets/defaultLocale-DX6XiGOO.js +1 -0
  57. package/dist-renderer/assets/diagram-PSM6KHXK-CwT3TLjx.js +24 -0
  58. package/dist-renderer/assets/diagram-QEK2KX5R-BWH6-ZFd.js +43 -0
  59. package/dist-renderer/assets/diagram-S2PKOQOG-DfpPnfi1.js +24 -0
  60. package/dist-renderer/assets/diff-DbItnlRl.js +1 -0
  61. package/dist-renderer/assets/dockerfile-BKs6k2Af.js +1 -0
  62. package/dist-renderer/assets/dtd-DF_7sFjM.js +1 -0
  63. package/dist-renderer/assets/dylan-DwRh75JA.js +1 -0
  64. package/dist-renderer/assets/ebnf-CDyGwa7X.js +1 -0
  65. package/dist-renderer/assets/ecl-Cabwm37j.js +1 -0
  66. package/dist-renderer/assets/eiffel-CnydiIhH.js +1 -0
  67. package/dist-renderer/assets/elm-vLlmbW-K.js +1 -0
  68. package/dist-renderer/assets/erDiagram-Q2GNP2WA-BFbEFR4x.js +60 -0
  69. package/dist-renderer/assets/erlang-BNw1qcRV.js +1 -0
  70. package/dist-renderer/assets/factor-kuTfRLto.js +1 -0
  71. package/dist-renderer/assets/favicon-B8xY-GVk.png +0 -0
  72. package/dist-renderer/assets/fcl-Kvtd6kyn.js +1 -0
  73. package/dist-renderer/assets/flowDiagram-NV44I4VS-Dg3cf5hW.js +162 -0
  74. package/dist-renderer/assets/forth-Ffai-XNe.js +1 -0
  75. package/dist-renderer/assets/fortran-DYz_wnZ1.js +1 -0
  76. package/dist-renderer/assets/ganttDiagram-JELNMOA3-B21y55W5.js +267 -0
  77. package/dist-renderer/assets/gas-Bneqetm1.js +1 -0
  78. package/dist-renderer/assets/gherkin-heZmZLOM.js +1 -0
  79. package/dist-renderer/assets/gitGraphDiagram-V2S2FVAM-BDV3BJzn.js +65 -0
  80. package/dist-renderer/assets/graph-BfaZ4hZt.js +1 -0
  81. package/dist-renderer/assets/groovy-D9Dt4D0W.js +1 -0
  82. package/dist-renderer/assets/haskell-BWDZoCOh.js +1 -0
  83. package/dist-renderer/assets/haxe-H-WmDvRZ.js +1 -0
  84. package/dist-renderer/assets/http-DBlCnlav.js +1 -0
  85. package/dist-renderer/assets/idl-BEugSyMb.js +1 -0
  86. package/dist-renderer/assets/index-BMXHMpkG.js +1 -0
  87. package/dist-renderer/assets/index-CCqtDawH.js +1 -0
  88. package/dist-renderer/assets/index-CVMSpK8C.js +1 -0
  89. package/dist-renderer/assets/index-CZltVMDP.js +1844 -0
  90. package/dist-renderer/assets/index-CaG9mf8s.css +1 -0
  91. package/dist-renderer/assets/index-Ct0-y9TF.js +1 -0
  92. package/dist-renderer/assets/index-pMg_LlsS.js +1 -0
  93. package/dist-renderer/assets/infoDiagram-HS3SLOUP-DvMlS0CL.js +2 -0
  94. package/dist-renderer/assets/init-Gi6I4Gst.js +1 -0
  95. package/dist-renderer/assets/javascript-qCveANmP.js +1 -0
  96. package/dist-renderer/assets/journeyDiagram-XKPGCS4Q-DIyMluRv.js +139 -0
  97. package/dist-renderer/assets/julia-DuME0IfC.js +1 -0
  98. package/dist-renderer/assets/kanban-definition-3W4ZIXB7-CVOx8f-7.js +89 -0
  99. package/dist-renderer/assets/katex-DGN8GczM.js +261 -0
  100. package/dist-renderer/assets/layout-BPKIXUf4.js +1 -0
  101. package/dist-renderer/assets/linear-CScZGLr2.js +1 -0
  102. package/dist-renderer/assets/livescript-BwQOo05w.js +1 -0
  103. package/dist-renderer/assets/lua-BgMRiT3U.js +1 -0
  104. package/dist-renderer/assets/mathematica-DTrFuWx2.js +1 -0
  105. package/dist-renderer/assets/mbox-CNhZ1qSd.js +1 -0
  106. package/dist-renderer/assets/mindmap-definition-VGOIOE7T-CmDQ7Wo6.js +68 -0
  107. package/dist-renderer/assets/mirc-CjQqDB4T.js +1 -0
  108. package/dist-renderer/assets/mllike-CXdrOF99.js +1 -0
  109. package/dist-renderer/assets/modelica-Dc1JOy9r.js +1 -0
  110. package/dist-renderer/assets/mscgen-BA5vi2Kp.js +1 -0
  111. package/dist-renderer/assets/mumps-BT43cFF4.js +1 -0
  112. package/dist-renderer/assets/nginx-DdIZxoE0.js +1 -0
  113. package/dist-renderer/assets/nsis-LdVXkNf5.js +1 -0
  114. package/dist-renderer/assets/ntriples-BfvgReVJ.js +1 -0
  115. package/dist-renderer/assets/octave-Ck1zUtKM.js +1 -0
  116. package/dist-renderer/assets/ordinal-Cboi1Yqb.js +1 -0
  117. package/dist-renderer/assets/oz-BzwKVEFT.js +1 -0
  118. package/dist-renderer/assets/pascal--L3eBynH.js +1 -0
  119. package/dist-renderer/assets/perl-CdXCOZ3F.js +1 -0
  120. package/dist-renderer/assets/pieDiagram-ADFJNKIX-DbVClin-.js +30 -0
  121. package/dist-renderer/assets/pig-CevX1Tat.js +1 -0
  122. package/dist-renderer/assets/powershell-CFHJl5sT.js +1 -0
  123. package/dist-renderer/assets/properties-C78fOPTZ.js +1 -0
  124. package/dist-renderer/assets/protobuf-ChK-085T.js +1 -0
  125. package/dist-renderer/assets/pug-DukmZTjD.js +1 -0
  126. package/dist-renderer/assets/puppet-DMA9R1ak.js +1 -0
  127. package/dist-renderer/assets/python-BuPzkPfP.js +1 -0
  128. package/dist-renderer/assets/q-pXgVlZs6.js +1 -0
  129. package/dist-renderer/assets/quadrantDiagram-AYHSOK5B-CAB0MYcW.js +7 -0
  130. package/dist-renderer/assets/r-DUYO_cvP.js +1 -0
  131. package/dist-renderer/assets/requirementDiagram-UZGBJVZJ-w2Lfpg3T.js +64 -0
  132. package/dist-renderer/assets/rpm-CTu-6PCP.js +1 -0
  133. package/dist-renderer/assets/ruby-B2Rjki9n.js +1 -0
  134. package/dist-renderer/assets/sankeyDiagram-TZEHDZUN-kvG1QoKY.js +10 -0
  135. package/dist-renderer/assets/sas-B4kiWyti.js +1 -0
  136. package/dist-renderer/assets/scheme-C41bIUwD.js +1 -0
  137. package/dist-renderer/assets/sequenceDiagram-WL72ISMW-DCVBQ23J.js +145 -0
  138. package/dist-renderer/assets/shell-CjFT_Tl9.js +1 -0
  139. package/dist-renderer/assets/sieve-C3Gn_uJK.js +1 -0
  140. package/dist-renderer/assets/simple-mode-GW_nhZxv.js +1 -0
  141. package/dist-renderer/assets/smalltalk-CnHTOXQT.js +1 -0
  142. package/dist-renderer/assets/solr-DehyRSwq.js +1 -0
  143. package/dist-renderer/assets/sparql-DkYu6x3z.js +1 -0
  144. package/dist-renderer/assets/splashScene-C8lWNnm4.js +1 -0
  145. package/dist-renderer/assets/spreadsheet-BCZA_wO0.js +1 -0
  146. package/dist-renderer/assets/sql-D0XecflT.js +1 -0
  147. package/dist-renderer/assets/stateDiagram-FKZM4ZOC-ItZ0JBvq.js +1 -0
  148. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Hpmw4dMm.js +1 -0
  149. package/dist-renderer/assets/stex-C3f8Ysf7.js +1 -0
  150. package/dist-renderer/assets/stylus-B533Al4x.js +1 -0
  151. package/dist-renderer/assets/swift-BzpIVaGY.js +1 -0
  152. package/dist-renderer/assets/tcl-DVfN8rqt.js +1 -0
  153. package/dist-renderer/assets/textile-CnDTJFAw.js +1 -0
  154. package/dist-renderer/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  155. package/dist-renderer/assets/tiki-DGYXhP31.js +1 -0
  156. package/dist-renderer/assets/timeline-definition-IT6M3QCI-BzSFaAjV.js +61 -0
  157. package/dist-renderer/assets/toml-Bm5Em-hy.js +1 -0
  158. package/dist-renderer/assets/treemap-GDKQZRPO-fSz4hQn0.js +162 -0
  159. package/dist-renderer/assets/troff-wAsdV37c.js +1 -0
  160. package/dist-renderer/assets/ttcn-CfJYG6tj.js +1 -0
  161. package/dist-renderer/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  162. package/dist-renderer/assets/turtle-B1tBg_DP.js +1 -0
  163. package/dist-renderer/assets/vb-CmGdzxic.js +1 -0
  164. package/dist-renderer/assets/vbscript-BuJXcnF6.js +1 -0
  165. package/dist-renderer/assets/velocity-D8B20fx6.js +1 -0
  166. package/dist-renderer/assets/verilog-C6RDOZhf.js +1 -0
  167. package/dist-renderer/assets/vhdl-lSbBsy5d.js +1 -0
  168. package/dist-renderer/assets/webidl-ZXfAyPTL.js +1 -0
  169. package/dist-renderer/assets/xquery-CQfU5ijd.js +1 -0
  170. package/dist-renderer/assets/xychartDiagram-PRI3JC2R-CT1kaGlv.js +7 -0
  171. package/dist-renderer/assets/yacas-BJ4BC0dw.js +1 -0
  172. package/dist-renderer/assets/z80-Hz9HOZM7.js +1 -0
  173. package/dist-renderer/index.html +1274 -0
  174. package/package.json +181 -0
  175. package/src/features/README.md +24 -0
  176. package/src/features/agent-graph/README.md +21 -0
  177. package/src/features/agent-graph/STABLE_SLOT_LAYOUT_PLAN.md +2846 -0
  178. package/src/features/agent-graph/core/domain/buildInlineActivityEntries.ts +416 -0
  179. package/src/features/agent-graph/core/domain/collapseOverflowStacks.ts +126 -0
  180. package/src/features/agent-graph/core/domain/graphOwnerIdentity.ts +55 -0
  181. package/src/features/agent-graph/core/domain/taskGraphSemantics.ts +48 -0
  182. package/src/features/agent-graph/renderer/adapters/TeamGraphAdapter.ts +1400 -0
  183. package/src/features/agent-graph/renderer/hooks/useGraphActivityContext.ts +34 -0
  184. package/src/features/agent-graph/renderer/hooks/useGraphCreateTaskDialog.tsx +126 -0
  185. package/src/features/agent-graph/renderer/hooks/useGraphMemberPopoverContext.ts +34 -0
  186. package/src/features/agent-graph/renderer/hooks/useGraphSidebarVisibility.ts +52 -0
  187. package/src/features/agent-graph/renderer/hooks/useTeamGraphAdapter.ts +173 -0
  188. package/src/features/agent-graph/renderer/hooks/useTeamGraphSurfaceActions.ts +98 -0
  189. package/src/features/agent-graph/renderer/index.ts +14 -0
  190. package/src/features/agent-graph/renderer/ui/GraphActivityCard.tsx +96 -0
  191. package/src/features/agent-graph/renderer/ui/GraphActivityHud.tsx +498 -0
  192. package/src/features/agent-graph/renderer/ui/GraphBlockingEdgePopover.tsx +207 -0
  193. package/src/features/agent-graph/renderer/ui/GraphNodePopover.tsx +573 -0
  194. package/src/features/agent-graph/renderer/ui/GraphProvisioningHud.tsx +113 -0
  195. package/src/features/agent-graph/renderer/ui/GraphTaskCard.tsx +149 -0
  196. package/src/features/agent-graph/renderer/ui/GraphTransientHandoffHud.tsx +176 -0
  197. package/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx +224 -0
  198. package/src/features/agent-graph/renderer/ui/TeamGraphTab.tsx +257 -0
  199. package/src/features/agent-graph/renderer/ui/buildTransientHandoffMessage.ts +70 -0
  200. package/src/features/recent-projects/contracts/api.ts +5 -0
  201. package/src/features/recent-projects/contracts/channels.ts +2 -0
  202. package/src/features/recent-projects/contracts/dto.ts +24 -0
  203. package/src/features/recent-projects/contracts/index.ts +4 -0
  204. package/src/features/recent-projects/contracts/normalize.ts +32 -0
  205. package/src/features/recent-projects/core/application/models/ListDashboardRecentProjectsResponse.ts +6 -0
  206. package/src/features/recent-projects/core/application/ports/ClockPort.ts +3 -0
  207. package/src/features/recent-projects/core/application/ports/ListDashboardRecentProjectsOutputPort.ts +5 -0
  208. package/src/features/recent-projects/core/application/ports/LoggerPort.ts +5 -0
  209. package/src/features/recent-projects/core/application/ports/RecentProjectsCachePort.ts +5 -0
  210. package/src/features/recent-projects/core/application/ports/RecentProjectsSourcePort.ts +14 -0
  211. package/src/features/recent-projects/core/application/use-cases/ListDashboardRecentProjectsUseCase.ts +191 -0
  212. package/src/features/recent-projects/core/domain/models/ProviderId.ts +1 -0
  213. package/src/features/recent-projects/core/domain/models/RecentProjectAggregate.ts +14 -0
  214. package/src/features/recent-projects/core/domain/models/RecentProjectCandidate.ts +14 -0
  215. package/src/features/recent-projects/core/domain/models/RecentProjectOpenTarget.ts +3 -0
  216. package/src/features/recent-projects/core/domain/policies/mergeRecentProjectCandidates.ts +88 -0
  217. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +30 -0
  218. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +27 -0
  219. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +91 -0
  220. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +326 -0
  221. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +43 -0
  222. package/src/features/recent-projects/main/index.ts +3 -0
  223. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +34 -0
  224. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +116 -0
  225. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +20 -0
  226. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +10 -0
  227. package/src/features/recent-projects/renderer/adapters/RecentProjectsSectionAdapter.ts +132 -0
  228. package/src/features/recent-projects/renderer/hooks/useOpenRecentProject.ts +143 -0
  229. package/src/features/recent-projects/renderer/hooks/useRecentProjectsSection.ts +289 -0
  230. package/src/features/recent-projects/renderer/index.ts +2 -0
  231. package/src/features/recent-projects/renderer/ui/RecentProjectCard.tsx +221 -0
  232. package/src/features/recent-projects/renderer/ui/RecentProjectsSection.tsx +167 -0
  233. package/src/features/recent-projects/renderer/utils/activeProjectTeams.ts +48 -0
  234. package/src/features/recent-projects/renderer/utils/navigation.ts +65 -0
  235. package/src/features/recent-projects/renderer/utils/projectDecorations.ts +11 -0
  236. package/src/features/recent-projects/renderer/utils/recentProjectOpenHistory.ts +268 -0
  237. package/src/features/recent-projects/renderer/utils/recentProjectsClientCache.ts +78 -0
  238. package/src/main/constants/messageTags.ts +46 -0
  239. package/src/main/constants/worktreePatterns.ts +47 -0
  240. package/src/main/server.ts +3705 -0
  241. package/src/main/services/UpdateService.ts +166 -0
  242. package/src/main/services/ccConnect/CcConnectBridge.ts +313 -0
  243. package/src/main/services/ccConnect/CcConnectClient.ts +397 -0
  244. package/src/main/services/ccConnect/MessageBridge.ts +162 -0
  245. package/src/main/services/ccConnect/ProjectMappingStore.ts +148 -0
  246. package/src/main/services/ccConnect/index.ts +8 -0
  247. package/src/main/services/teams-mvp/TeamProvisioningService.ts +275 -0
  248. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +404 -0
  249. package/src/main/services/teams-mvp/index.ts +26 -0
  250. package/src/main/types/chunks.ts +506 -0
  251. package/src/main/types/domain.ts +342 -0
  252. package/src/main/types/index.ts +24 -0
  253. package/src/main/types/jsonl.ts +355 -0
  254. package/src/main/types/messages.ts +395 -0
  255. package/src/renderer/App.tsx +287 -0
  256. package/src/renderer/api/httpClient.ts +2207 -0
  257. package/src/renderer/api/index.ts +19 -0
  258. package/src/renderer/api/providers.ts +77 -0
  259. package/src/renderer/assets/participant-avatars/01.png +0 -0
  260. package/src/renderer/assets/participant-avatars/02.png +0 -0
  261. package/src/renderer/assets/participant-avatars/03.png +0 -0
  262. package/src/renderer/assets/participant-avatars/04.png +0 -0
  263. package/src/renderer/assets/participant-avatars/05.png +0 -0
  264. package/src/renderer/assets/participant-avatars/06.png +0 -0
  265. package/src/renderer/assets/participant-avatars/07.png +0 -0
  266. package/src/renderer/assets/participant-avatars/08.png +0 -0
  267. package/src/renderer/assets/participant-avatars/09.png +0 -0
  268. package/src/renderer/assets/participant-avatars/10.png +0 -0
  269. package/src/renderer/assets/participant-avatars/11.png +0 -0
  270. package/src/renderer/assets/participant-avatars/12.png +0 -0
  271. package/src/renderer/assets/participant-avatars/13.png +0 -0
  272. package/src/renderer/components/chat/AIChatGroup.tsx +519 -0
  273. package/src/renderer/components/chat/ChatHistory.tsx +1115 -0
  274. package/src/renderer/components/chat/ChatHistoryEmptyState.tsx +15 -0
  275. package/src/renderer/components/chat/ChatHistoryItem.tsx +144 -0
  276. package/src/renderer/components/chat/ChatHistoryLoadingState.tsx +45 -0
  277. package/src/renderer/components/chat/CompactBoundary.tsx +169 -0
  278. package/src/renderer/components/chat/ContextBadge.tsx +582 -0
  279. package/src/renderer/components/chat/DisplayItemList.tsx +431 -0
  280. package/src/renderer/components/chat/LastOutputDisplay.tsx +259 -0
  281. package/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx +125 -0
  282. package/src/renderer/components/chat/SessionContextPanel/DirectoryTree/buildDirectoryTree.ts +47 -0
  283. package/src/renderer/components/chat/SessionContextPanel/DirectoryTree/types.ts +12 -0
  284. package/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdFilesSection.tsx +90 -0
  285. package/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdSection.tsx +86 -0
  286. package/src/renderer/components/chat/SessionContextPanel/components/CollapsibleSection.tsx +77 -0
  287. package/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx +248 -0
  288. package/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx +50 -0
  289. package/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx +284 -0
  290. package/src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx +290 -0
  291. package/src/renderer/components/chat/SessionContextPanel/components/SessionContextHelpTooltip.tsx +165 -0
  292. package/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx +47 -0
  293. package/src/renderer/components/chat/SessionContextPanel/components/ThinkingTextSection.tsx +47 -0
  294. package/src/renderer/components/chat/SessionContextPanel/components/ToolOutputsSection.tsx +47 -0
  295. package/src/renderer/components/chat/SessionContextPanel/components/UserMessagesSection.tsx +47 -0
  296. package/src/renderer/components/chat/SessionContextPanel/index.tsx +314 -0
  297. package/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx +76 -0
  298. package/src/renderer/components/chat/SessionContextPanel/items/MentionedFileItem.tsx +91 -0
  299. package/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx +116 -0
  300. package/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx +96 -0
  301. package/src/renderer/components/chat/SessionContextPanel/items/ToolBreakdownItem.tsx +38 -0
  302. package/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx +113 -0
  303. package/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx +69 -0
  304. package/src/renderer/components/chat/SessionContextPanel/types.ts +96 -0
  305. package/src/renderer/components/chat/SessionContextPanel/utils/formatting.ts +6 -0
  306. package/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts +23 -0
  307. package/src/renderer/components/chat/SystemChatGroup.tsx +60 -0
  308. package/src/renderer/components/chat/UserChatGroup.tsx +668 -0
  309. package/src/renderer/components/chat/items/BaseItem.tsx +213 -0
  310. package/src/renderer/components/chat/items/ExecutionTrace.tsx +279 -0
  311. package/src/renderer/components/chat/items/LinkedToolItem.tsx +235 -0
  312. package/src/renderer/components/chat/items/MetricsPill.tsx +215 -0
  313. package/src/renderer/components/chat/items/SlashItem.tsx +81 -0
  314. package/src/renderer/components/chat/items/SubagentItem.tsx +592 -0
  315. package/src/renderer/components/chat/items/TeammateMessageItem.tsx +261 -0
  316. package/src/renderer/components/chat/items/TextItem.tsx +82 -0
  317. package/src/renderer/components/chat/items/ThinkingItem.tsx +82 -0
  318. package/src/renderer/components/chat/items/baseItemHelpers.ts +42 -0
  319. package/src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx +57 -0
  320. package/src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx +56 -0
  321. package/src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx +74 -0
  322. package/src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx +102 -0
  323. package/src/renderer/components/chat/items/linkedTool/SkillToolViewer.tsx +67 -0
  324. package/src/renderer/components/chat/items/linkedTool/ToolErrorDisplay.tsx +43 -0
  325. package/src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx +66 -0
  326. package/src/renderer/components/chat/items/linkedTool/index.ts +13 -0
  327. package/src/renderer/components/chat/items/linkedTool/renderHelpers.tsx +259 -0
  328. package/src/renderer/components/chat/markdownComponents.tsx +257 -0
  329. package/src/renderer/components/chat/markdownCopyUtils.ts +18 -0
  330. package/src/renderer/components/chat/searchHighlightUtils.ts +166 -0
  331. package/src/renderer/components/chat/viewers/CodeBlockViewer.tsx +244 -0
  332. package/src/renderer/components/chat/viewers/DiffViewer.tsx +459 -0
  333. package/src/renderer/components/chat/viewers/FileLink.tsx +182 -0
  334. package/src/renderer/components/chat/viewers/MarkdownViewer.tsx +1093 -0
  335. package/src/renderer/components/chat/viewers/MermaidDiagram.tsx +116 -0
  336. package/src/renderer/components/chat/viewers/index.ts +3 -0
  337. package/src/renderer/components/chat/viewers/syntaxHighlighter.ts +583 -0
  338. package/src/renderer/components/common/AppLogo.tsx +61 -0
  339. package/src/renderer/components/common/CliInstallWarningBanner.tsx +57 -0
  340. package/src/renderer/components/common/ConfirmDialog.tsx +176 -0
  341. package/src/renderer/components/common/ConnectionStatusBadge.tsx +56 -0
  342. package/src/renderer/components/common/ContextSwitchOverlay.tsx +38 -0
  343. package/src/renderer/components/common/CopyButton.tsx +85 -0
  344. package/src/renderer/components/common/CopyablePath.tsx +68 -0
  345. package/src/renderer/components/common/ErrorBoundary.tsx +211 -0
  346. package/src/renderer/components/common/ExportDropdown.tsx +142 -0
  347. package/src/renderer/components/common/FileTree.tsx +182 -0
  348. package/src/renderer/components/common/GlobalProviderStatusHeader.tsx +316 -0
  349. package/src/renderer/components/common/OngoingIndicator.tsx +67 -0
  350. package/src/renderer/components/common/ProviderBrandLogo.tsx +206 -0
  351. package/src/renderer/components/common/RepositoryDropdown.tsx +230 -0
  352. package/src/renderer/components/common/TokenUsageDisplay.tsx +581 -0
  353. package/src/renderer/components/common/WarningBanner.tsx +25 -0
  354. package/src/renderer/components/common/WorkspaceIndicator.tsx +183 -0
  355. package/src/renderer/components/common/WorktreeBadge.tsx +123 -0
  356. package/src/renderer/components/dashboard/CliStatusBanner.tsx +1845 -0
  357. package/src/renderer/components/dashboard/DashboardView.tsx +274 -0
  358. package/src/renderer/components/extensions/ExtensionStoreView.tsx +591 -0
  359. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +52 -0
  360. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +143 -0
  361. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +282 -0
  362. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +280 -0
  363. package/src/renderer/components/extensions/common/InstallButton.tsx +186 -0
  364. package/src/renderer/components/extensions/common/InstallCountBadge.tsx +21 -0
  365. package/src/renderer/components/extensions/common/SearchInput.tsx +70 -0
  366. package/src/renderer/components/extensions/common/SourceBadge.tsx +31 -0
  367. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +560 -0
  368. package/src/renderer/components/extensions/mcp/McpServerCard.tsx +314 -0
  369. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +669 -0
  370. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +543 -0
  371. package/src/renderer/components/extensions/plugins/CapabilityChips.tsx +70 -0
  372. package/src/renderer/components/extensions/plugins/CategoryChips.tsx +67 -0
  373. package/src/renderer/components/extensions/plugins/PluginCard.tsx +146 -0
  374. package/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +270 -0
  375. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +436 -0
  376. package/src/renderer/components/extensions/skills/SkillCodeEditor.tsx +117 -0
  377. package/src/renderer/components/extensions/skills/SkillDetailDialog.tsx +372 -0
  378. package/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +856 -0
  379. package/src/renderer/components/extensions/skills/SkillImportDialog.tsx +343 -0
  380. package/src/renderer/components/extensions/skills/SkillReviewDialog.tsx +166 -0
  381. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +784 -0
  382. package/src/renderer/components/extensions/skills/skillDraftUtils.ts +234 -0
  383. package/src/renderer/components/extensions/skills/skillFolderNameUtils.ts +19 -0
  384. package/src/renderer/components/extensions/skills/skillProjectUtils.ts +13 -0
  385. package/src/renderer/components/extensions/skills/skillValidationUtils.ts +31 -0
  386. package/src/renderer/components/layout/MiddlePanel.tsx +18 -0
  387. package/src/renderer/components/layout/MoreMenu.tsx +243 -0
  388. package/src/renderer/components/layout/PaneContainer.tsx +27 -0
  389. package/src/renderer/components/layout/PaneContent.tsx +84 -0
  390. package/src/renderer/components/layout/PaneResizeHandle.tsx +85 -0
  391. package/src/renderer/components/layout/PaneSplitDropZone.tsx +54 -0
  392. package/src/renderer/components/layout/PaneView.tsx +75 -0
  393. package/src/renderer/components/layout/SessionTabContent.tsx +102 -0
  394. package/src/renderer/components/layout/Sidebar.tsx +205 -0
  395. package/src/renderer/components/layout/SortableTab.tsx +261 -0
  396. package/src/renderer/components/layout/TabBar.tsx +354 -0
  397. package/src/renderer/components/layout/TabBarActions.tsx +176 -0
  398. package/src/renderer/components/layout/TabBarRow.tsx +99 -0
  399. package/src/renderer/components/layout/TabContextMenu.tsx +171 -0
  400. package/src/renderer/components/layout/TabbedLayout.tsx +186 -0
  401. package/src/renderer/components/layout/TeamTabSectionNav.tsx +146 -0
  402. package/src/renderer/components/notifications/NotificationRow.tsx +228 -0
  403. package/src/renderer/components/notifications/NotificationsView.tsx +371 -0
  404. package/src/renderer/components/report/AssessmentBadge.tsx +78 -0
  405. package/src/renderer/components/report/ReportSection.tsx +58 -0
  406. package/src/renderer/components/report/SessionReportTab.tsx +102 -0
  407. package/src/renderer/components/report/sections/CostSection.tsx +259 -0
  408. package/src/renderer/components/report/sections/ErrorSection.tsx +100 -0
  409. package/src/renderer/components/report/sections/FrictionSection.tsx +91 -0
  410. package/src/renderer/components/report/sections/GitSection.tsx +72 -0
  411. package/src/renderer/components/report/sections/InsightsSection.tsx +207 -0
  412. package/src/renderer/components/report/sections/KeyTakeawaysSection.tsx +55 -0
  413. package/src/renderer/components/report/sections/OverviewSection.tsx +64 -0
  414. package/src/renderer/components/report/sections/QualitySection.tsx +151 -0
  415. package/src/renderer/components/report/sections/SubagentSection.tsx +88 -0
  416. package/src/renderer/components/report/sections/TimelineSection.tsx +111 -0
  417. package/src/renderer/components/report/sections/TokenSection.tsx +116 -0
  418. package/src/renderer/components/report/sections/ToolSection.tsx +77 -0
  419. package/src/renderer/components/runtime/ProviderModelBadges.tsx +142 -0
  420. package/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +327 -0
  421. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +288 -0
  422. package/src/renderer/components/runtime/providerConnectionUi.ts +408 -0
  423. package/src/renderer/components/schedules/SchedulesView.tsx +529 -0
  424. package/src/renderer/components/search/CommandPalette.tsx +610 -0
  425. package/src/renderer/components/search/SearchBar.tsx +171 -0
  426. package/src/renderer/components/settings/NotificationTriggerSettings/components/AddTriggerForm.tsx +233 -0
  427. package/src/renderer/components/settings/NotificationTriggerSettings/components/ColorPaletteSelector.tsx +144 -0
  428. package/src/renderer/components/settings/NotificationTriggerSettings/components/DynamicConfigSection.tsx +189 -0
  429. package/src/renderer/components/settings/NotificationTriggerSettings/components/GeneralInfoSection.tsx +68 -0
  430. package/src/renderer/components/settings/NotificationTriggerSettings/components/IgnorePatternsSection.tsx +73 -0
  431. package/src/renderer/components/settings/NotificationTriggerSettings/components/ModeSelector.tsx +45 -0
  432. package/src/renderer/components/settings/NotificationTriggerSettings/components/RepositoryScopeSection.tsx +63 -0
  433. package/src/renderer/components/settings/NotificationTriggerSettings/components/SectionHeader.tsx +15 -0
  434. package/src/renderer/components/settings/NotificationTriggerSettings/components/TriggerCard.tsx +150 -0
  435. package/src/renderer/components/settings/NotificationTriggerSettings/components/TriggerCardHeader.tsx +125 -0
  436. package/src/renderer/components/settings/NotificationTriggerSettings/components/TriggerConfiguration.tsx +342 -0
  437. package/src/renderer/components/settings/NotificationTriggerSettings/components/TriggerPreview.tsx +108 -0
  438. package/src/renderer/components/settings/NotificationTriggerSettings/hooks/useAddTriggerFormHandlers.ts +218 -0
  439. package/src/renderer/components/settings/NotificationTriggerSettings/hooks/useAddTriggerFormState.ts +135 -0
  440. package/src/renderer/components/settings/NotificationTriggerSettings/hooks/useRepositoryLookup.ts +47 -0
  441. package/src/renderer/components/settings/NotificationTriggerSettings/hooks/useTriggerCardState.ts +281 -0
  442. package/src/renderer/components/settings/NotificationTriggerSettings/hooks/useTriggerForm.ts +185 -0
  443. package/src/renderer/components/settings/NotificationTriggerSettings/index.tsx +87 -0
  444. package/src/renderer/components/settings/NotificationTriggerSettings/types.ts +39 -0
  445. package/src/renderer/components/settings/NotificationTriggerSettings/utils/constants.ts +50 -0
  446. package/src/renderer/components/settings/NotificationTriggerSettings/utils/trigger.ts +113 -0
  447. package/src/renderer/components/settings/SettingsTabs.tsx +110 -0
  448. package/src/renderer/components/settings/SettingsView.tsx +153 -0
  449. package/src/renderer/components/settings/components/SettingRow.tsx +44 -0
  450. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +24 -0
  451. package/src/renderer/components/settings/components/SettingsSelect.tsx +100 -0
  452. package/src/renderer/components/settings/components/SettingsToggle.tsx +45 -0
  453. package/src/renderer/components/settings/components/index.ts +8 -0
  454. package/src/renderer/components/settings/hooks/index.ts +6 -0
  455. package/src/renderer/components/settings/hooks/useSettingsConfig.ts +270 -0
  456. package/src/renderer/components/settings/hooks/useSettingsHandlers.ts +468 -0
  457. package/src/renderer/components/settings/sections/AdvancedSection.tsx +234 -0
  458. package/src/renderer/components/settings/sections/CliStatusSection.tsx +930 -0
  459. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +391 -0
  460. package/src/renderer/components/settings/sections/GeneralSection.tsx +665 -0
  461. package/src/renderer/components/settings/sections/HarnessSection.tsx +133 -0
  462. package/src/renderer/components/settings/sections/PlatformsSection.tsx +517 -0
  463. package/src/renderer/components/settings/sections/index.ts +8 -0
  464. package/src/renderer/components/sidebar/DateGroupedSessions.tsx +1115 -0
  465. package/src/renderer/components/sidebar/GlobalTaskList.tsx +853 -0
  466. package/src/renderer/components/sidebar/SessionContextMenu.tsx +182 -0
  467. package/src/renderer/components/sidebar/SessionFiltersPopover.tsx +115 -0
  468. package/src/renderer/components/sidebar/SessionItem.tsx +393 -0
  469. package/src/renderer/components/sidebar/SidebarSessions.tsx +542 -0
  470. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +286 -0
  471. package/src/renderer/components/sidebar/TaskContextMenu.tsx +86 -0
  472. package/src/renderer/components/sidebar/TaskFiltersPopover.tsx +203 -0
  473. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +370 -0
  474. package/src/renderer/components/sidebar/dateGroupedSessionsSelection.ts +33 -0
  475. package/src/renderer/components/sidebar/projectGroupPagination.ts +89 -0
  476. package/src/renderer/components/sidebar/taskFiltersState.ts +82 -0
  477. package/src/renderer/components/splash/splashScene.ts +979 -0
  478. package/src/renderer/components/team/CcSessionsSection.tsx +202 -0
  479. package/src/renderer/components/team/ClaudeLogsDialog.tsx +71 -0
  480. package/src/renderer/components/team/ClaudeLogsFilterPopover.tsx +213 -0
  481. package/src/renderer/components/team/ClaudeLogsPanel.tsx +170 -0
  482. package/src/renderer/components/team/ClaudeLogsSection.tsx +154 -0
  483. package/src/renderer/components/team/CliLogsRichView.tsx +640 -0
  484. package/src/renderer/components/team/CollapsibleTeamSection.tsx +177 -0
  485. package/src/renderer/components/team/HarnessCards.ts +38 -0
  486. package/src/renderer/components/team/MemberBadge.tsx +117 -0
  487. package/src/renderer/components/team/ProcessesSection.tsx +193 -0
  488. package/src/renderer/components/team/ProvisioningProgressBlock.tsx +389 -0
  489. package/src/renderer/components/team/RoleSelect.tsx +171 -0
  490. package/src/renderer/components/team/StepProgressBar.tsx +165 -0
  491. package/src/renderer/components/team/TaskTooltip.tsx +197 -0
  492. package/src/renderer/components/team/TeamDetailView.tsx +3002 -0
  493. package/src/renderer/components/team/TeamEmptyState.tsx +102 -0
  494. package/src/renderer/components/team/TeamListFilterPopover.tsx +183 -0
  495. package/src/renderer/components/team/TeamListView.tsx +1336 -0
  496. package/src/renderer/components/team/TeamProvisioningBanner.tsx +16 -0
  497. package/src/renderer/components/team/TeamProvisioningPanel.tsx +115 -0
  498. package/src/renderer/components/team/TeamSessionsSection.tsx +267 -0
  499. package/src/renderer/components/team/ToolApprovalDiffPreview.tsx +206 -0
  500. package/src/renderer/components/team/ToolApprovalSheet.tsx +675 -0
  501. package/src/renderer/components/team/UnreadCommentsBadge.tsx +37 -0
  502. package/src/renderer/components/team/activity/ActiveTasksBlock.tsx +191 -0
  503. package/src/renderer/components/team/activity/ActivityItem.tsx +1649 -0
  504. package/src/renderer/components/team/activity/ActivityTimeline.tsx +959 -0
  505. package/src/renderer/components/team/activity/AnimatedHeightReveal.tsx +117 -0
  506. package/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +1152 -0
  507. package/src/renderer/components/team/activity/MessageExpandDialog.tsx +213 -0
  508. package/src/renderer/components/team/activity/PendingRepliesBlock.tsx +275 -0
  509. package/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +79 -0
  510. package/src/renderer/components/team/activity/ThoughtBodyContent.tsx +150 -0
  511. package/src/renderer/components/team/activity/activityMarkdown.ts +36 -0
  512. package/src/renderer/components/team/activity/activityMessageContext.ts +68 -0
  513. package/src/renderer/components/team/activity/collapseState.ts +66 -0
  514. package/src/renderer/components/team/activity/useNewItemKeys.ts +70 -0
  515. package/src/renderer/components/team/attachments/AttachmentDisplay.tsx +132 -0
  516. package/src/renderer/components/team/attachments/AttachmentPreviewItem.tsx +62 -0
  517. package/src/renderer/components/team/attachments/AttachmentPreviewList.tsx +193 -0
  518. package/src/renderer/components/team/attachments/AttachmentThumbnail.tsx +42 -0
  519. package/src/renderer/components/team/attachments/DropZoneOverlay.tsx +54 -0
  520. package/src/renderer/components/team/attachments/ImageLightbox.tsx +132 -0
  521. package/src/renderer/components/team/attachments/SourceMessageAttachments.tsx +70 -0
  522. package/src/renderer/components/team/dialogs/AddMemberDialog.tsx +222 -0
  523. package/src/renderer/components/team/dialogs/AdvancedCliSection.tsx +347 -0
  524. package/src/renderer/components/team/dialogs/AnthropicFastModeSelector.tsx +120 -0
  525. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +489 -0
  526. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +484 -0
  527. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +448 -0
  528. package/src/renderer/components/team/dialogs/EffortLevelSelector.tsx +69 -0
  529. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +165 -0
  530. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +2859 -0
  531. package/src/renderer/components/team/dialogs/LimitContextCheckbox.tsx +57 -0
  532. package/src/renderer/components/team/dialogs/MembersJsonEditor.tsx +123 -0
  533. package/src/renderer/components/team/dialogs/OptionalSettingsSection.tsx +124 -0
  534. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +145 -0
  535. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +289 -0
  536. package/src/renderer/components/team/dialogs/ProjectPathSelector.tsx +330 -0
  537. package/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx +744 -0
  538. package/src/renderer/components/team/dialogs/ReviewDialog.tsx +130 -0
  539. package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +530 -0
  540. package/src/renderer/components/team/dialogs/SkipPermissionsCheckbox.tsx +62 -0
  541. package/src/renderer/components/team/dialogs/StatusHistoryTimeline.tsx +229 -0
  542. package/src/renderer/components/team/dialogs/TaskAttachments.tsx +394 -0
  543. package/src/renderer/components/team/dialogs/TaskCommentAwaitingReply.tsx +57 -0
  544. package/src/renderer/components/team/dialogs/TaskCommentInput.tsx +420 -0
  545. package/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +620 -0
  546. package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +1480 -0
  547. package/src/renderer/components/team/dialogs/TeamModelSelector.tsx +560 -0
  548. package/src/renderer/components/team/dialogs/TeammateRuntimeCompatibilityNotice.tsx +60 -0
  549. package/src/renderer/components/team/dialogs/ToolApprovalSettingsPanel.tsx +173 -0
  550. package/src/renderer/components/team/dialogs/editTeamRuntimeChanges.ts +179 -0
  551. package/src/renderer/components/team/dialogs/globalTaskDetailDialogLoading.ts +34 -0
  552. package/src/renderer/components/team/dialogs/launchDialogPrefill.ts +125 -0
  553. package/src/renderer/components/team/dialogs/memberModelScope.ts +77 -0
  554. package/src/renderer/components/team/dialogs/platformMeta.ts +118 -0
  555. package/src/renderer/components/team/dialogs/projectPathOptions.ts +50 -0
  556. package/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts +222 -0
  557. package/src/renderer/components/team/dialogs/providerPrepareRequestSignature.ts +122 -0
  558. package/src/renderer/components/team/dialogs/provisioningMemberScope.ts +10 -0
  559. package/src/renderer/components/team/dialogs/provisioningModelIssues.ts +124 -0
  560. package/src/renderer/components/team/dialogs/teamNameSets.ts +67 -0
  561. package/src/renderer/components/team/dialogs/teamRelaunchFlow.ts +30 -0
  562. package/src/renderer/components/team/dialogs/teammateLaunchMode.ts +49 -0
  563. package/src/renderer/components/team/dialogs/teammateRuntimeCompatibility.tsx +101 -0
  564. package/src/renderer/components/team/editor/CodeMirrorEditor.tsx +506 -0
  565. package/src/renderer/components/team/editor/EditorBinaryPlaceholder.tsx +43 -0
  566. package/src/renderer/components/team/editor/EditorBinaryState.tsx +29 -0
  567. package/src/renderer/components/team/editor/EditorBreadcrumb.tsx +84 -0
  568. package/src/renderer/components/team/editor/EditorContextMenu.tsx +213 -0
  569. package/src/renderer/components/team/editor/EditorEmptyState.tsx +35 -0
  570. package/src/renderer/components/team/editor/EditorErrorBoundary.tsx +63 -0
  571. package/src/renderer/components/team/editor/EditorErrorState.tsx +41 -0
  572. package/src/renderer/components/team/editor/EditorFileTree.tsx +903 -0
  573. package/src/renderer/components/team/editor/EditorImagePreview.tsx +138 -0
  574. package/src/renderer/components/team/editor/EditorSearchPanel.tsx +508 -0
  575. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +112 -0
  576. package/src/renderer/components/team/editor/EditorShortcutsHelp.tsx +125 -0
  577. package/src/renderer/components/team/editor/EditorStatusBar.tsx +72 -0
  578. package/src/renderer/components/team/editor/EditorTabBar.tsx +265 -0
  579. package/src/renderer/components/team/editor/EditorTabContextMenu.tsx +88 -0
  580. package/src/renderer/components/team/editor/EditorToolbar.tsx +163 -0
  581. package/src/renderer/components/team/editor/FileIcon.tsx +66 -0
  582. package/src/renderer/components/team/editor/GitStatusBadge.tsx +47 -0
  583. package/src/renderer/components/team/editor/GoToLineDialog.tsx +186 -0
  584. package/src/renderer/components/team/editor/MarkdownPreviewPane.tsx +57 -0
  585. package/src/renderer/components/team/editor/MarkdownSplitView.tsx +127 -0
  586. package/src/renderer/components/team/editor/NewFileDialog.tsx +131 -0
  587. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +924 -0
  588. package/src/renderer/components/team/editor/QuickOpenDialog.tsx +163 -0
  589. package/src/renderer/components/team/editor/SearchInFilesPanel.tsx +358 -0
  590. package/src/renderer/components/team/editor/fileIcons.ts +222 -0
  591. package/src/renderer/components/team/kanban/KanbanBoard.tsx +664 -0
  592. package/src/renderer/components/team/kanban/KanbanColumn.tsx +61 -0
  593. package/src/renderer/components/team/kanban/KanbanFilterPopover.tsx +210 -0
  594. package/src/renderer/components/team/kanban/KanbanGridLayout.tsx +460 -0
  595. package/src/renderer/components/team/kanban/KanbanSearchInput.tsx +284 -0
  596. package/src/renderer/components/team/kanban/KanbanSortPopover.tsx +140 -0
  597. package/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx +199 -0
  598. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +446 -0
  599. package/src/renderer/components/team/kanban/TrashDialog.tsx +112 -0
  600. package/src/renderer/components/team/members/CurrentTaskIndicator.tsx +56 -0
  601. package/src/renderer/components/team/members/LeadModelRow.test.tsx +133 -0
  602. package/src/renderer/components/team/members/LeadModelRow.tsx +183 -0
  603. package/src/renderer/components/team/members/MemberCard.tsx +665 -0
  604. package/src/renderer/components/team/members/MemberDetailDialog.tsx +309 -0
  605. package/src/renderer/components/team/members/MemberDetailHeader.tsx +183 -0
  606. package/src/renderer/components/team/members/MemberDetailStats.tsx +80 -0
  607. package/src/renderer/components/team/members/MemberDraftRow.test.tsx +184 -0
  608. package/src/renderer/components/team/members/MemberDraftRow.tsx +515 -0
  609. package/src/renderer/components/team/members/MemberExecutionLog.tsx +224 -0
  610. package/src/renderer/components/team/members/MemberHoverCard.tsx +292 -0
  611. package/src/renderer/components/team/members/MemberLaunchDiagnosticsButton.tsx +60 -0
  612. package/src/renderer/components/team/members/MemberList.tsx +405 -0
  613. package/src/renderer/components/team/members/MemberLogsTab.tsx +958 -0
  614. package/src/renderer/components/team/members/MemberMessagesTab.tsx +251 -0
  615. package/src/renderer/components/team/members/MemberPresenceDot.tsx +28 -0
  616. package/src/renderer/components/team/members/MemberRoleEditor.tsx +84 -0
  617. package/src/renderer/components/team/members/MemberStatsTab.tsx +299 -0
  618. package/src/renderer/components/team/members/MemberTasksTab.tsx +87 -0
  619. package/src/renderer/components/team/members/MemberWorkspaceTab.tsx +141 -0
  620. package/src/renderer/components/team/members/MembersEditorSection.tsx +495 -0
  621. package/src/renderer/components/team/members/SubagentRecentMessagesPreview.tsx +125 -0
  622. package/src/renderer/components/team/members/TeamRosterEditorSection.tsx +153 -0
  623. package/src/renderer/components/team/members/memberActivityEntries.ts +42 -0
  624. package/src/renderer/components/team/members/memberDetailTypes.ts +3 -0
  625. package/src/renderer/components/team/members/memberNameSets.ts +65 -0
  626. package/src/renderer/components/team/members/membersEditorTypes.ts +21 -0
  627. package/src/renderer/components/team/members/membersEditorUtils.ts +267 -0
  628. package/src/renderer/components/team/messages/MessageComposer.tsx +939 -0
  629. package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +228 -0
  630. package/src/renderer/components/team/messages/MessagesPanel.tsx +1508 -0
  631. package/src/renderer/components/team/messages/OpenCodeDeliveryWarning.tsx +151 -0
  632. package/src/renderer/components/team/messages/StatusBlock.tsx +126 -0
  633. package/src/renderer/components/team/provisioningSteps.ts +363 -0
  634. package/src/renderer/components/team/review/ChangeReviewDialog.tsx +133 -0
  635. package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +218 -0
  636. package/src/renderer/components/team/schedule/CronScheduleInput.tsx +254 -0
  637. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +15 -0
  638. package/src/renderer/components/team/schedule/ScheduleRunLogDialog.tsx +277 -0
  639. package/src/renderer/components/team/schedule/ScheduleRunRow.tsx +106 -0
  640. package/src/renderer/components/team/schedule/ScheduleSection.tsx +281 -0
  641. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +55 -0
  642. package/src/renderer/components/team/sidebar/TeamSidebarHost.tsx +76 -0
  643. package/src/renderer/components/team/sidebar/TeamSidebarPortalManager.ts +173 -0
  644. package/src/renderer/components/team/sidebar/TeamSidebarPortalSource.tsx +66 -0
  645. package/src/renderer/components/team/sidebar/TeamSidebarRail.tsx +68 -0
  646. package/src/renderer/components/team/sidebar/teamSidebarUiState.ts +136 -0
  647. package/src/renderer/components/team/taskLogs/ExactTaskLogCard.tsx +132 -0
  648. package/src/renderer/components/team/taskLogs/ExactTaskLogsSection.tsx +258 -0
  649. package/src/renderer/components/team/taskLogs/ExecutionSessionsSection.tsx +48 -0
  650. package/src/renderer/components/team/taskLogs/TaskActivityLinkedToolCard.tsx +31 -0
  651. package/src/renderer/components/team/taskLogs/TaskActivitySection.tsx +462 -0
  652. package/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx +375 -0
  653. package/src/renderer/components/team/taskLogs/TaskLogsPanel.tsx +294 -0
  654. package/src/renderer/components/team/taskLogs/featureGates.ts +22 -0
  655. package/src/renderer/components/team/tasks/TaskList.tsx +111 -0
  656. package/src/renderer/components/team/tasks/TaskRow.tsx +65 -0
  657. package/src/renderer/components/team/teamProjectSelection.ts +156 -0
  658. package/src/renderer/components/team/teamSessionFetchGuards.ts +26 -0
  659. package/src/renderer/components/team/useClaudeLogsController.ts +668 -0
  660. package/src/renderer/components/team/useTeamProvisioningPresentation.ts +54 -0
  661. package/src/renderer/components/terminal/TerminalLogPanel.tsx +37 -0
  662. package/src/renderer/components/ui/ChipInteractionLayer.tsx +255 -0
  663. package/src/renderer/components/ui/CodeChipBadge.tsx +37 -0
  664. package/src/renderer/components/ui/ExpandableContent.tsx +110 -0
  665. package/src/renderer/components/ui/MemberSelect.tsx +209 -0
  666. package/src/renderer/components/ui/MentionInteractionLayer.tsx +121 -0
  667. package/src/renderer/components/ui/MentionSuggestionList.tsx +274 -0
  668. package/src/renderer/components/ui/MentionableTextarea.tsx +1426 -0
  669. package/src/renderer/components/ui/SlashCommandInteractionLayer.tsx +88 -0
  670. package/src/renderer/components/ui/TaskReferenceInteractionLayer.tsx +101 -0
  671. package/src/renderer/components/ui/UrlInteractionLayer.tsx +102 -0
  672. package/src/renderer/components/ui/alert-dialog.tsx +127 -0
  673. package/src/renderer/components/ui/auto-resize-textarea.tsx +93 -0
  674. package/src/renderer/components/ui/badge.tsx +37 -0
  675. package/src/renderer/components/ui/button.tsx +54 -0
  676. package/src/renderer/components/ui/checkbox.tsx +29 -0
  677. package/src/renderer/components/ui/combobox.tsx +168 -0
  678. package/src/renderer/components/ui/context-menu.tsx +124 -0
  679. package/src/renderer/components/ui/dialog.tsx +114 -0
  680. package/src/renderer/components/ui/hover-card.tsx +30 -0
  681. package/src/renderer/components/ui/input.tsx +22 -0
  682. package/src/renderer/components/ui/label.tsx +21 -0
  683. package/src/renderer/components/ui/popover.tsx +31 -0
  684. package/src/renderer/components/ui/select.tsx +150 -0
  685. package/src/renderer/components/ui/tabs.tsx +52 -0
  686. package/src/renderer/components/ui/textarea.tsx +21 -0
  687. package/src/renderer/components/ui/tiptap/TiptapBubbleMenu.tsx +75 -0
  688. package/src/renderer/components/ui/tiptap/TiptapEditor.tsx +73 -0
  689. package/src/renderer/components/ui/tiptap/TiptapToolbar.tsx +269 -0
  690. package/src/renderer/components/ui/tiptap/index.ts +3 -0
  691. package/src/renderer/components/ui/tiptap/presets.ts +46 -0
  692. package/src/renderer/components/ui/tiptap/tiptapStyles.css +235 -0
  693. package/src/renderer/components/ui/tiptap/types.ts +32 -0
  694. package/src/renderer/components/ui/tiptap/useTiptapEditor.ts +94 -0
  695. package/src/renderer/components/ui/tooltip.tsx +32 -0
  696. package/src/renderer/constants/cssVariables.ts +226 -0
  697. package/src/renderer/constants/layout.ts +6 -0
  698. package/src/renderer/constants/teamColors.ts +397 -0
  699. package/src/renderer/constants/teamRoles.ts +41 -0
  700. package/src/renderer/contexts/TabUIContext.tsx +51 -0
  701. package/src/renderer/contexts/useTabUIContext.ts +18 -0
  702. package/src/renderer/favicon.png +0 -0
  703. package/src/renderer/features/CLAUDE.md +19 -0
  704. package/src/renderer/hooks/navigation/utils.ts +263 -0
  705. package/src/renderer/hooks/useAttachments.ts +312 -0
  706. package/src/renderer/hooks/useAutoScrollBottom.ts +285 -0
  707. package/src/renderer/hooks/useBranchSync.ts +105 -0
  708. package/src/renderer/hooks/useChipDraftPersistence.ts +172 -0
  709. package/src/renderer/hooks/useCliInstaller.ts +106 -0
  710. package/src/renderer/hooks/useCollapsedGroups.ts +71 -0
  711. package/src/renderer/hooks/useComposerDraft.ts +504 -0
  712. package/src/renderer/hooks/useContinuousScrollNav.ts +50 -0
  713. package/src/renderer/hooks/useCreateTeamDraft.ts +280 -0
  714. package/src/renderer/hooks/useDraftPersistence.ts +140 -0
  715. package/src/renderer/hooks/useEditorKeyboardShortcuts.ts +257 -0
  716. package/src/renderer/hooks/useEffectiveCliProviderStatus.ts +66 -0
  717. package/src/renderer/hooks/useExtensionsTabState.ts +206 -0
  718. package/src/renderer/hooks/useFileListCacheWarmer.ts +38 -0
  719. package/src/renderer/hooks/useFileSuggestions.ts +255 -0
  720. package/src/renderer/hooks/useKeyboardShortcuts.ts +363 -0
  721. package/src/renderer/hooks/useLazyFileContent.ts +150 -0
  722. package/src/renderer/hooks/useMarkCommentsRead.ts +27 -0
  723. package/src/renderer/hooks/useMarkdownScrollSync.ts +158 -0
  724. package/src/renderer/hooks/useMemberStats.ts +45 -0
  725. package/src/renderer/hooks/useMentionDetection.ts +375 -0
  726. package/src/renderer/hooks/usePersistedGridLayout.ts +109 -0
  727. package/src/renderer/hooks/useResizableColumns.ts +140 -0
  728. package/src/renderer/hooks/useResizablePanel.ts +144 -0
  729. package/src/renderer/hooks/useStableTeamMentionMeta.ts +68 -0
  730. package/src/renderer/hooks/useSyncedAnimationStyle.ts +29 -0
  731. package/src/renderer/hooks/useTabNavigationController.ts +524 -0
  732. package/src/renderer/hooks/useTabUI.ts +252 -0
  733. package/src/renderer/hooks/useTaskLocalState.ts +163 -0
  734. package/src/renderer/hooks/useTaskSuggestions.ts +131 -0
  735. package/src/renderer/hooks/useTeamMessagesExpanded.ts +34 -0
  736. package/src/renderer/hooks/useTeamMessagesRead.ts +52 -0
  737. package/src/renderer/hooks/useTeamSuggestions.ts +78 -0
  738. package/src/renderer/hooks/useTheme.ts +138 -0
  739. package/src/renderer/hooks/useToolApprovalDiff.ts +212 -0
  740. package/src/renderer/hooks/useUnreadCommentCount.ts +14 -0
  741. package/src/renderer/hooks/useViewedFiles.ts +74 -0
  742. package/src/renderer/hooks/useViewportCommentRead.ts +147 -0
  743. package/src/renderer/hooks/useViewportObserver.ts +138 -0
  744. package/src/renderer/hooks/useVisibleAIGroup.ts +122 -0
  745. package/src/renderer/hooks/useVisibleFileSection.ts +114 -0
  746. package/src/renderer/hooks/useZoomFactor.ts +36 -0
  747. package/src/renderer/index.css +1560 -0
  748. package/src/renderer/index.html +1293 -0
  749. package/src/renderer/lib/utils.ts +6 -0
  750. package/src/renderer/main.tsx +30 -0
  751. package/src/renderer/sentry.ts +104 -0
  752. package/src/renderer/services/__tests__/createTeamPreferences.test.ts +67 -0
  753. package/src/renderer/services/commentReadStorage.ts +349 -0
  754. package/src/renderer/services/composerDraftStorage.ts +271 -0
  755. package/src/renderer/services/contextStorage.ts +201 -0
  756. package/src/renderer/services/createTeamDraftStorage.ts +151 -0
  757. package/src/renderer/services/createTeamPreferences.ts +361 -0
  758. package/src/renderer/services/dashboardCliStatusBannerPreference.ts +20 -0
  759. package/src/renderer/services/draftStorage.ts +128 -0
  760. package/src/renderer/services/layout-system/BrowserGridLayoutRepository.ts +111 -0
  761. package/src/renderer/services/layout-system/GridLayoutRepository.ts +8 -0
  762. package/src/renderer/services/layout-system/gridLayoutSchema.ts +137 -0
  763. package/src/renderer/services/layout-system/gridLayoutTypes.ts +17 -0
  764. package/src/renderer/store/index.ts +1556 -0
  765. package/src/renderer/store/slices/changeReviewSlice.ts +1694 -0
  766. package/src/renderer/store/slices/cliInstallerSlice.ts +689 -0
  767. package/src/renderer/store/slices/configSlice.ts +111 -0
  768. package/src/renderer/store/slices/connectionSlice.ts +221 -0
  769. package/src/renderer/store/slices/contextSlice.ts +394 -0
  770. package/src/renderer/store/slices/conversationSlice.ts +510 -0
  771. package/src/renderer/store/slices/editorSlice.ts +1455 -0
  772. package/src/renderer/store/slices/extensionsSlice.ts +1415 -0
  773. package/src/renderer/store/slices/notificationSlice.ts +277 -0
  774. package/src/renderer/store/slices/paneSlice.ts +357 -0
  775. package/src/renderer/store/slices/projectSlice.ts +70 -0
  776. package/src/renderer/store/slices/repositorySlice.ts +165 -0
  777. package/src/renderer/store/slices/scheduleSlice.ts +246 -0
  778. package/src/renderer/store/slices/sessionDetailSlice.ts +755 -0
  779. package/src/renderer/store/slices/sessionSlice.ts +539 -0
  780. package/src/renderer/store/slices/subagentSlice.ts +145 -0
  781. package/src/renderer/store/slices/tabSlice.ts +842 -0
  782. package/src/renderer/store/slices/tabUISlice.ts +319 -0
  783. package/src/renderer/store/slices/teamSlice.ts +5080 -0
  784. package/src/renderer/store/slices/uiSlice.ts +45 -0
  785. package/src/renderer/store/types.ts +103 -0
  786. package/src/renderer/store/utils/paneHelpers.ts +134 -0
  787. package/src/renderer/store/utils/pathResolution.ts +121 -0
  788. package/src/renderer/store/utils/stateResetHelpers.ts +72 -0
  789. package/src/renderer/types/api.ts +8 -0
  790. package/src/renderer/types/claudeMd.ts +74 -0
  791. package/src/renderer/types/contextInjection.ts +309 -0
  792. package/src/renderer/types/data.ts +144 -0
  793. package/src/renderer/types/groups.ts +406 -0
  794. package/src/renderer/types/inlineChip.ts +110 -0
  795. package/src/renderer/types/mention.ts +38 -0
  796. package/src/renderer/types/notifications.ts +18 -0
  797. package/src/renderer/types/panes.ts +35 -0
  798. package/src/renderer/types/sessionReport.ts +386 -0
  799. package/src/renderer/types/tabs.ts +249 -0
  800. package/src/renderer/types/teamMessagesPanelMode.ts +1 -0
  801. package/src/renderer/utils/__tests__/teamEffortOptions.test.ts +217 -0
  802. package/src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts +383 -0
  803. package/src/renderer/utils/agentMessageFormatting.ts +139 -0
  804. package/src/renderer/utils/aiGroupEnhancer.ts +78 -0
  805. package/src/renderer/utils/aiGroupHelpers.ts +208 -0
  806. package/src/renderer/utils/attachmentUtils.ts +60 -0
  807. package/src/renderer/utils/bootstrapPromptSanitizer.ts +225 -0
  808. package/src/renderer/utils/bugReportUtils.ts +157 -0
  809. package/src/renderer/utils/buildSelectionAction.ts +116 -0
  810. package/src/renderer/utils/chipUtils.ts +372 -0
  811. package/src/renderer/utils/claudeCodeOnlyProviders.ts +126 -0
  812. package/src/renderer/utils/claudeMdTracker.ts +644 -0
  813. package/src/renderer/utils/codemirrorLanguages.ts +141 -0
  814. package/src/renderer/utils/codemirrorSelectionInfo.ts +41 -0
  815. package/src/renderer/utils/codemirrorTheme.ts +138 -0
  816. package/src/renderer/utils/contextMath.ts +55 -0
  817. package/src/renderer/utils/contextTracker.ts +1100 -0
  818. package/src/renderer/utils/crossTeamPendingReplies.ts +92 -0
  819. package/src/renderer/utils/dateGrouping.ts +91 -0
  820. package/src/renderer/utils/diffViewedStorage.ts +120 -0
  821. package/src/renderer/utils/displayItemBuilder.ts +587 -0
  822. package/src/renderer/utils/displaySummary.ts +74 -0
  823. package/src/renderer/utils/editorBridge.ts +90 -0
  824. package/src/renderer/utils/fileTreeBuilder.ts +110 -0
  825. package/src/renderer/utils/formatAgentRole.ts +24 -0
  826. package/src/renderer/utils/formatters.ts +61 -0
  827. package/src/renderer/utils/groupTransformer.ts +744 -0
  828. package/src/renderer/utils/idleNotificationSemantics.ts +76 -0
  829. package/src/renderer/utils/keyboardUtils.ts +92 -0
  830. package/src/renderer/utils/lastOutputDetector.ts +150 -0
  831. package/src/renderer/utils/markdownPlugins.ts +60 -0
  832. package/src/renderer/utils/memberAvatarCatalog.ts +38 -0
  833. package/src/renderer/utils/memberHelpers.ts +858 -0
  834. package/src/renderer/utils/memberLaunchDiagnostics.ts +216 -0
  835. package/src/renderer/utils/memberRuntimeSummary.ts +122 -0
  836. package/src/renderer/utils/memberSpawnStatusPolling.ts +29 -0
  837. package/src/renderer/utils/mentionLinkify.ts +90 -0
  838. package/src/renderer/utils/mentionSuggestions.ts +34 -0
  839. package/src/renderer/utils/mergeTeamMessages.ts +27 -0
  840. package/src/renderer/utils/messageRenderEquality.ts +158 -0
  841. package/src/renderer/utils/modelExtractor.ts +90 -0
  842. package/src/renderer/utils/multimodelProviderVisibility.ts +32 -0
  843. package/src/renderer/utils/openCodeModelRecommendations.ts +1326 -0
  844. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +79 -0
  845. package/src/renderer/utils/pathDisplay.ts +149 -0
  846. package/src/renderer/utils/pathNormalize.ts +61 -0
  847. package/src/renderer/utils/pathUtils.ts +47 -0
  848. package/src/renderer/utils/platformKeys.ts +24 -0
  849. package/src/renderer/utils/previewRegistry.ts +45 -0
  850. package/src/renderer/utils/projectColor.ts +54 -0
  851. package/src/renderer/utils/projectLookup.ts +64 -0
  852. package/src/renderer/utils/providerBackendIdentity.ts +43 -0
  853. package/src/renderer/utils/providerSlashCommands.ts +122 -0
  854. package/src/renderer/utils/quickOpenCache.ts +40 -0
  855. package/src/renderer/utils/refreshCliStatus.ts +17 -0
  856. package/src/renderer/utils/reportAssessments.ts +555 -0
  857. package/src/renderer/utils/reviewDecisionScope.ts +81 -0
  858. package/src/renderer/utils/reviewKey.ts +132 -0
  859. package/src/renderer/utils/runtimeDisplayName.ts +26 -0
  860. package/src/renderer/utils/scheduleFormatters.ts +45 -0
  861. package/src/renderer/utils/sessionAnalyzer.ts +1346 -0
  862. package/src/renderer/utils/sessionExporter.ts +427 -0
  863. package/src/renderer/utils/sessionTitleParser.ts +69 -0
  864. package/src/renderer/utils/skillCommandSuggestions.ts +77 -0
  865. package/src/renderer/utils/slashCommandExtractor.ts +154 -0
  866. package/src/renderer/utils/streamJsonParser.ts +393 -0
  867. package/src/renderer/utils/stringUtils.ts +50 -0
  868. package/src/renderer/utils/syntaxHighlighter.ts +158 -0
  869. package/src/renderer/utils/tabLabelDisambiguation.ts +99 -0
  870. package/src/renderer/utils/taskChangePresence.ts +19 -0
  871. package/src/renderer/utils/taskChangeRequest.ts +122 -0
  872. package/src/renderer/utils/taskCommentPendingReply.ts +74 -0
  873. package/src/renderer/utils/taskGrouping.ts +151 -0
  874. package/src/renderer/utils/taskReferenceUtils.ts +331 -0
  875. package/src/renderer/utils/teamEffortOptions.ts +150 -0
  876. package/src/renderer/utils/teamLaunchSummaryCopy.ts +17 -0
  877. package/src/renderer/utils/teamMessageExpandStorage.ts +39 -0
  878. package/src/renderer/utils/teamMessageFiltering.ts +95 -0
  879. package/src/renderer/utils/teamMessageKey.ts +14 -0
  880. package/src/renderer/utils/teamMessageReadStorage.ts +49 -0
  881. package/src/renderer/utils/teamModelAvailability.ts +465 -0
  882. package/src/renderer/utils/teamModelCatalog.ts +502 -0
  883. package/src/renderer/utils/teamModelContext.ts +34 -0
  884. package/src/renderer/utils/teamProvisioningPresentation.ts +772 -0
  885. package/src/renderer/utils/teamRuntimeSummary.ts +53 -0
  886. package/src/renderer/utils/toolLinkingEngine.ts +117 -0
  887. package/src/renderer/utils/toolRendering/index.ts +14 -0
  888. package/src/renderer/utils/toolRendering/toolContentChecks.ts +58 -0
  889. package/src/renderer/utils/toolRendering/toolSummaryHelpers.ts +276 -0
  890. package/src/renderer/utils/toolRendering/toolTokens.ts +57 -0
  891. package/src/renderer/utils/unwrapIpc.ts +31 -0
  892. package/src/renderer/utils/urlMatchUtils.ts +45 -0
  893. package/src/renderer/vite-env.d.ts +21 -0
  894. package/src/shared/constants/agentBlocks.ts +129 -0
  895. package/src/shared/constants/attachments.ts +175 -0
  896. package/src/shared/constants/cache.ts +12 -0
  897. package/src/shared/constants/cli.ts +15 -0
  898. package/src/shared/constants/crossTeam.ts +126 -0
  899. package/src/shared/constants/index.ts +15 -0
  900. package/src/shared/constants/kanban.ts +9 -0
  901. package/src/shared/constants/memberColors.ts +140 -0
  902. package/src/shared/constants/opencodeTaskLogAttribution.ts +1 -0
  903. package/src/shared/constants/teamLimits.ts +2 -0
  904. package/src/shared/constants/trafficLights.ts +60 -0
  905. package/src/shared/constants/triggerColors.ts +126 -0
  906. package/src/shared/constants/window.ts +12 -0
  907. package/src/shared/types/api.ts +1062 -0
  908. package/src/shared/types/ccConnect.ts +399 -0
  909. package/src/shared/types/cliInstaller.ts +335 -0
  910. package/src/shared/types/editor.ts +279 -0
  911. package/src/shared/types/extensions/api.ts +90 -0
  912. package/src/shared/types/extensions/apikey.ts +40 -0
  913. package/src/shared/types/extensions/common.ts +16 -0
  914. package/src/shared/types/extensions/index.ts +67 -0
  915. package/src/shared/types/extensions/mcp.ts +132 -0
  916. package/src/shared/types/extensions/plugin.ts +85 -0
  917. package/src/shared/types/extensions/skill.ts +173 -0
  918. package/src/shared/types/index.ts +48 -0
  919. package/src/shared/types/ipc.ts +5 -0
  920. package/src/shared/types/notifications.ts +407 -0
  921. package/src/shared/types/providers.ts +116 -0
  922. package/src/shared/types/review.ts +319 -0
  923. package/src/shared/types/schedule.ts +124 -0
  924. package/src/shared/types/team.ts +1726 -0
  925. package/src/shared/types/terminal.ts +49 -0
  926. package/src/shared/types/visualization.ts +60 -0
  927. package/src/shared/utils/__tests__/contextMetrics.test.ts +260 -0
  928. package/src/shared/utils/__tests__/ephemeralProjectPath.test.ts +42 -0
  929. package/src/shared/utils/__tests__/teamProvider.test.ts +29 -0
  930. package/src/shared/utils/agentLanguage.ts +122 -0
  931. package/src/shared/utils/anthropicLaunchModel.ts +96 -0
  932. package/src/shared/utils/anthropicModelDefaults.ts +3 -0
  933. package/src/shared/utils/apiErrorDetector.ts +13 -0
  934. package/src/shared/utils/boardTaskActivityLabels.ts +128 -0
  935. package/src/shared/utils/boardTaskActivityPresentation.ts +75 -0
  936. package/src/shared/utils/cliArgsParser.ts +129 -0
  937. package/src/shared/utils/contentSanitizer.ts +207 -0
  938. package/src/shared/utils/contextMetrics.ts +236 -0
  939. package/src/shared/utils/costFormatting.ts +45 -0
  940. package/src/shared/utils/diffContextHash.ts +22 -0
  941. package/src/shared/utils/effortLevels.ts +73 -0
  942. package/src/shared/utils/ephemeralProjectPath.ts +41 -0
  943. package/src/shared/utils/errorHandling.ts +26 -0
  944. package/src/shared/utils/extensionNormalizers.ts +336 -0
  945. package/src/shared/utils/idleNotificationSemantics.ts +86 -0
  946. package/src/shared/utils/inboxNoise.ts +158 -0
  947. package/src/shared/utils/leadDetection.ts +63 -0
  948. package/src/shared/utils/logger.ts +69 -0
  949. package/src/shared/utils/markdownTextSearch.ts +210 -0
  950. package/src/shared/utils/mcpScopes.ts +36 -0
  951. package/src/shared/utils/modelParser.ts +158 -0
  952. package/src/shared/utils/opencodeModelRef.ts +78 -0
  953. package/src/shared/utils/platformPath.ts +103 -0
  954. package/src/shared/utils/pricing.ts +129 -0
  955. package/src/shared/utils/providerBackend.ts +90 -0
  956. package/src/shared/utils/providerExtensionCapabilities.ts +92 -0
  957. package/src/shared/utils/providerModelSelection.ts +5 -0
  958. package/src/shared/utils/providerModelVisibility.ts +47 -0
  959. package/src/shared/utils/rateLimitDetector.ts +334 -0
  960. package/src/shared/utils/reviewState.ts +74 -0
  961. package/src/shared/utils/sentryConfig.ts +26 -0
  962. package/src/shared/utils/skillRoots.ts +93 -0
  963. package/src/shared/utils/slashCommands.ts +128 -0
  964. package/src/shared/utils/taskChangePresence.ts +35 -0
  965. package/src/shared/utils/taskChangeSince.ts +51 -0
  966. package/src/shared/utils/taskChangeState.ts +49 -0
  967. package/src/shared/utils/taskHistory.ts +82 -0
  968. package/src/shared/utils/taskIdentity.ts +32 -0
  969. package/src/shared/utils/teamGraphDefaultLayout.ts +97 -0
  970. package/src/shared/utils/teamMemberColors.ts +107 -0
  971. package/src/shared/utils/teamMemberName.ts +77 -0
  972. package/src/shared/utils/teamProvider.ts +73 -0
  973. package/src/shared/utils/teamStableOwnerId.ts +12 -0
  974. package/src/shared/utils/teammateMessageParser.ts +52 -0
  975. package/src/shared/utils/tokenFormatting.ts +91 -0
  976. package/src/shared/utils/toolSummary.ts +279 -0
  977. package/src/shared/utils/version.ts +29 -0
  978. package/src/types/agent-teams-controller.d.ts +163 -0
  979. package/src/types/node-pty.d.ts +22 -0
  980. package/src/types/pidusage.d.ts +23 -0
@@ -0,0 +1,3705 @@
1
+ /**
2
+ * Hermit standalone server (cc-connect sidecar mode).
3
+ *
4
+ * 这是 hermit 的"正式"后端入口(取代 bin/hermit-mvp/server.mjs)。
5
+ *
6
+ * 职责:
7
+ * 1. 团队管理(/api/teams /api/teams/:slug/messages /api/teams/:slug/tasks ...)
8
+ * 2. 群聊 SSE(/api/teams/:slug/group-send,通过 cc-connect Bridge WS 转发)
9
+ * 3. cc-connect 原子能力 proxy(/api/cc/* → cc-connect:9820/api/v1/*)
10
+ * 4. 静态资源托管(serve src/renderer 的 vite build 产物)
11
+ *
12
+ * 启动:
13
+ * pnpm dev:server # 仅后端
14
+ * pnpm dev # 后端 + vite dev(前端 5174,代理 /api 到 5680)
15
+ *
16
+ * 环境变量:
17
+ * HOST 默认 127.0.0.1
18
+ * PORT 默认 5680
19
+ * HERMIT_HOME 默认 ~/.hermit
20
+ * CC_CONNECT_BASE_URL 默认 http://127.0.0.1:9820
21
+ * CC_CONNECT_TOKEN cc-connect Management API token(必填)
22
+ * CC_CONNECT_BRIDGE_URL 默认 ws://127.0.0.1:9810/bridge/ws
23
+ * CC_CONNECT_BRIDGE_TOKEN cc-connect Bridge token(必填)
24
+ * STATIC_DIR 静态资源目录,默认 dist-renderer/(若不存在,/ 返回 503 提示)
25
+ */
26
+
27
+ import {
28
+ existsSync as _existsSync2,
29
+ mkdirSync,
30
+ readFileSync,
31
+ readdirSync,
32
+ renameSync,
33
+ rmSync,
34
+ statSync,
35
+ writeFileSync,
36
+ } from 'node:fs';
37
+ import os from 'node:os';
38
+ import path from 'node:path';
39
+ import { fileURLToPath } from 'node:url';
40
+
41
+ import cors from '@fastify/cors';
42
+ import staticPlugin from '@fastify/static';
43
+ import Fastify from 'fastify';
44
+
45
+ import { CcConnectBridge } from './services/ccConnect/CcConnectBridge';
46
+ import { CcConnectClient } from './services/ccConnect/CcConnectClient';
47
+ import { TeamProvisioningService } from './services/teams-mvp';
48
+ import { UpdateService } from './services/UpdateService';
49
+
50
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
51
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
52
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
53
+
54
+ const HOST = process.env.HOST ?? '127.0.0.1';
55
+ const PORT = Number.parseInt(process.env.PORT ?? '5680', 10);
56
+ const STATIC_DIR = process.env.STATIC_DIR ?? path.resolve(REPO_ROOT, 'dist-renderer');
57
+ const HARNESS_BRIDGE_CONNECT_TIMEOUT_MS = 10_000;
58
+
59
+ // ===========================================================================
60
+ // Hermit runtime config — ~/.hermit/config.json
61
+ // Priority: file > env vars > defaults
62
+ // ===========================================================================
63
+
64
+ const HERMIT_HOME = process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit');
65
+ const HERMIT_CONFIG_FILE = path.join(HERMIT_HOME, 'config.json');
66
+ const HERMIT_APP_CONFIG_FILE = path.join(HERMIT_HOME, 'app-config.json');
67
+ const HERMIT_CC_CONNECT_CONFIG_FILE = path.join(HERMIT_HOME, 'cc-connect', 'config.toml');
68
+
69
+ interface HermitConfig {
70
+ ccBaseUrl: string;
71
+ ccToken: string;
72
+ ccBridgeUrl: string;
73
+ ccBridgeToken: string;
74
+ }
75
+
76
+ function ensureWritableCcConnectConfigFile(): string {
77
+ if (_existsSync2(HERMIT_CC_CONNECT_CONFIG_FILE)) {
78
+ return HERMIT_CC_CONNECT_CONFIG_FILE;
79
+ }
80
+ throw new Error('cc-connect 配置文件不存在: ~/.hermit/cc-connect/config.toml');
81
+ }
82
+
83
+ function readCcConnectConfigTomlRaw(): { path: string; content: string } {
84
+ if (!_existsSync2(HERMIT_CC_CONNECT_CONFIG_FILE)) {
85
+ throw new Error('cc-connect 配置文件不存在: ~/.hermit/cc-connect/config.toml');
86
+ }
87
+ return {
88
+ path: HERMIT_CC_CONNECT_CONFIG_FILE,
89
+ content: readFileSync(HERMIT_CC_CONNECT_CONFIG_FILE, 'utf-8'),
90
+ };
91
+ }
92
+
93
+ function readCcConnectTomlToken(section: 'bridge' | 'management'): string {
94
+ try {
95
+ if (!_existsSync2(HERMIT_CC_CONNECT_CONFIG_FILE)) {
96
+ return '';
97
+ }
98
+ const raw = readFileSync(HERMIT_CC_CONNECT_CONFIG_FILE, 'utf-8');
99
+ const match = raw.match(new RegExp(`\\[${section}\\][^\\[]*token\\s*=\\s*"([^"]+)"`, 's'));
100
+ return match?.[1]?.trim() ?? '';
101
+ } catch {
102
+ return '';
103
+ }
104
+ }
105
+
106
+ function loadConfig(): HermitConfig {
107
+ const tomlManagementToken = readCcConnectTomlToken('management');
108
+ const tomlBridgeToken = readCcConnectTomlToken('bridge');
109
+ const defaults: HermitConfig = {
110
+ ccBaseUrl: process.env.CC_CONNECT_BASE_URL ?? 'http://127.0.0.1:9820',
111
+ ccToken: process.env.CC_CONNECT_TOKEN || process.env.CC_CONNECT_MANAGEMENT_TOKEN || tomlManagementToken,
112
+ ccBridgeUrl: process.env.CC_CONNECT_BRIDGE_URL ?? 'ws://127.0.0.1:9810/bridge/ws',
113
+ ccBridgeToken:
114
+ process.env.CC_CONNECT_BRIDGE_TOKEN ||
115
+ tomlBridgeToken ||
116
+ process.env.CC_CONNECT_TOKEN ||
117
+ process.env.CC_CONNECT_MANAGEMENT_TOKEN ||
118
+ tomlManagementToken,
119
+ };
120
+ let merged = { ...defaults };
121
+ try {
122
+ if (_existsSync2(HERMIT_CONFIG_FILE)) {
123
+ const raw = JSON.parse(readFileSync(HERMIT_CONFIG_FILE, 'utf-8')) as Partial<HermitConfig>;
124
+ merged = { ...defaults, ...raw };
125
+ }
126
+ } catch { /* ignore parse errors */ }
127
+ if (!merged.ccBridgeToken.trim()) {
128
+ merged = { ...merged, ccBridgeToken: tomlBridgeToken || merged.ccToken };
129
+ }
130
+ return merged;
131
+ }
132
+
133
+ function saveConfig(patch: Partial<HermitConfig>): HermitConfig {
134
+ const current = loadConfig();
135
+ const next = { ...current, ...patch };
136
+ mkdirSync(HERMIT_HOME, { recursive: true });
137
+ writeFileSync(HERMIT_CONFIG_FILE, JSON.stringify(next, null, 2), 'utf-8');
138
+ return next;
139
+ }
140
+
141
+ function readHermitConfigRaw(): { path: string; content: string } {
142
+ if (_existsSync2(HERMIT_CONFIG_FILE)) {
143
+ return {
144
+ path: HERMIT_CONFIG_FILE,
145
+ content: readFileSync(HERMIT_CONFIG_FILE, 'utf-8'),
146
+ };
147
+ }
148
+ return {
149
+ path: HERMIT_CONFIG_FILE,
150
+ content: `${JSON.stringify(loadConfig(), null, 2)}\n`,
151
+ };
152
+ }
153
+
154
+ function writeHermitConfigRaw(content: string): HermitConfig {
155
+ const parsed = JSON.parse(content) as unknown;
156
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
157
+ throw new Error('Hermit 配置必须是 JSON 对象');
158
+ }
159
+ mkdirSync(HERMIT_HOME, { recursive: true });
160
+ const normalized = content.endsWith('\n') ? content : `${content}\n`;
161
+ writeFileSync(HERMIT_CONFIG_FILE, normalized, 'utf-8');
162
+ return loadConfig();
163
+ }
164
+
165
+ // Mutable runtime config — updated via /api/hermit-config POST
166
+ let runtimeConfig = loadConfig();
167
+
168
+ const cc = new CcConnectClient({
169
+ baseUrl: runtimeConfig.ccBaseUrl,
170
+ token: runtimeConfig.ccToken,
171
+ bridgeUrl: runtimeConfig.ccBridgeUrl,
172
+ });
173
+ const bridge = new CcConnectBridge({
174
+ bridgeUrl: runtimeConfig.ccBridgeUrl,
175
+ bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
176
+ });
177
+ const svc = new TeamProvisioningService(cc, bridge);
178
+
179
+ function normalizeStringArray(value: unknown): string[] {
180
+ if (!Array.isArray(value)) {
181
+ return [];
182
+ }
183
+ return value
184
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
185
+ .filter((entry) => entry.length > 0);
186
+ }
187
+
188
+ function normalizePlatformAllowFrom(value: unknown): Record<string, string> {
189
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
190
+ return {};
191
+ }
192
+ const entries = Object.entries(value as Record<string, unknown>)
193
+ .map(([platform, allowFrom]) => [
194
+ platform.trim(),
195
+ typeof allowFrom === 'string' ? allowFrom.trim() : '',
196
+ ] as const)
197
+ .filter(([platform, allowFrom]) => platform.length > 0 && allowFrom.length > 0);
198
+ return Object.fromEntries(entries);
199
+ }
200
+
201
+ // ===========================================================================
202
+ // SSE 客户端管理器 — 广播 bridge 事件到所有连接的前端客户端
203
+ // ===========================================================================
204
+
205
+ type SseClient = { res: import('node:http').ServerResponse; id: string };
206
+ const sseClients = new Set<SseClient>();
207
+
208
+ function broadcastSse(eventName: string, data: unknown): void {
209
+ const payload = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
210
+ for (const client of sseClients) {
211
+ try {
212
+ client.res.write(payload);
213
+ } catch {
214
+ sseClients.delete(client);
215
+ }
216
+ }
217
+ }
218
+
219
+ // 启动 bridge 并把事件广播到 SSE 客户端
220
+ bridge.start();
221
+
222
+ bridge.on('reply', (msg) => {
223
+ const sessionKey: string = (msg as { session_key?: string }).session_key ?? '';
224
+ const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
225
+
226
+ // 存储 agent 回复到本地
227
+ svc.appendMessage(teamName, {
228
+ from: teamName,
229
+ to: 'user',
230
+ role: 'agent',
231
+ content: (msg as { content?: string }).content ?? '',
232
+ meta: { sessionKey },
233
+ }).catch(() => {});
234
+
235
+ // 广播 inbox 事件 — 前端收到后会调 scheduleTrackedTeamMessageRefresh 重拉消息
236
+ broadcastSse('team-change', { type: 'inbox', teamName });
237
+ });
238
+
239
+ bridge.on('reply_stream', (msg) => {
240
+ const sessionKey: string = (msg as { session_key?: string }).session_key ?? '';
241
+ const teamName = resolveTeamFromSessionKey(sessionKey) ?? sessionKey;
242
+ const done = (msg as { done?: boolean }).done ?? false;
243
+
244
+ if (done) {
245
+ // 流式结束,存储完整回复
246
+ const fullText = (msg as { full_text?: string }).full_text ?? '';
247
+ if (fullText) {
248
+ svc.appendMessage(teamName, {
249
+ from: teamName,
250
+ to: 'user',
251
+ role: 'agent',
252
+ content: fullText,
253
+ meta: { sessionKey },
254
+ }).catch(() => {});
255
+ }
256
+ broadcastSse('team-change', { type: 'inbox', teamName });
257
+ } else {
258
+ broadcastSse('team-change', { type: 'lead-message', teamName });
259
+ }
260
+ });
261
+
262
+ bridge.on('message', (msg) => {
263
+ const type = (msg as { type?: string }).type ?? '';
264
+ const sessionKey: string = (msg as { session_key?: string }).session_key ?? '';
265
+ if (!sessionKey) return; // 无 session_key 的控制帧(pong 等)不广播
266
+ const teamName = resolveTeamFromSessionKey(sessionKey);
267
+ if (!teamName) return;
268
+ // typing_start/stop → lead-message;其他 → inbox
269
+ const eventType = type === 'typing_start' || type === 'typing_stop' ? 'lead-message' : 'inbox';
270
+ broadcastSse('team-change', { type: eventType, teamName });
271
+ });
272
+
273
+ /**
274
+ * 从 session_key 解析 teamName。
275
+ * 约定格式:
276
+ * hermit:{teamName}:session (老格式)
277
+ * hermit:{teamName}:lead (新格式)
278
+ * bridge:hermit-{team}:{member}
279
+ * {teamName} (直接就是 teamName)
280
+ */
281
+ function resolveTeamFromSessionKey(sessionKey: string): string | null {
282
+ if (!sessionKey) return null;
283
+ // hermit:{teamName}:xxx
284
+ const hermitMatch = sessionKey.match(/^hermit:([^:]+):/);
285
+ if (hermitMatch) return hermitMatch[1];
286
+ // bridge:hermit-{team}:{member}
287
+ const bridgeMatch = sessionKey.match(/^bridge:hermit-([^:]+):/);
288
+ if (bridgeMatch) return bridgeMatch[1];
289
+ // 否则当成 teamName 本身
290
+ return sessionKey;
291
+ }
292
+
293
+ const app = Fastify({ logger: { level: 'info' } });
294
+
295
+ // ===========================================================================
296
+ // Plugins
297
+ // ===========================================================================
298
+
299
+ await app.register(cors, {
300
+ origin: process.env.CORS_ORIGIN?.split(',') ?? true,
301
+ methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
302
+ });
303
+
304
+ // ===========================================================================
305
+ // /api/cc/* → cc-connect /api/v1/* (proxy with token)
306
+ // /api/v1/* → cc-connect /api/v1/* (兼容旧 renderer 直接打 /api/v1 的代码)
307
+ // ===========================================================================
308
+
309
+ async function proxyToCcConnect(request: import('fastify').FastifyRequest, reply: import('fastify').FastifyReply, stripPrefix: string) {
310
+ const baseUrl = runtimeConfig.ccBaseUrl.replace(/\/+$/, '');
311
+ const token = runtimeConfig.ccToken;
312
+
313
+ const url = request.url;
314
+ const subPath = url.replace(new RegExp(`^${stripPrefix}`), '') || '/';
315
+ const target = `${baseUrl}/api/v1${subPath}`;
316
+
317
+ const headers: Record<string, string> = {
318
+ 'Content-Type': (request.headers['content-type'] as string) ?? 'application/json',
319
+ };
320
+ if (token) headers.Authorization = `Bearer ${token}`;
321
+
322
+ const init: RequestInit = { method: request.method, headers };
323
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
324
+ init.body = request.body == null ? undefined : JSON.stringify(request.body);
325
+ }
326
+
327
+ let upstream: Response;
328
+ try {
329
+ upstream = await fetch(target, init);
330
+ } catch (err) {
331
+ request.log.warn({ target, err }, 'cc-connect proxy network error');
332
+ return reply.code(502).send({
333
+ ok: false,
334
+ error: `cc-connect 不可达: ${err instanceof Error ? err.message : String(err)}`,
335
+ });
336
+ }
337
+
338
+ const body = Buffer.from(await upstream.arrayBuffer());
339
+ return reply
340
+ .code(upstream.status)
341
+ .header('Content-Type', upstream.headers.get('content-type') ?? 'application/json; charset=utf-8')
342
+ .send(body);
343
+ }
344
+
345
+ app.all('/api/cc/*', async (request, reply) => proxyToCcConnect(request, reply, '/api/cc'));
346
+ app.all('/api/v1/*', async (request, reply) => proxyToCcConnect(request, reply, '/api/v1'));
347
+
348
+ // ===========================================================================
349
+ // Hermit config (read/write ~/.hermit/config.json)
350
+ // ===========================================================================
351
+
352
+ app.get('/api/hermit-config', async () => ({
353
+ ok: true,
354
+ data: {
355
+ ccBaseUrl: runtimeConfig.ccBaseUrl,
356
+ // mask token: show only first 4 chars if present
357
+ ccToken: runtimeConfig.ccToken ? runtimeConfig.ccToken.slice(0, 4) + '****' : '',
358
+ ccTokenSet: runtimeConfig.ccToken.length > 0,
359
+ ccBridgeUrl: runtimeConfig.ccBridgeUrl,
360
+ },
361
+ }));
362
+
363
+ app.post<{
364
+ Body: { ccBaseUrl?: string; ccToken?: string; ccBridgeUrl?: string };
365
+ }>('/api/hermit-config', async (request, reply) => {
366
+ const { ccBaseUrl, ccToken, ccBridgeUrl } = request.body ?? {};
367
+ const patch: Partial<HermitConfig> = {};
368
+ if (ccBaseUrl !== undefined) patch.ccBaseUrl = ccBaseUrl.trim() || 'http://127.0.0.1:9820';
369
+ if (ccToken !== undefined) patch.ccToken = ccToken.trim();
370
+ if (ccBridgeUrl !== undefined)
371
+ patch.ccBridgeUrl = ccBridgeUrl.trim() || 'ws://127.0.0.1:9810/bridge/ws';
372
+
373
+ runtimeConfig = saveConfig(patch);
374
+ // Hot-update the cc client so subsequent requests use new config immediately
375
+ cc.updateConfig({ baseUrl: runtimeConfig.ccBaseUrl, token: runtimeConfig.ccToken });
376
+ bridge.updateConfig({
377
+ bridgeUrl: runtimeConfig.ccBridgeUrl,
378
+ bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
379
+ });
380
+
381
+ return {
382
+ ok: true,
383
+ data: { ccBaseUrl: runtimeConfig.ccBaseUrl, ccTokenSet: runtimeConfig.ccToken.length > 0 },
384
+ };
385
+ });
386
+
387
+ app.get('/api/hermit-config/raw', async () => {
388
+ try {
389
+ const data = readHermitConfigRaw();
390
+ return { ok: true, data };
391
+ } catch (err) {
392
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
393
+ }
394
+ });
395
+
396
+ app.post<{ Body: { content?: unknown } }>('/api/hermit-config/raw', async (request) => {
397
+ try {
398
+ const content = request.body?.content;
399
+ if (typeof content !== 'string') {
400
+ return { ok: false, error: 'content 必须是字符串' };
401
+ }
402
+ runtimeConfig = writeHermitConfigRaw(content);
403
+ cc.updateConfig({ baseUrl: runtimeConfig.ccBaseUrl, token: runtimeConfig.ccToken });
404
+ bridge.updateConfig({
405
+ bridgeUrl: runtimeConfig.ccBridgeUrl,
406
+ bridgeToken: runtimeConfig.ccBridgeToken || runtimeConfig.ccToken,
407
+ });
408
+ return {
409
+ ok: true,
410
+ data: {
411
+ ccBaseUrl: runtimeConfig.ccBaseUrl,
412
+ ccTokenSet: runtimeConfig.ccToken.length > 0,
413
+ },
414
+ };
415
+ } catch (err) {
416
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
417
+ }
418
+ });
419
+
420
+ // ===========================================================================
421
+ // cc-connect config (Hermit-managed: ~/.hermit/cc-connect/config.toml)
422
+ // ===========================================================================
423
+
424
+ function readCcConnectConfigRaw(): { path: string; content: string } {
425
+ return readCcConnectConfigTomlRaw();
426
+ }
427
+
428
+ /** Simple TOML parser for cc-connect config (handles only the fields we need). */
429
+ function readCcConnectConfig(): Record<string, unknown> {
430
+ const { content: raw } = readCcConnectConfigTomlRaw();
431
+
432
+ const result: Record<string, unknown> = {};
433
+
434
+ // Top-level simple fields
435
+ const dataDirMatch = raw.match(/^data_dir\s*=\s*"([^"]*)"/m);
436
+ if (dataDirMatch) result.data_dir = dataDirMatch[1];
437
+
438
+ const languageMatch = raw.match(/^language\s*=\s*"([^"]*)"/m);
439
+ if (languageMatch) result.language = languageMatch[1];
440
+
441
+ const idleTimeoutMatch = raw.match(/^idle_timeout_mins\s*=\s*(\d+)/m);
442
+ if (idleTimeoutMatch) result.idle_timeout_mins = Number(idleTimeoutMatch[1]);
443
+
444
+ // [management] section
445
+ const mgmtSection = raw.match(/\[management\]([^\[]*)/s);
446
+ if (mgmtSection) {
447
+ const section = mgmtSection[1];
448
+ const enabledMatch = section.match(/enabled\s*=\s*(true|false)/);
449
+ if (enabledMatch) result.management_enabled = enabledMatch[1] === 'true';
450
+ const portMatch = section.match(/port\s*=\s*(\d+)/);
451
+ if (portMatch) result.management_port = Number(portMatch[1]);
452
+ const tokenMatch = section.match(/token\s*=\s*"([^"]*)"/);
453
+ if (tokenMatch) result.management_token = tokenMatch[1];
454
+ }
455
+
456
+ // [bridge] section
457
+ const bridgeSection = raw.match(/\[bridge\]([^\[]*)/s);
458
+ if (bridgeSection) {
459
+ const section = bridgeSection[1];
460
+ const enabledMatch = section.match(/enabled\s*=\s*(true|false)/);
461
+ if (enabledMatch) result.bridge_enabled = enabledMatch[1] === 'true';
462
+ const portMatch = section.match(/port\s*=\s*(\d+)/);
463
+ if (portMatch) result.bridge_port = Number(portMatch[1]);
464
+ const tokenMatch = section.match(/token\s*=\s*"([^"]*)"/);
465
+ if (tokenMatch) result.bridge_token = tokenMatch[1];
466
+ }
467
+
468
+ // [log] section
469
+ const logSection = raw.match(/\[log\]([^\[]*)/s);
470
+ if (logSection) {
471
+ const levelMatch = logSection[1].match(/level\s*=\s*"([^"]*)"/);
472
+ if (levelMatch) result.log_level = levelMatch[1];
473
+ }
474
+
475
+ // [display] section
476
+ const displaySection = raw.match(/\[display\]([^\[]*)/s);
477
+ if (displaySection) {
478
+ const section = displaySection[1];
479
+ const thinkingMatch = section.match(/thinking_messages\s*=\s*(true|false)/);
480
+ if (thinkingMatch) result.display_thinking = thinkingMatch[1] === 'true';
481
+ const toolMatch = section.match(/tool_messages\s*=\s*(true|false)/);
482
+ if (toolMatch) result.display_tool = toolMatch[1] === 'true';
483
+ }
484
+
485
+ return result;
486
+ }
487
+
488
+ function writeCcConnectConfig(updates: Record<string, unknown>): void {
489
+ const configFile = ensureWritableCcConnectConfigFile();
490
+ let raw = readFileSync(configFile, 'utf-8');
491
+
492
+ // Update top-level fields
493
+ if (updates.language !== undefined) {
494
+ raw = raw.replace(/^(language\s*=\s*)"[^"]*"/m, `$1"${updates.language}"`);
495
+ }
496
+ if (updates.idle_timeout_mins !== undefined) {
497
+ raw = raw.replace(/^(idle_timeout_mins\s*=\s*)\d+/m, `$1${updates.idle_timeout_mins}`);
498
+ }
499
+
500
+ // Update [management] section
501
+ if (updates.management_enabled !== undefined) {
502
+ const val = updates.management_enabled ? 'true' : 'false';
503
+ raw = raw.replace(/(\[management\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s, (match, prefix, key) => `${prefix}${key}${val}`);
504
+ }
505
+ if (updates.management_port !== undefined) {
506
+ raw = raw.replace(/(\[management\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s, `$1$2${updates.management_port}`);
507
+ }
508
+ if (updates.management_token !== undefined) {
509
+ raw = raw.replace(/(\[management\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s, `$1$2"${updates.management_token}"`);
510
+ }
511
+
512
+ // Update [bridge] section
513
+ if (updates.bridge_enabled !== undefined) {
514
+ const val = updates.bridge_enabled ? 'true' : 'false';
515
+ raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(enabled\s*=\s*)(true|false)/s, `$1$2${val}`);
516
+ }
517
+ if (updates.bridge_port !== undefined) {
518
+ raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(port\s*=\s*)\d+/s, `$1$2${updates.bridge_port}`);
519
+ }
520
+ if (updates.bridge_token !== undefined) {
521
+ raw = raw.replace(/(\[bridge\][^\n]*\n(?:[^\[]*)?)(token\s*=\s*)"[^"]*"/s, `$1$2"${updates.bridge_token}"`);
522
+ }
523
+
524
+ // Update [log] section
525
+ if (updates.log_level !== undefined) {
526
+ raw = raw.replace(/(\[log\][^\n]*\n(?:[^\[]*)?)(level\s*=\s*)"[^"]*"/s, `$1$2"${updates.log_level}"`);
527
+ }
528
+
529
+ // Update [display] section
530
+ if (updates.display_thinking !== undefined) {
531
+ const val = updates.display_thinking ? 'true' : 'false';
532
+ raw = raw.replace(/(\[display\][^\n]*\n(?:[^\[]*)?)(thinking_messages\s*=\s*)(true|false)/s, `$1$2${val}`);
533
+ }
534
+ if (updates.display_tool !== undefined) {
535
+ const val = updates.display_tool ? 'true' : 'false';
536
+ raw = raw.replace(/(\[display\][^\n]*\n(?:[^\[]*)?)(tool_messages\s*=\s*)(true|false)/s, `$1$2${val}`);
537
+ }
538
+
539
+ writeFileSync(configFile, raw, 'utf-8');
540
+ }
541
+
542
+ function writeCcConnectConfigRaw(content: string): void {
543
+ const configFile = ensureWritableCcConnectConfigFile();
544
+ writeFileSync(configFile, content, 'utf-8');
545
+ }
546
+
547
+ app.get('/api/cc-config', async () => {
548
+ try {
549
+ const config = readCcConnectConfig();
550
+ return { ok: true, data: config };
551
+ } catch (err) {
552
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
553
+ }
554
+ });
555
+
556
+ app.post<{ Body: Record<string, unknown> }>('/api/cc-config', async (request, reply) => {
557
+ try {
558
+ const updates = request.body ?? {};
559
+ writeCcConnectConfig(updates);
560
+
561
+ // If management port/token changed, notify user to restart cc-connect
562
+ const needsRestart = 'management_port' in updates || 'management_token' in updates || 'bridge_port' in updates || 'bridge_token' in updates;
563
+
564
+ return {
565
+ ok: true,
566
+ data: { needsRestart },
567
+ };
568
+ } catch (err) {
569
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
570
+ }
571
+ });
572
+
573
+ app.get('/api/cc-config/raw', async () => {
574
+ try {
575
+ const data = readCcConnectConfigRaw();
576
+ return { ok: true, data };
577
+ } catch (err) {
578
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
579
+ }
580
+ });
581
+
582
+ app.post<{ Body: { content?: unknown } }>('/api/cc-config/raw', async (request) => {
583
+ try {
584
+ const content = request.body?.content;
585
+ if (typeof content !== 'string') {
586
+ return { ok: false, error: 'content 必须是字符串' };
587
+ }
588
+ writeCcConnectConfigRaw(content);
589
+ return { ok: true };
590
+ } catch (err) {
591
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
592
+ }
593
+ });
594
+
595
+ // ===========================================================================
596
+ // Health / cc-connect status (alias)
597
+ // ===========================================================================
598
+
599
+ app.get('/api/status', async () => {
600
+ try {
601
+ const data = await cc.getStatus();
602
+ return { ok: true, data };
603
+ } catch (err) {
604
+ return reply500(err);
605
+ }
606
+ });
607
+
608
+ // ===========================================================================
609
+ // cc-connect global settings proxy
610
+ // ===========================================================================
611
+
612
+ app.get('/api/cc-settings', async () => {
613
+ try {
614
+ const data = await cc.getGlobalSettings();
615
+ return { ok: true, data };
616
+ } catch (err) {
617
+ return reply500(err);
618
+ }
619
+ });
620
+
621
+ app.patch<{ Body: Record<string, unknown> }>('/api/cc-settings', async (request) => {
622
+ try {
623
+ const data = await cc.patchGlobalSettings(request.body ?? {});
624
+ return { ok: true, data };
625
+ } catch (err) {
626
+ return reply500(err);
627
+ }
628
+ });
629
+
630
+ // restart / reload cc-connect
631
+ app.post('/api/cc-restart', async () => {
632
+ try {
633
+ await cc.restart();
634
+ return { ok: true };
635
+ } catch (err) {
636
+ return reply500(err);
637
+ }
638
+ });
639
+
640
+ app.post('/api/cc-reload', async () => {
641
+ try {
642
+ await cc.reload();
643
+ return { ok: true };
644
+ } catch (err) {
645
+ return reply500(err);
646
+ }
647
+ });
648
+
649
+ // ===========================================================================
650
+ // Teams — cc-connect projects 即团队,本地 ~/.hermit/teams/ 仅存 tasks + 额外元数据
651
+ // ===========================================================================
652
+
653
+ // GET /api/teams → 从 cc-connect 读取 project 列表,合并本地元数据(displayName 等)
654
+ app.get('/api/teams', async () => {
655
+ try {
656
+ const projects = await cc.listProjects();
657
+ return await Promise.all(projects.map(async (p) => {
658
+ // platforms 从 listProjects 返回的是 string[],有 platform 即认为在线
659
+ const isOnline = Array.isArray(p.platforms) && p.platforms.length > 0;
660
+
661
+ // 尝试从本地元数据读取 displayName 等信息
662
+ let displayName = p.name;
663
+ let color = 'blue';
664
+ let description = `${p.agent_type} · ${p.platforms?.join(', ') ?? ''}`;
665
+ let workDir = '';
666
+ let pendingDelete = false;
667
+ let restartRequired = false;
668
+ try {
669
+ const meta = await svc.readTeamManifest(p.name);
670
+ if (meta.displayName) displayName = meta.displayName;
671
+ if (meta.color) color = meta.color;
672
+ if (meta.description) description = meta.description;
673
+ pendingDelete = meta.pendingDelete === true;
674
+ restartRequired = meta.restartRequired === true;
675
+ if (typeof meta.workDir === 'string') {
676
+ workDir = meta.workDir.trim();
677
+ }
678
+ } catch { /* no local manifest, use defaults */ }
679
+
680
+ // 兼容仅存在于 cc-connect 的团队:回退读取 project 详情拿 work_dir。
681
+ if (!workDir) {
682
+ try {
683
+ const detail = await cc.getProject(p.name);
684
+ if (typeof detail.work_dir === 'string') {
685
+ workDir = detail.work_dir.trim();
686
+ }
687
+ } catch {
688
+ // ignore detail read failure, keep empty path
689
+ }
690
+ }
691
+
692
+ return {
693
+ teamName: p.name,
694
+ displayName,
695
+ description,
696
+ color,
697
+ memberCount: 1,
698
+ members: [{ name: p.name, role: 'agent', agentId: p.agent_type, color }],
699
+ taskCount: 0,
700
+ lastActivity: null,
701
+ isAlive: isOnline,
702
+ harness: p.agent_type,
703
+ bindProject: p.name,
704
+ workDir,
705
+ projectPath: workDir || undefined,
706
+ sessionsCount: p.sessions_count,
707
+ heartbeatEnabled: p.heartbeat_enabled,
708
+ pendingDelete,
709
+ restartRequired,
710
+ };
711
+ }));
712
+ } catch { return []; }
713
+ });
714
+
715
+ // POST /api/teams/create → 直接在 cc-connect 创建 project
716
+ app.post('/api/teams/create', async (request, reply) => {
717
+ try {
718
+ const body = (request.body ?? {}) as Record<string, unknown>;
719
+ const name = String(body.teamName ?? body.displayName ?? '').trim();
720
+ const displayName = String(body.displayName ?? body.teamName ?? '').trim() || name;
721
+ const harness = String(body.harness ?? 'claudecode');
722
+ let workDir = String(body.workDir ?? body.cwd ?? '');
723
+
724
+ if (!name) return reply.code(400).send({ error: 'name required' });
725
+ if (!workDir) return reply.code(400).send({ error: 'workDir required' });
726
+
727
+ // Normalize path: fullwidth tilde → regular tilde, expand ~ to home
728
+ workDir = workDir.replace(/\uff5e/g, '~');
729
+ if (workDir.startsWith('~')) {
730
+ workDir = path.join(os.homedir(), workDir.slice(1));
731
+ }
732
+
733
+ // 直接调用 cc-connect add-platform(project 自动创建)
734
+ const platformType = (body.platform as string) ?? 'feishu';
735
+ const result = await cc.createProject(name, harness, workDir, platformType, {});
736
+ try {
737
+ await svc.createTeam({
738
+ displayName,
739
+ bindProject: name,
740
+ harness,
741
+ workDir,
742
+ color: typeof body.color === 'string' ? body.color : undefined,
743
+ description: typeof body.description === 'string' ? body.description : undefined,
744
+ platform: platformType,
745
+ createCcProject: false,
746
+ });
747
+ } catch (err) {
748
+ request.log.warn({ err, teamName: name }, 'failed to persist local team metadata');
749
+ }
750
+
751
+ return { ok: true, teamName: name, runId: null };
752
+ } catch (err) {
753
+ return reply.code(500).send({ error: err instanceof Error ? err.message : String(err) });
754
+ }
755
+ });
756
+
757
+ // GET /api/teams/:name/data → TeamViewSnapshot (cc-connect project 为主,本地 tasks 为辅)
758
+ app.get<{ Params: { name: string } }>('/api/teams/:name/data', async (request, reply) => {
759
+ const { name } = request.params;
760
+
761
+ // 本地元数据(始终尝试读取)
762
+ let displayName = name; // 默认使用 team ID
763
+ let color = 'blue';
764
+ let description = '';
765
+ let collaboration = true;
766
+ let workDir = '';
767
+ let harness = 'claudecode';
768
+ let language = '';
769
+ let permissionMode = 'default';
770
+ let showContextIndicator = false;
771
+ let replyFooter = false;
772
+ let injectSender = false;
773
+ let managedSources = '*';
774
+ let disabledCommands: string[] = [];
775
+ let platformAllowFrom: Record<string, string> = {};
776
+ try {
777
+ const meta = await svc.readTeamManifest(name);
778
+ if (meta.displayName) displayName = meta.displayName;
779
+ if (meta.color) color = meta.color;
780
+ if (meta.description) description = meta.description;
781
+ collaboration = meta.collaboration ?? true;
782
+ if (meta.workDir) workDir = meta.workDir;
783
+ if (meta.harness) harness = meta.harness;
784
+ if (meta.language) language = meta.language;
785
+ if (meta.permissionMode) permissionMode = meta.permissionMode;
786
+ if (typeof meta.showContextIndicator === 'boolean') {
787
+ showContextIndicator = meta.showContextIndicator;
788
+ }
789
+ if (typeof meta.replyFooter === 'boolean') {
790
+ replyFooter = meta.replyFooter;
791
+ }
792
+ if (typeof meta.injectSender === 'boolean') {
793
+ injectSender = meta.injectSender;
794
+ }
795
+ if (meta.managedSources) managedSources = meta.managedSources;
796
+ if (Array.isArray(meta.disabledCommands)) {
797
+ disabledCommands = normalizeStringArray(meta.disabledCommands);
798
+ }
799
+ if (meta.platformAllowFrom) {
800
+ platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
801
+ }
802
+ } catch { /* no local manifest */ }
803
+
804
+ // 本地任务
805
+ const rawTasks = activeTasks(await svc.readTasks(name).catch(() => []));
806
+ const teamTasks = rawTasks.map(toTeamTask);
807
+
808
+ try {
809
+ const p = await cc.getProject(name);
810
+ const isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
811
+ const projectSettings = (p.settings ?? {}) as Record<string, unknown>;
812
+ const resolvedLanguage =
813
+ typeof projectSettings.language === 'string' && projectSettings.language.trim().length > 0
814
+ ? projectSettings.language.trim()
815
+ : language;
816
+ const resolvedManagedSources =
817
+ typeof projectSettings.admin_from === 'string' && projectSettings.admin_from.trim().length > 0
818
+ ? projectSettings.admin_from.trim()
819
+ : managedSources;
820
+ const resolvedDisabledCommands =
821
+ Array.isArray(projectSettings.disabled_commands) &&
822
+ normalizeStringArray(projectSettings.disabled_commands).length > 0
823
+ ? normalizeStringArray(projectSettings.disabled_commands)
824
+ : disabledCommands;
825
+ const resolvedShowContextIndicator =
826
+ typeof projectSettings.show_context_indicator === 'boolean'
827
+ ? projectSettings.show_context_indicator
828
+ : showContextIndicator;
829
+ const resolvedReplyFooter =
830
+ typeof projectSettings.reply_footer === 'boolean'
831
+ ? projectSettings.reply_footer
832
+ : replyFooter;
833
+ const resolvedInjectSender =
834
+ typeof projectSettings.inject_sender === 'boolean'
835
+ ? projectSettings.inject_sender
836
+ : injectSender;
837
+ const resolvedPlatformAllowFrom = (() => {
838
+ const normalized = normalizePlatformAllowFrom(projectSettings.platform_allow_from);
839
+ if (Object.keys(normalized).length > 0) {
840
+ return normalized;
841
+ }
842
+ return platformAllowFrom;
843
+ })();
844
+ const resolvedPermissionMode =
845
+ typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
846
+ ? p.agent_mode.trim()
847
+ : permissionMode;
848
+
849
+ return {
850
+ teamName: name,
851
+ config: {
852
+ name: displayName, // 使用 displayName 作为展示名称
853
+ color,
854
+ description,
855
+ language: resolvedLanguage,
856
+ agentType: p.agent_type,
857
+ permissionMode: resolvedPermissionMode,
858
+ showContextIndicator: resolvedShowContextIndicator,
859
+ replyFooter: resolvedReplyFooter,
860
+ injectSender: resolvedInjectSender,
861
+ managedSources: resolvedManagedSources,
862
+ disabledCommands: resolvedDisabledCommands,
863
+ platformAllowFrom: resolvedPlatformAllowFrom,
864
+ projectPath: p.work_dir ?? workDir,
865
+ members: [{ name: displayName, role: 'lead' }],
866
+ },
867
+ tasks: teamTasks,
868
+ members: [{
869
+ name: displayName,
870
+ agentId: p.agent_type,
871
+ agentType: p.agent_type,
872
+ role: 'lead',
873
+ color,
874
+ currentTaskId: null,
875
+ taskCount: teamTasks.length,
876
+ }],
877
+ kanbanState: { teamName: name, reviewers: [], tasks: {} },
878
+ processes: [],
879
+ isAlive: isOnline,
880
+ harness: p.agent_type,
881
+ bindProject: name,
882
+ collaboration,
883
+ description,
884
+ workDir: p.work_dir ?? workDir,
885
+ permissionMode: resolvedPermissionMode,
886
+ settings: {
887
+ ...projectSettings,
888
+ language: resolvedLanguage,
889
+ admin_from: resolvedManagedSources,
890
+ disabled_commands: resolvedDisabledCommands,
891
+ show_context_indicator: resolvedShowContextIndicator,
892
+ reply_footer: resolvedReplyFooter,
893
+ inject_sender: resolvedInjectSender,
894
+ platform_allow_from: resolvedPlatformAllowFrom,
895
+ },
896
+ heartbeat: p.heartbeat,
897
+ activeSessions: p.active_session_keys ?? [],
898
+ };
899
+ } catch {
900
+ // Project deleted from cc-connect (e.g., after stop) — return offline team data from local metadata
901
+ return {
902
+ teamName: name,
903
+ config: {
904
+ name: displayName, // 使用 displayName 作为展示名称
905
+ color,
906
+ description,
907
+ language,
908
+ agentType: harness,
909
+ permissionMode,
910
+ showContextIndicator,
911
+ replyFooter,
912
+ injectSender,
913
+ managedSources,
914
+ disabledCommands,
915
+ platformAllowFrom,
916
+ projectPath: workDir,
917
+ members: [{ name: displayName, role: 'lead' }],
918
+ },
919
+ tasks: teamTasks,
920
+ members: [{
921
+ name: displayName,
922
+ agentId: harness,
923
+ agentType: harness,
924
+ role: 'lead',
925
+ color,
926
+ currentTaskId: null,
927
+ taskCount: teamTasks.length,
928
+ }],
929
+ kanbanState: { teamName: name, reviewers: [], tasks: {} },
930
+ processes: [],
931
+ isAlive: false,
932
+ harness,
933
+ bindProject: name,
934
+ collaboration,
935
+ description,
936
+ workDir,
937
+ permissionMode,
938
+ heartbeat: null,
939
+ settings: {
940
+ language,
941
+ admin_from: managedSources,
942
+ disabled_commands: disabledCommands,
943
+ show_context_indicator: showContextIndicator,
944
+ reply_footer: replyFooter,
945
+ inject_sender: injectSender,
946
+ platform_allow_from: platformAllowFrom,
947
+ },
948
+ activeSessions: [],
949
+ };
950
+ }
951
+ });
952
+
953
+ // PATCH /api/teams/:name — 更新团队元数据
954
+ app.patch<{ Params: { name: string }; Body: { displayName?: string; color?: string; description?: string } }>(
955
+ '/api/teams/:name', async (request, reply) => {
956
+ try {
957
+ const updated = await svc.updateTeam(request.params.name, request.body ?? {});
958
+ return { ok: true, data: updated };
959
+ } catch (err) {
960
+ return reply.code(404).send(reply500(err));
961
+ }
962
+ }
963
+ );
964
+
965
+ // DELETE /api/teams/:name
966
+ app.delete<{ Params: { name: string }; Querystring: { deleteFiles?: string } }>(
967
+ '/api/teams/:name', async (request, reply) => {
968
+ const teamName = request.params.name;
969
+ try {
970
+ try {
971
+ const result = await cc.deleteProject(teamName);
972
+ await svc.updateTeam(teamName, {
973
+ pendingDelete: true,
974
+ restartRequired: result.restart_required === true,
975
+ });
976
+ } catch (err) {
977
+ request.log.warn({ err, teamName }, 'delete cc-connect project failed or project missing');
978
+ }
979
+ if (request.query.deleteFiles === 'true') {
980
+ await svc.deleteTeam(teamName, { deleteFiles: true });
981
+ }
982
+ return { ok: true, restartRequired: true };
983
+ } catch (err) {
984
+ return reply.code(500).send(reply500(err));
985
+ }
986
+ }
987
+ );
988
+
989
+ // ===========================================================================
990
+ // Tasks — 存储在 ~/.hermit/teams/:name/tasks/board.json
991
+ // 双向映射:TeamTask(pending/in_progress/completed) ↔ Task(todo/doing/done)
992
+ // assignee 变化时触发 Task Dispatcher(Bridge 推消息给目标团队 agent)
993
+ // ===========================================================================
994
+
995
+ /** TeamTask status → internal Task status */
996
+ function toTaskStatus(s: string): 'todo' | 'doing' | 'done' {
997
+ if (s === 'in_progress') return 'doing';
998
+ if (s === 'completed') return 'done';
999
+ return 'todo';
1000
+ }
1001
+
1002
+ /** internal Task → TeamTask shape (for UI consumption) */
1003
+ function toTeamTask(task: { id: string; title?: string; subject?: string; description?: string; status: string; assignee?: string | null; result?: string | null; createdAt: string; updatedAt: string; order: number; teamSlug: string }) {
1004
+ const statusMap: Record<string, string> = { todo: 'pending', doing: 'in_progress', done: 'completed' };
1005
+ return {
1006
+ id: task.id,
1007
+ displayId: task.id.slice(0, 8),
1008
+ subject: task.title ?? task.subject ?? '',
1009
+ description: task.description ?? '',
1010
+ status: statusMap[task.status] ?? 'pending',
1011
+ owner: task.assignee ?? undefined,
1012
+ createdAt: task.createdAt,
1013
+ updatedAt: task.updatedAt,
1014
+ result: task.result ?? undefined,
1015
+ };
1016
+ }
1017
+
1018
+ function isSoftDeletedTask(task: { result?: string | null }): boolean {
1019
+ return task.result === '__deleted__';
1020
+ }
1021
+
1022
+ function activeTasks<T extends { result?: string | null }>(tasks: T[]): T[] {
1023
+ return tasks.filter((task) => !isSoftDeletedTask(task));
1024
+ }
1025
+
1026
+ function mapCcSessionDetail(detail: {
1027
+ id: string;
1028
+ name: string;
1029
+ session_key: string;
1030
+ agent_session_id?: string;
1031
+ agent_type: string;
1032
+ active: boolean;
1033
+ live: boolean;
1034
+ history_count: number;
1035
+ created_at: string;
1036
+ updated_at: string;
1037
+ platform: string;
1038
+ history: { role: 'user' | 'assistant'; content: string; timestamp: string }[];
1039
+ }) {
1040
+ return {
1041
+ id: detail.id,
1042
+ name: detail.name,
1043
+ sessionKey: detail.session_key,
1044
+ agentSessionId: detail.agent_session_id,
1045
+ agentType: detail.agent_type,
1046
+ active: detail.active,
1047
+ live: detail.live,
1048
+ historyCount: detail.history_count,
1049
+ createdAt: detail.created_at,
1050
+ updatedAt: detail.updated_at,
1051
+ platform: detail.platform,
1052
+ history: detail.history ?? [],
1053
+ };
1054
+ }
1055
+
1056
+ app.get<{ Params: { name: string } }>('/api/teams/:name/tasks', async (request) => {
1057
+ try {
1058
+ const tasks = activeTasks(await svc.readTasks(request.params.name));
1059
+ return tasks.map(toTeamTask);
1060
+ } catch { return []; }
1061
+ });
1062
+
1063
+ app.post<{ Params: { name: string }; Body: Record<string, unknown> }>(
1064
+ '/api/teams/:name/tasks', async (request, reply) => {
1065
+ const body = request.body ?? {};
1066
+ // 支持 subject(TeamTask)或 title(内部)
1067
+ const title = (body.subject ?? body.title) as string | undefined;
1068
+ if (!title) return reply.code(400).send({ error: 'title/subject required' });
1069
+ const task = await svc.createTask(request.params.name, {
1070
+ title,
1071
+ description: body.description as string | undefined,
1072
+ assignee: (body.owner ?? body.assignee) as string | null | undefined,
1073
+ status: body.status ? toTaskStatus(body.status as string) : 'todo',
1074
+ });
1075
+ if (task.assignee) {
1076
+ svc.dispatchTask(request.params.name, task).catch((err) =>
1077
+ request.log.warn({ err }, 'dispatchTask failed')
1078
+ );
1079
+ }
1080
+ return toTeamTask(task);
1081
+ }
1082
+ );
1083
+
1084
+ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
1085
+ '/api/teams/:name/tasks/:id', async (request) => {
1086
+ const body = request.body ?? {};
1087
+ const patch: Record<string, unknown> = {};
1088
+ if (body.subject !== undefined) patch.title = body.subject;
1089
+ if (body.title !== undefined) patch.title = body.title;
1090
+ if (body.description !== undefined) patch.description = body.description;
1091
+ if (body.status !== undefined) patch.status = toTaskStatus(body.status as string);
1092
+ if (body.owner !== undefined) patch.assignee = body.owner;
1093
+ if (body.assignee !== undefined) patch.assignee = body.assignee;
1094
+ if (body.result !== undefined) patch.result = body.result;
1095
+ const task = await svc.patchTask(request.params.name, request.params.id, patch);
1096
+ if (patch.assignee && task.assignee) {
1097
+ svc.dispatchTask(request.params.name, task).catch((err) =>
1098
+ request.log.warn({ err }, 'dispatchTask failed')
1099
+ );
1100
+ }
1101
+ return toTeamTask(task);
1102
+ }
1103
+ );
1104
+
1105
+ app.delete<{ Params: { name: string; id: string } }>(
1106
+ '/api/teams/:name/tasks/:id', async (request, reply) => {
1107
+ try {
1108
+ await svc.patchTask(request.params.name, request.params.id, {
1109
+ status: 'done',
1110
+ result: '__deleted__',
1111
+ });
1112
+ return { ok: true };
1113
+ } catch {
1114
+ return reply.code(404).send({ error: 'not found' });
1115
+ }
1116
+ }
1117
+ );
1118
+
1119
+ // ===========================================================================
1120
+ // 协同开关 — PATCH /api/teams/:name/collaboration
1121
+ // ===========================================================================
1122
+
1123
+ app.patch<{ Params: { name: string }; Body: { collaboration: boolean } }>(
1124
+ '/api/teams/:name/collaboration', async (request, reply) => {
1125
+ const { collaboration } = request.body ?? {};
1126
+ if (typeof collaboration !== 'boolean') {
1127
+ return reply.code(400).send({ error: 'collaboration must be boolean' });
1128
+ }
1129
+ try {
1130
+ const updated = await svc.updateTeam(request.params.name, { collaboration });
1131
+ return { ok: true, data: { collaboration: updated.collaboration } };
1132
+ } catch (err) {
1133
+ return reply.code(404).send(reply500(err));
1134
+ }
1135
+ }
1136
+ );
1137
+
1138
+ // ===========================================================================
1139
+ // 定时任务 — 透传 cc-connect heartbeat API
1140
+ // GET /api/teams/:name/heartbeat
1141
+ // POST /api/teams/:name/heartbeat/enable
1142
+ // POST /api/teams/:name/heartbeat/disable
1143
+ // POST /api/teams/:name/heartbeat/pause
1144
+ // POST /api/teams/:name/heartbeat/resume
1145
+ // PATCH /api/teams/:name/heartbeat { interval_mins, only_when_idle, silent }
1146
+ // ===========================================================================
1147
+
1148
+ app.get<{ Params: { name: string } }>('/api/teams/:name/heartbeat', async (request, reply) => {
1149
+ try {
1150
+ const data = await cc.getHeartbeat(request.params.name);
1151
+ return { ok: true, data };
1152
+ } catch (err) { return reply.code(404).send(reply500(err)); }
1153
+ });
1154
+
1155
+ app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/enable', async (request, reply) => {
1156
+ try {
1157
+ await cc.resumeHeartbeat(request.params.name);
1158
+ return { ok: true };
1159
+ } catch (err) { return reply.code(500).send(reply500(err)); }
1160
+ });
1161
+
1162
+ app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/disable', async (request, reply) => {
1163
+ try {
1164
+ await cc.pauseHeartbeat(request.params.name);
1165
+ return { ok: true };
1166
+ } catch (err) { return reply.code(500).send(reply500(err)); }
1167
+ });
1168
+
1169
+ app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/pause', async (request, reply) => {
1170
+ try {
1171
+ await cc.pauseHeartbeat(request.params.name);
1172
+ return { ok: true };
1173
+ } catch (err) { return reply.code(500).send(reply500(err)); }
1174
+ });
1175
+
1176
+ app.post<{ Params: { name: string } }>('/api/teams/:name/heartbeat/resume', async (request, reply) => {
1177
+ try {
1178
+ await cc.resumeHeartbeat(request.params.name);
1179
+ return { ok: true };
1180
+ } catch (err) { return reply.code(500).send(reply500(err)); }
1181
+ });
1182
+
1183
+ app.patch<{ Params: { name: string }; Body: { interval_mins?: number; only_when_idle?: boolean; silent?: boolean } }>(
1184
+ '/api/teams/:name/heartbeat', async (request, reply) => {
1185
+ try {
1186
+ await cc.updateProject(request.params.name, request.body as Record<string, unknown>);
1187
+ const data = await cc.getHeartbeat(request.params.name);
1188
+ return { ok: true, data };
1189
+ } catch (err) { return reply.code(500).send(reply500(err)); }
1190
+ }
1191
+ );
1192
+
1193
+ // ===========================================================================
1194
+ // Harness 列表 — 从 cc-connect projects 提取已用 agent_type,合并固定枚举
1195
+ // GET /api/harnesses
1196
+ // ===========================================================================
1197
+
1198
+ const CC_AGENT_TYPES = [
1199
+ 'claudecode', 'codex', 'cursor', 'gemini', 'iflow',
1200
+ 'kimi', 'devin', 'opencode', 'qoder', 'pi', 'acp', 'tmux',
1201
+ ] as const;
1202
+
1203
+ app.get('/api/harnesses', async () => {
1204
+ try {
1205
+ const projects = await cc.listProjects();
1206
+ const usedTypes = new Set(projects.map((p) => p.agent_type));
1207
+ return CC_AGENT_TYPES.map((type) => ({
1208
+ type,
1209
+ inUse: usedTypes.has(type),
1210
+ }));
1211
+ } catch {
1212
+ // cc-connect 不可达时返回完整枚举列表
1213
+ return CC_AGENT_TYPES.map((type) => ({ type, inUse: false }));
1214
+ }
1215
+ });
1216
+
1217
+ // ===========================================================================
1218
+ // 团队启动验活 — 直接复用 cc-connect heartbeat / project 状态
1219
+ // POST /api/teams/:name/launch → 校验 bindProject 是否存在+在线,返回状态
1220
+ // POST /api/teams/:name/stop → 无需操作(cc-connect 自管理),返回 ok
1221
+ // ===========================================================================
1222
+
1223
+ app.post<{ Params: { name: string } }>('/api/teams/:name/launch', async (request, reply) => {
1224
+ try {
1225
+ const name = request.params.name;
1226
+ let isOnline = false;
1227
+ let projectExists = false;
1228
+ try {
1229
+ const p = await cc.getProject(name);
1230
+ projectExists = true;
1231
+ isOnline = Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
1232
+ } catch { /* project 不存在 */ }
1233
+
1234
+ return {
1235
+ ok: true,
1236
+ data: {
1237
+ teamName: name,
1238
+ bindProject: name,
1239
+ projectExists,
1240
+ isOnline,
1241
+ message: projectExists
1242
+ ? isOnline ? '团队在线' : '团队 project 存在但无活跃连接'
1243
+ : `cc-connect 中不存在 project "${name}"`,
1244
+ },
1245
+ };
1246
+ } catch (err) { return reply.code(404).send(reply500(err)); }
1247
+ });
1248
+
1249
+ app.post<{ Params: { name: string } }>('/api/teams/:name/stop', async (request) => {
1250
+ const name = request.params.name;
1251
+ // Stop = delete project from cc-connect (this is the only way to stop agents)
1252
+ await cc.stopProject(name);
1253
+ // Keep local team metadata intact by not deleting it
1254
+ // The team will show as offline (isAlive: false) on next data fetch
1255
+ return { ok: true };
1256
+ });
1257
+
1258
+ // ===========================================================================
1259
+ // cc-connect setup proxy — QR code & platform binding flows
1260
+ // These endpoints proxy to cc-connect /api/v1/setup/* APIs
1261
+ // ===========================================================================
1262
+
1263
+ // Feishu/Lark setup
1264
+ app.post('/api/setup/feishu/begin', async (request, reply) => {
1265
+ try {
1266
+ const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/begin`, {
1267
+ method: 'POST',
1268
+ headers: {
1269
+ 'Content-Type': 'application/json',
1270
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1271
+ },
1272
+ body: JSON.stringify(request.body ?? {}),
1273
+ })).json();
1274
+ return result;
1275
+ } catch (err) { return reply500(err); }
1276
+ });
1277
+
1278
+ app.post('/api/setup/feishu/poll', async (request, reply) => {
1279
+ try {
1280
+ const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/poll`, {
1281
+ method: 'POST',
1282
+ headers: {
1283
+ 'Content-Type': 'application/json',
1284
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1285
+ },
1286
+ body: JSON.stringify(request.body ?? {}),
1287
+ })).json();
1288
+ return result;
1289
+ } catch (err) { return reply500(err); }
1290
+ });
1291
+
1292
+ app.post('/api/setup/feishu/save', async (request, reply) => {
1293
+ try {
1294
+ const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/feishu/save`, {
1295
+ method: 'POST',
1296
+ headers: {
1297
+ 'Content-Type': 'application/json',
1298
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1299
+ },
1300
+ body: JSON.stringify(request.body ?? {}),
1301
+ })).json();
1302
+ return result;
1303
+ } catch (err) { return reply500(err); }
1304
+ });
1305
+
1306
+ // Weixin setup
1307
+ app.post('/api/setup/weixin/begin', async (request, reply) => {
1308
+ try {
1309
+ const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/begin`, {
1310
+ method: 'POST',
1311
+ headers: {
1312
+ 'Content-Type': 'application/json',
1313
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1314
+ },
1315
+ body: JSON.stringify(request.body ?? {}),
1316
+ })).json();
1317
+ return result;
1318
+ } catch (err) { return reply500(err); }
1319
+ });
1320
+
1321
+ app.post('/api/setup/weixin/poll', async (request, reply) => {
1322
+ try {
1323
+ const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/poll`, {
1324
+ method: 'POST',
1325
+ headers: {
1326
+ 'Content-Type': 'application/json',
1327
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1328
+ },
1329
+ body: JSON.stringify(request.body ?? {}),
1330
+ })).json();
1331
+ return result;
1332
+ } catch (err) { return reply500(err); }
1333
+ });
1334
+
1335
+ app.post('/api/setup/weixin/save', async (request, reply) => {
1336
+ try {
1337
+ const result = await (await fetch(`${runtimeConfig.ccBaseUrl}/api/v1/setup/weixin/save`, {
1338
+ method: 'POST',
1339
+ headers: {
1340
+ 'Content-Type': 'application/json',
1341
+ ...(runtimeConfig.ccToken ? { Authorization: `Bearer ${runtimeConfig.ccToken}` } : {}),
1342
+ },
1343
+ body: JSON.stringify(request.body ?? {}),
1344
+ })).json();
1345
+ return result;
1346
+ } catch (err) { return reply500(err); }
1347
+ });
1348
+
1349
+ // Generic platform add (manual credential form)
1350
+ app.post<{ Params: { name: string }; Body: { type: string; options?: Record<string, unknown>; work_dir?: string; agent_type?: string } }>(
1351
+ '/api/projects/:name/add-platform', async (request, reply) => {
1352
+ try {
1353
+ const result = await cc.createProject(
1354
+ request.params.name,
1355
+ request.body.agent_type ?? 'claudecode',
1356
+ request.body.work_dir ?? '',
1357
+ request.body.type,
1358
+ (request.body.options ?? {}) as Record<string, string>
1359
+ );
1360
+ return result;
1361
+ } catch (err) { return reply500(err); }
1362
+ }
1363
+ );
1364
+
1365
+ // ===========================================================================
1366
+ // 组织图 API — GET /api/graph
1367
+ // 返回 nodes(团队)+ edges(任务 assignee 关系)供前端 Graph 渲染
1368
+ // ===========================================================================
1369
+
1370
+ app.get('/api/graph', async () => {
1371
+ try {
1372
+ const projects = await cc.listProjects();
1373
+ const nodes = projects.map((p) => ({
1374
+ id: p.name,
1375
+ label: p.name,
1376
+ harness: p.agent_type,
1377
+ color: 'blue',
1378
+ collaboration: true,
1379
+ bindProject: p.name,
1380
+ }));
1381
+
1382
+ const edges: { source: string; target: string; taskId: string; taskTitle: string }[] = [];
1383
+ for (const p of projects) {
1384
+ try {
1385
+ const tasks = await svc.readTasks(p.name);
1386
+ for (const task of tasks) {
1387
+ if (task.assignee && task.status !== 'done') {
1388
+ edges.push({
1389
+ source: p.name,
1390
+ target: task.assignee,
1391
+ taskId: task.id,
1392
+ taskTitle: task.title,
1393
+ });
1394
+ }
1395
+ }
1396
+ } catch { /* skip */ }
1397
+ }
1398
+
1399
+ return { ok: true, data: { nodes, edges } };
1400
+ } catch (err) { return reply500(err); }
1401
+ });
1402
+
1403
+ // ===========================================================================
1404
+ // MCP Server — hermit-tasks (MCP over HTTP: SSE + JSON-RPC)
1405
+ //
1406
+ // Claude Code / Qoder 等 agent 通过 MCP 协议读取和更新任务。
1407
+ // MCP 配置在创建团队时自动注入到 workDir/.claude/settings.json。
1408
+ //
1409
+ // Tools:
1410
+ // list_tasks(team_slug)
1411
+ // claim_task(team_slug, task_id)
1412
+ // complete_task(team_slug, task_id, result?)
1413
+ // create_task(team_slug, title, description?, assignee?)
1414
+ // ===========================================================================
1415
+
1416
+ const MCP_TOOLS = [
1417
+ {
1418
+ name: 'list_tasks',
1419
+ description: '列出指定团队的任务看板',
1420
+ inputSchema: {
1421
+ type: 'object',
1422
+ properties: {
1423
+ team_slug: { type: 'string', description: '团队 slug' },
1424
+ },
1425
+ required: ['team_slug'],
1426
+ },
1427
+ },
1428
+ {
1429
+ name: 'claim_task',
1430
+ description: '认领任务(状态改为 doing)',
1431
+ inputSchema: {
1432
+ type: 'object',
1433
+ properties: {
1434
+ team_slug: { type: 'string', description: '团队 slug' },
1435
+ task_id: { type: 'string', description: '任务 ID' },
1436
+ },
1437
+ required: ['team_slug', 'task_id'],
1438
+ },
1439
+ },
1440
+ {
1441
+ name: 'complete_task',
1442
+ description: '标记任务完成(状态改为 done),可写入结果摘要',
1443
+ inputSchema: {
1444
+ type: 'object',
1445
+ properties: {
1446
+ team_slug: { type: 'string', description: '团队 slug' },
1447
+ task_id: { type: 'string', description: '任务 ID' },
1448
+ result: { type: 'string', description: '完成结果摘要(可选)' },
1449
+ },
1450
+ required: ['team_slug', 'task_id'],
1451
+ },
1452
+ },
1453
+ {
1454
+ name: 'create_task',
1455
+ description: '创建任务,可分配给其他团队',
1456
+ inputSchema: {
1457
+ type: 'object',
1458
+ properties: {
1459
+ team_slug: { type: 'string', description: '创建任务的团队 slug' },
1460
+ title: { type: 'string', description: '任务标题' },
1461
+ description: { type: 'string', description: '任务描述(可选)' },
1462
+ assignee: { type: 'string', description: '分配给哪个团队的 slug(可选)' },
1463
+ },
1464
+ required: ['team_slug', 'title'],
1465
+ },
1466
+ },
1467
+ ];
1468
+
1469
+ /** 执行 MCP tool,返回 content array */
1470
+ async function executeMcpTool(
1471
+ toolName: string,
1472
+ args: Record<string, string>
1473
+ ): Promise<{ type: string; text: string }[]> {
1474
+ const text = async (result: unknown) => [{ type: 'text', text: JSON.stringify(result, null, 2) }];
1475
+
1476
+ if (toolName === 'list_tasks') {
1477
+ const tasks = await svc.readTasks(args.team_slug);
1478
+ return text(tasks);
1479
+ }
1480
+
1481
+ if (toolName === 'claim_task') {
1482
+ const task = await svc.patchTask(args.team_slug, args.task_id, { status: 'doing' });
1483
+ return text(task);
1484
+ }
1485
+
1486
+ if (toolName === 'complete_task') {
1487
+ const patch: Record<string, unknown> = { status: 'done' };
1488
+ if (args.result) patch.result = args.result;
1489
+ const task = await svc.patchTask(args.team_slug, args.task_id, patch);
1490
+ return text(task);
1491
+ }
1492
+
1493
+ if (toolName === 'create_task') {
1494
+ const task = await svc.createTask(args.team_slug, {
1495
+ title: args.title,
1496
+ description: args.description,
1497
+ assignee: args.assignee ?? null,
1498
+ });
1499
+ if (task.assignee) {
1500
+ svc.dispatchTask(args.team_slug, task).catch(() => { /* best-effort */ });
1501
+ }
1502
+ return text(task);
1503
+ }
1504
+
1505
+ throw new Error(`Unknown tool: ${toolName}`);
1506
+ }
1507
+
1508
+ // GET /mcp — SSE 端点(MCP over HTTP-SSE transport)
1509
+ app.get('/mcp', (request, reply) => {
1510
+ reply.raw.writeHead(200, {
1511
+ 'Content-Type': 'text/event-stream; charset=utf-8',
1512
+ 'Cache-Control': 'no-cache, no-transform',
1513
+ Connection: 'keep-alive',
1514
+ 'X-Accel-Buffering': 'no',
1515
+ });
1516
+
1517
+ // MCP initialize 握手
1518
+ const endpoint = `http://${request.hostname}/mcp`;
1519
+ reply.raw.write(
1520
+ `event: endpoint\ndata: ${JSON.stringify({ endpoint })}\n\n`
1521
+ );
1522
+
1523
+ const ka = setInterval(() => {
1524
+ try { reply.raw.write(': keep-alive\n\n'); } catch { clearInterval(ka); }
1525
+ }, 15000);
1526
+
1527
+ request.raw.on('close', () => clearInterval(ka));
1528
+ return reply.hijack();
1529
+ });
1530
+
1531
+ // POST /mcp — JSON-RPC 请求处理
1532
+ app.post<{ Body: { jsonrpc?: string; id?: unknown; method?: string; params?: Record<string, unknown> } }>(
1533
+ '/mcp',
1534
+ async (request, reply) => {
1535
+ const { id, method, params = {} } = request.body ?? {};
1536
+
1537
+ // MCP initialize
1538
+ if (method === 'initialize') {
1539
+ return {
1540
+ jsonrpc: '2.0',
1541
+ id,
1542
+ result: {
1543
+ protocolVersion: '2024-11-05',
1544
+ capabilities: { tools: {} },
1545
+ serverInfo: { name: 'hermit-tasks', version: '1.0.0' },
1546
+ },
1547
+ };
1548
+ }
1549
+
1550
+ // MCP tools/list
1551
+ if (method === 'tools/list') {
1552
+ return { jsonrpc: '2.0', id, result: { tools: MCP_TOOLS } };
1553
+ }
1554
+
1555
+ // MCP tools/call
1556
+ if (method === 'tools/call') {
1557
+ const toolName = params.name as string;
1558
+ const toolArgs = (params.arguments ?? {}) as Record<string, string>;
1559
+ try {
1560
+ const content = await executeMcpTool(toolName, toolArgs);
1561
+ return { jsonrpc: '2.0', id, result: { content } };
1562
+ } catch (err) {
1563
+ return {
1564
+ jsonrpc: '2.0',
1565
+ id,
1566
+ result: {
1567
+ content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
1568
+ isError: true,
1569
+ },
1570
+ };
1571
+ }
1572
+ }
1573
+
1574
+ // notifications/initialized — 无需响应
1575
+ if (method === 'notifications/initialized') {
1576
+ return reply.code(204).send();
1577
+ }
1578
+
1579
+ return reply.code(400).send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
1580
+ }
1581
+ );
1582
+
1583
+ // ===========================================================================
1584
+ // Hermit 主仓 UI 首屏强依赖的几个 stub(占位实现)
1585
+ // ===========================================================================
1586
+
1587
+ // hermit getAppVersion 期望返回字符串(不是数组),通配 stub 给的 [] 会让 JSON.parse 后类型对不上
1588
+ app.get('/api/version', async () => pkg.version);
1589
+
1590
+ // GET /api/update/check — 检查是否有新版本
1591
+ const updateService = new UpdateService();
1592
+ app.get('/api/update/check', async () => updateService.checkForUpdates());
1593
+
1594
+ // POST /api/update/apply — 应用更新(SSE 推送进度)
1595
+ app.post('/api/update/apply', async (request, reply) => {
1596
+ reply.raw.writeHead(200, {
1597
+ 'Content-Type': 'text/event-stream',
1598
+ 'Cache-Control': 'no-cache',
1599
+ Connection: 'keep-alive',
1600
+ });
1601
+
1602
+ const send = (data: unknown) => {
1603
+ reply.raw.write(`data: ${JSON.stringify(data)}\n\n`);
1604
+ };
1605
+
1606
+ try {
1607
+ await updateService.applyUpdate((progress) => {
1608
+ send(progress);
1609
+ if (progress.phase === 'completed' || progress.phase === 'error') {
1610
+ reply.raw.end();
1611
+ }
1612
+ });
1613
+ } catch (err: unknown) {
1614
+ send({
1615
+ phase: 'error',
1616
+ message: 'Update failed',
1617
+ error: err instanceof Error ? err.message : String(err),
1618
+ });
1619
+ reply.raw.end();
1620
+ }
1621
+ });
1622
+
1623
+ // 主仓有"recent projects"概念,mvp 不实现,返回空
1624
+ app.get('/api/dashboard/recent-projects', async () => ({
1625
+ recentProjects: [],
1626
+ pinnedSessions: [],
1627
+ }));
1628
+
1629
+ app.get('/api/projects', async () => []);
1630
+ app.get('/api/repository-groups', async () => []);
1631
+
1632
+ app.get('/api/notifications/unread-count', async () => ({ count: 0 }));
1633
+ app.get('/api/notifications', async () => []);
1634
+
1635
+ // CLI installer / runtime / context 相关查询(主仓启动时会调,mvp 没这些概念)
1636
+ app.get('/api/cli/status', async () => ({
1637
+ installed: true,
1638
+ version: 'cc-connect',
1639
+ path: null,
1640
+ }));
1641
+ app.get('/api/contexts', async () => []);
1642
+ app.get('/api/contexts/active', async () => null);
1643
+
1644
+ const DEFAULT_APP_CONFIG = {
1645
+ notifications: {
1646
+ enabled: true,
1647
+ soundEnabled: true,
1648
+ ignoredRegex: [] as string[],
1649
+ ignoredRepositories: [] as string[],
1650
+ snoozedUntil: null as number | null,
1651
+ snoozeMinutes: 30,
1652
+ includeSubagentErrors: false,
1653
+ notifyOnLeadInbox: false,
1654
+ notifyOnUserInbox: true,
1655
+ notifyOnClarifications: true,
1656
+ notifyOnStatusChange: true,
1657
+ notifyOnTaskComments: true,
1658
+ notifyOnTaskCreated: true,
1659
+ notifyOnAllTasksCompleted: true,
1660
+ notifyOnCrossTeamMessage: true,
1661
+ notifyOnTeamLaunched: true,
1662
+ notifyOnToolApproval: true,
1663
+ autoResumeOnRateLimit: false,
1664
+ statusChangeOnlySolo: false,
1665
+ statusChangeStatuses: ['in_progress', 'completed'] as string[],
1666
+ triggers: [] as unknown[],
1667
+ },
1668
+ general: {
1669
+ launchAtLogin: false,
1670
+ showDockIcon: true,
1671
+ theme: 'dark' as 'dark' | 'light' | 'system',
1672
+ defaultTab: 'dashboard' as 'dashboard' | 'last-session',
1673
+ multimodelEnabled: false,
1674
+ claudeRootPath: null as string | null,
1675
+ agentLanguage: 'system',
1676
+ autoExpandAIGroups: false,
1677
+ useNativeTitleBar: false,
1678
+ telemetryEnabled: true,
1679
+ },
1680
+ providerConnections: {
1681
+ anthropic: {
1682
+ authMode: 'auto' as 'auto' | 'oauth' | 'api_key',
1683
+ fastModeDefault: false,
1684
+ },
1685
+ codex: {
1686
+ preferredAuthMode: 'auto' as 'auto' | 'chatgpt' | 'api_key',
1687
+ },
1688
+ },
1689
+ runtime: {
1690
+ providerBackends: {
1691
+ gemini: 'auto' as 'auto' | 'api' | 'cli-sdk',
1692
+ codex: 'codex-native' as 'codex-native',
1693
+ },
1694
+ },
1695
+ display: {
1696
+ showTimestamps: true,
1697
+ compactMode: false,
1698
+ syntaxHighlighting: true,
1699
+ },
1700
+ sessions: {
1701
+ pinnedSessions: {} as Record<string, { sessionId: string; pinnedAt: number }[]>,
1702
+ hiddenSessions: {} as Record<string, { sessionId: string; hiddenAt: number }[]>,
1703
+ },
1704
+ claudeEnv: {} as Record<string, string>,
1705
+ };
1706
+
1707
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
1708
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1709
+ }
1710
+
1711
+ function mergeConfigDefaults<T extends Record<string, unknown>>(defaults: T, value: unknown): T {
1712
+ if (!isPlainObject(value)) {
1713
+ return defaults;
1714
+ }
1715
+ const output: Record<string, unknown> = { ...defaults };
1716
+ for (const [key, entry] of Object.entries(value)) {
1717
+ const defaultEntry = output[key];
1718
+ output[key] = isPlainObject(defaultEntry) ? mergeConfigDefaults(defaultEntry, entry) : entry;
1719
+ }
1720
+ return output as T;
1721
+ }
1722
+
1723
+ function readAppConfig() {
1724
+ try {
1725
+ if (_existsSync2(HERMIT_APP_CONFIG_FILE)) {
1726
+ const raw = JSON.parse(readFileSync(HERMIT_APP_CONFIG_FILE, 'utf-8')) as unknown;
1727
+ return mergeConfigDefaults(DEFAULT_APP_CONFIG, raw);
1728
+ }
1729
+ } catch (err) {
1730
+ app.log.warn({ err }, 'failed to read app config, using defaults');
1731
+ }
1732
+ return DEFAULT_APP_CONFIG;
1733
+ }
1734
+
1735
+ function writeAppConfig(config: typeof DEFAULT_APP_CONFIG): typeof DEFAULT_APP_CONFIG {
1736
+ mkdirSync(HERMIT_HOME, { recursive: true });
1737
+ writeFileSync(HERMIT_APP_CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
1738
+ return config;
1739
+ }
1740
+
1741
+ app.get('/api/config', async () => ({
1742
+ success: true,
1743
+ data: readAppConfig(),
1744
+ }));
1745
+
1746
+ app.post<{ Body: { section?: unknown; data?: unknown } }>('/api/config/update', async (request) => {
1747
+ const section = typeof request.body?.section === 'string' ? request.body.section : '';
1748
+ const patch = isPlainObject(request.body?.data) ? request.body.data : {};
1749
+ const current = readAppConfig();
1750
+ const next = section
1751
+ ? mergeConfigDefaults(current, {
1752
+ [section]: {
1753
+ ...(isPlainObject((current as Record<string, unknown>)[section])
1754
+ ? ((current as Record<string, unknown>)[section] as Record<string, unknown>)
1755
+ : {}),
1756
+ ...patch,
1757
+ },
1758
+ })
1759
+ : current;
1760
+ return {
1761
+ success: true,
1762
+ data: writeAppConfig(next),
1763
+ };
1764
+ });
1765
+
1766
+ app.get('/api/config/triggers', async () => []);
1767
+
1768
+ const CRON_ZERO_TIME_PREFIX = '0001-01-01T00:00:00';
1769
+ const DEFAULT_SCHEDULE_TIMEZONE =
1770
+ Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai';
1771
+ const DEFAULT_SCHEDULE_WARMUP_MINUTES = 15;
1772
+ const DEFAULT_SCHEDULE_MAX_TURNS = 50;
1773
+ const DEFAULT_SCHEDULE_MAX_CONSECUTIVE_FAILURES = 3;
1774
+
1775
+ type InMemoryScheduleRun = {
1776
+ id: string;
1777
+ scheduleId: string;
1778
+ teamName: string;
1779
+ status: 'pending' | 'warming_up' | 'warm' | 'running' | 'completed' | 'failed' | 'failed_interrupted' | 'cancelled';
1780
+ scheduledFor: string;
1781
+ startedAt: string;
1782
+ warmUpCompletedAt?: string;
1783
+ executionStartedAt?: string;
1784
+ completedAt?: string;
1785
+ durationMs?: number;
1786
+ exitCode?: number | null;
1787
+ error?: string;
1788
+ retryCount: number;
1789
+ summary?: string;
1790
+ };
1791
+
1792
+ const scheduleRunsById = new Map<string, InMemoryScheduleRun[]>();
1793
+ const scheduleRunLogsByKey = new Map<string, { stdout: string; stderr: string }>();
1794
+
1795
+ function makeScheduleRunLogKey(scheduleId: string, runId: string): string {
1796
+ return `${scheduleId}:${runId}`;
1797
+ }
1798
+
1799
+ function isNonEmptyString(value: unknown): value is string {
1800
+ return typeof value === 'string' && value.trim().length > 0;
1801
+ }
1802
+
1803
+ function normalizeCronLastRun(value: unknown): string | undefined {
1804
+ if (typeof value !== 'string' || value.length === 0) return undefined;
1805
+ if (value.startsWith(CRON_ZERO_TIME_PREFIX)) return undefined;
1806
+ return value;
1807
+ }
1808
+
1809
+ function buildFallbackSessionKey(teamName: string): string {
1810
+ return `hermit:${teamName}:session`;
1811
+ }
1812
+
1813
+ async function waitForHarnessBridgeConnected(timeoutMs = HARNESS_BRIDGE_CONNECT_TIMEOUT_MS): Promise<void> {
1814
+ if (bridge.connected) return;
1815
+ bridge.start();
1816
+ if (bridge.connected) return;
1817
+
1818
+ await new Promise<void>((resolve, reject) => {
1819
+ const timer = setTimeout(() => {
1820
+ cleanup();
1821
+ reject(new Error('cc-connect Bridge 连接超时,无法发送到 harness'));
1822
+ }, timeoutMs);
1823
+
1824
+ const onConnected = () => {
1825
+ cleanup();
1826
+ resolve();
1827
+ };
1828
+
1829
+ const cleanup = () => {
1830
+ clearTimeout(timer);
1831
+ bridge.off('connected', onConnected);
1832
+ };
1833
+
1834
+ bridge.on('connected', onConnected);
1835
+ });
1836
+ }
1837
+
1838
+ async function sendHarnessMessageViaBridge(params: {
1839
+ teamName: string;
1840
+ text: string;
1841
+ sessionKey?: string;
1842
+ msgId?: string;
1843
+ }): Promise<string> {
1844
+ await waitForHarnessBridgeConnected();
1845
+
1846
+ const sessionKey = params.sessionKey?.trim() || buildFallbackSessionKey(params.teamName);
1847
+ bridge.sendUserMessage({
1848
+ sessionKey,
1849
+ userId: 'hermit-user',
1850
+ userName: 'User',
1851
+ content: params.text,
1852
+ msgId: params.msgId,
1853
+ project: params.teamName,
1854
+ });
1855
+ return sessionKey;
1856
+ }
1857
+
1858
+ async function resolveTeamWorkDirs(teamNames: string[]): Promise<Map<string, string>> {
1859
+ const uniqueTeamNames = [...new Set(teamNames.filter((name) => name.trim().length > 0))];
1860
+ const results = new Map<string, string>();
1861
+
1862
+ await Promise.all(uniqueTeamNames.map(async (teamName) => {
1863
+ let cwd = '';
1864
+ try {
1865
+ const meta = await svc.readTeamManifest(teamName);
1866
+ if (typeof meta.workDir === 'string') {
1867
+ cwd = meta.workDir.trim();
1868
+ }
1869
+ } catch {
1870
+ // ignore
1871
+ }
1872
+
1873
+ if (!cwd) {
1874
+ try {
1875
+ const detail = await cc.getProject(teamName);
1876
+ if (typeof detail.work_dir === 'string') {
1877
+ cwd = detail.work_dir.trim();
1878
+ }
1879
+ } catch {
1880
+ // ignore
1881
+ }
1882
+ }
1883
+
1884
+ results.set(teamName, cwd);
1885
+ }));
1886
+
1887
+ return results;
1888
+ }
1889
+
1890
+ function mapCronJobToSchedule(
1891
+ cronJob: {
1892
+ id: string;
1893
+ project: string;
1894
+ cron_expr: string;
1895
+ prompt: string;
1896
+ description?: string;
1897
+ enabled: boolean;
1898
+ created_at: string;
1899
+ last_run?: string;
1900
+ },
1901
+ cwd: string
1902
+ ): {
1903
+ id: string;
1904
+ teamName: string;
1905
+ label?: string;
1906
+ cronExpression: string;
1907
+ timezone: string;
1908
+ status: 'active' | 'paused' | 'disabled';
1909
+ warmUpMinutes: number;
1910
+ maxConsecutiveFailures: number;
1911
+ consecutiveFailures: number;
1912
+ maxTurns: number;
1913
+ createdAt: string;
1914
+ updatedAt: string;
1915
+ lastRunAt?: string;
1916
+ launchConfig: { cwd: string; prompt: string };
1917
+ } {
1918
+ const lastRunAt = normalizeCronLastRun(cronJob.last_run);
1919
+ const status: 'active' | 'paused' = cronJob.enabled ? 'active' : 'paused';
1920
+
1921
+ return {
1922
+ id: cronJob.id,
1923
+ teamName: cronJob.project,
1924
+ label: isNonEmptyString(cronJob.description) ? cronJob.description.trim() : undefined,
1925
+ cronExpression: cronJob.cron_expr,
1926
+ timezone: DEFAULT_SCHEDULE_TIMEZONE,
1927
+ status,
1928
+ warmUpMinutes: DEFAULT_SCHEDULE_WARMUP_MINUTES,
1929
+ maxConsecutiveFailures: DEFAULT_SCHEDULE_MAX_CONSECUTIVE_FAILURES,
1930
+ consecutiveFailures: 0,
1931
+ maxTurns: DEFAULT_SCHEDULE_MAX_TURNS,
1932
+ createdAt: cronJob.created_at,
1933
+ updatedAt: lastRunAt ?? cronJob.created_at,
1934
+ lastRunAt,
1935
+ launchConfig: {
1936
+ cwd,
1937
+ prompt: cronJob.prompt,
1938
+ },
1939
+ };
1940
+ }
1941
+
1942
+ function normalizeScheduleRouteId(id: string): string {
1943
+ const trimmed = id.trim();
1944
+ if (trimmed.startsWith('schedule:')) {
1945
+ return trimmed.slice('schedule:'.length);
1946
+ }
1947
+ if (trimmed.startsWith('SCH-')) {
1948
+ return trimmed.slice('SCH-'.length);
1949
+ }
1950
+ return trimmed;
1951
+ }
1952
+
1953
+ function findCronJobByRouteId<
1954
+ T extends {
1955
+ id: string;
1956
+ },
1957
+ >(jobs: T[], id: string): T | undefined {
1958
+ const normalized = normalizeScheduleRouteId(id);
1959
+ const exact = jobs.find((job) => job.id === normalized || job.id === id);
1960
+ if (exact) return exact;
1961
+
1962
+ const prefixMatches = jobs.filter((job) => job.id.startsWith(normalized));
1963
+ return prefixMatches.length === 1 ? prefixMatches[0] : undefined;
1964
+ }
1965
+
1966
+ function clearScheduleRuntimeState(scheduleId: string): void {
1967
+ scheduleRunsById.delete(scheduleId);
1968
+ for (const key of [...scheduleRunLogsByKey.keys()]) {
1969
+ if (key.startsWith(`${scheduleId}:`)) {
1970
+ scheduleRunLogsByKey.delete(key);
1971
+ }
1972
+ }
1973
+ }
1974
+
1975
+ app.get('/api/schedules', async () => {
1976
+ try {
1977
+ const jobs = await cc.listCronJobs();
1978
+ if (jobs.length === 0) return [];
1979
+ const workDirMap = await resolveTeamWorkDirs(jobs.map((job) => job.project));
1980
+ return jobs.map((job) => mapCronJobToSchedule(job, workDirMap.get(job.project) ?? ''));
1981
+ } catch (err) {
1982
+ app.log.warn({ err }, 'list schedules from cc-connect failed');
1983
+ return [];
1984
+ }
1985
+ });
1986
+
1987
+ app.get<{ Params: { id: string } }>('/api/schedules/:id', async (request) => {
1988
+ try {
1989
+ const jobs = await cc.listCronJobs();
1990
+ const job = jobs.find((item) => item.id === request.params.id);
1991
+ if (!job) return null;
1992
+ const workDirMap = await resolveTeamWorkDirs([job.project]);
1993
+ return mapCronJobToSchedule(job, workDirMap.get(job.project) ?? '');
1994
+ } catch (err) {
1995
+ app.log.warn({ err, scheduleId: request.params.id }, 'get schedule from cc-connect failed');
1996
+ return null;
1997
+ }
1998
+ });
1999
+
2000
+ app.post<{ Body: Record<string, unknown> }>('/api/schedules', async (request, reply) => {
2001
+ try {
2002
+ const body = request.body ?? {};
2003
+ const teamName = typeof body.teamName === 'string' ? body.teamName.trim() : '';
2004
+ const cronExpression =
2005
+ typeof body.cronExpression === 'string' ? body.cronExpression.trim() : '';
2006
+ const label = typeof body.label === 'string' ? body.label.trim() : '';
2007
+ const maxTurns =
2008
+ typeof body.maxTurns === 'number' && Number.isFinite(body.maxTurns)
2009
+ ? Math.max(1, Math.floor(body.maxTurns))
2010
+ : DEFAULT_SCHEDULE_MAX_TURNS;
2011
+
2012
+ const launchConfig =
2013
+ body.launchConfig && typeof body.launchConfig === 'object' && !Array.isArray(body.launchConfig)
2014
+ ? (body.launchConfig as Record<string, unknown>)
2015
+ : {};
2016
+ const prompt = typeof launchConfig.prompt === 'string' ? launchConfig.prompt.trim() : '';
2017
+ const cwd = typeof launchConfig.cwd === 'string' ? launchConfig.cwd.trim() : '';
2018
+ const sessionKey = typeof launchConfig.session_key === 'string' && launchConfig.session_key.trim().length > 0
2019
+ ? launchConfig.session_key.trim()
2020
+ : buildFallbackSessionKey(teamName);
2021
+
2022
+ if (!teamName || !cronExpression || !prompt) {
2023
+ return reply
2024
+ .code(400)
2025
+ .send({ error: 'teamName、cronExpression、launchConfig.prompt 不能为空' });
2026
+ }
2027
+
2028
+ const created = await cc.createCronJob({
2029
+ project: teamName,
2030
+ session_key: sessionKey,
2031
+ cron_expr: cronExpression,
2032
+ prompt,
2033
+ description: label || undefined,
2034
+ enabled: true,
2035
+ timeout_mins: maxTurns,
2036
+ });
2037
+
2038
+ const schedule = mapCronJobToSchedule(created, cwd);
2039
+ broadcastSse('schedule:change', {
2040
+ type: 'schedule-updated',
2041
+ scheduleId: schedule.id,
2042
+ teamName: schedule.teamName,
2043
+ detail: 'created',
2044
+ });
2045
+ return schedule;
2046
+ } catch (err) {
2047
+ return reply500(err);
2048
+ }
2049
+ });
2050
+
2051
+ app.patch<{ Params: { id: string }; Body: Record<string, unknown> }>(
2052
+ '/api/schedules/:id',
2053
+ async (request, reply) => {
2054
+ try {
2055
+ const jobs = await cc.listCronJobs();
2056
+ const existing = jobs.find((item) => item.id === request.params.id);
2057
+ if (!existing) {
2058
+ return reply.code(404).send({ error: 'Schedule not found' });
2059
+ }
2060
+
2061
+ const patchBody = request.body ?? {};
2062
+ const patch: Record<string, unknown> = {};
2063
+ if (typeof patchBody.label === 'string') {
2064
+ patch.description = patchBody.label.trim();
2065
+ }
2066
+ if (typeof patchBody.cronExpression === 'string') {
2067
+ patch.cron_expr = patchBody.cronExpression.trim();
2068
+ }
2069
+ const launchConfig =
2070
+ patchBody.launchConfig &&
2071
+ typeof patchBody.launchConfig === 'object' &&
2072
+ !Array.isArray(patchBody.launchConfig)
2073
+ ? (patchBody.launchConfig as Record<string, unknown>)
2074
+ : null;
2075
+ if (launchConfig && typeof launchConfig.prompt === 'string') {
2076
+ patch.prompt = launchConfig.prompt.trim();
2077
+ }
2078
+ if (typeof patchBody.maxTurns === 'number' && Number.isFinite(patchBody.maxTurns)) {
2079
+ patch.timeout_mins = Math.max(1, Math.floor(patchBody.maxTurns));
2080
+ }
2081
+
2082
+ const updated = Object.keys(patch).length
2083
+ ? await cc.updateCronJob(request.params.id, patch)
2084
+ : existing;
2085
+
2086
+ const workDirMap = await resolveTeamWorkDirs([updated.project]);
2087
+ const schedule = mapCronJobToSchedule(updated, workDirMap.get(updated.project) ?? '');
2088
+ broadcastSse('schedule:change', {
2089
+ type: 'schedule-updated',
2090
+ scheduleId: schedule.id,
2091
+ teamName: schedule.teamName,
2092
+ detail: 'updated',
2093
+ });
2094
+ return schedule;
2095
+ } catch (err) {
2096
+ return reply500(err);
2097
+ }
2098
+ }
2099
+ );
2100
+
2101
+ app.delete<{ Params: { id: string } }>('/api/schedules/:id', async (request, reply) => {
2102
+ const requestedId = request.params.id;
2103
+ const normalizedId = normalizeScheduleRouteId(requestedId);
2104
+ let resolvedId = normalizedId;
2105
+ let resolvedTeamName = '';
2106
+
2107
+ try {
2108
+ let jobs: Awaited<ReturnType<typeof cc.listCronJobs>> = [];
2109
+ let listedJobs = false;
2110
+ try {
2111
+ jobs = await cc.listCronJobs();
2112
+ listedJobs = true;
2113
+ } catch (listErr) {
2114
+ request.log.warn({ err: listErr, scheduleId: requestedId }, 'list cron jobs before delete failed');
2115
+ }
2116
+ const target = findCronJobByRouteId(jobs, requestedId);
2117
+ if (target) {
2118
+ resolvedId = target.id;
2119
+ resolvedTeamName = 'project' in target && typeof target.project === 'string' ? target.project : '';
2120
+ } else if (
2121
+ listedJobs &&
2122
+ !jobs.some((job) => job.id === normalizedId || job.id.startsWith(normalizedId))
2123
+ ) {
2124
+ clearScheduleRuntimeState(normalizedId);
2125
+ broadcastSse('schedule:change', {
2126
+ type: 'schedule-updated',
2127
+ scheduleId: normalizedId,
2128
+ teamName: '',
2129
+ detail: 'deleted',
2130
+ });
2131
+ return {};
2132
+ }
2133
+
2134
+ await cc.deleteCronJob(resolvedId);
2135
+ clearScheduleRuntimeState(resolvedId);
2136
+ broadcastSse('schedule:change', {
2137
+ type: 'schedule-updated',
2138
+ scheduleId: resolvedId,
2139
+ teamName: resolvedTeamName,
2140
+ detail: 'deleted',
2141
+ });
2142
+ return {};
2143
+ } catch (err) {
2144
+ try {
2145
+ const jobs = await cc.listCronJobs();
2146
+ const stillExists = Boolean(findCronJobByRouteId(jobs, requestedId));
2147
+ if (!stillExists) {
2148
+ clearScheduleRuntimeState(resolvedId);
2149
+ broadcastSse('schedule:change', {
2150
+ type: 'schedule-updated',
2151
+ scheduleId: resolvedId,
2152
+ teamName: resolvedTeamName,
2153
+ detail: 'deleted',
2154
+ });
2155
+ return {};
2156
+ }
2157
+ } catch (verifyErr) {
2158
+ request.log.warn({ err: verifyErr, scheduleId: requestedId }, 'verify cron delete failed');
2159
+ }
2160
+ return reply.code(500).send(reply500(err));
2161
+ }
2162
+ });
2163
+
2164
+ app.post<{ Params: { id: string } }>('/api/schedules/:id/pause', async (request, reply) => {
2165
+ try {
2166
+ const jobs = await cc.listCronJobs();
2167
+ const current = jobs.find((item) => item.id === request.params.id);
2168
+ if (current) {
2169
+ try {
2170
+ await cc.sendMessage(
2171
+ current.project,
2172
+ current.session_key || buildFallbackSessionKey(current.project),
2173
+ '/stop'
2174
+ );
2175
+ } catch (err) {
2176
+ request.log.warn({ err, scheduleId: request.params.id }, 'send /stop for cron failed');
2177
+ }
2178
+ }
2179
+ const updated = await cc.updateCronJob(request.params.id, { enabled: false });
2180
+ broadcastSse('schedule:change', {
2181
+ type: 'schedule-paused',
2182
+ scheduleId: request.params.id,
2183
+ teamName: updated.project,
2184
+ detail: 'paused',
2185
+ });
2186
+ return {};
2187
+ } catch (err) {
2188
+ return reply500(err);
2189
+ }
2190
+ });
2191
+
2192
+ app.post<{ Params: { id: string } }>('/api/schedules/:id/resume', async (request, reply) => {
2193
+ try {
2194
+ const updated = await cc.updateCronJob(request.params.id, { enabled: true });
2195
+ broadcastSse('schedule:change', {
2196
+ type: 'schedule-updated',
2197
+ scheduleId: request.params.id,
2198
+ teamName: updated.project,
2199
+ detail: 'resumed',
2200
+ });
2201
+ return {};
2202
+ } catch (err) {
2203
+ return reply500(err);
2204
+ }
2205
+ });
2206
+
2207
+ app.post<{ Params: { id: string } }>('/api/schedules/:id/trigger', async (request, reply) => {
2208
+ try {
2209
+ const jobs = await cc.listCronJobs();
2210
+ const job = jobs.find((item) => item.id === request.params.id);
2211
+ if (!job) {
2212
+ return reply.code(404).send({ error: 'Schedule not found' });
2213
+ }
2214
+ const nowIso = new Date().toISOString();
2215
+ const runId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2216
+ let run: InMemoryScheduleRun;
2217
+
2218
+ try {
2219
+ await cc.sendMessage(job.project, job.session_key || buildFallbackSessionKey(job.project), job.prompt);
2220
+ run = {
2221
+ id: runId,
2222
+ scheduleId: job.id,
2223
+ teamName: job.project,
2224
+ status: 'running',
2225
+ scheduledFor: nowIso,
2226
+ startedAt: nowIso,
2227
+ executionStartedAt: nowIso,
2228
+ retryCount: 0,
2229
+ summary: 'Triggered via Hermit; waiting for agent runtime',
2230
+ };
2231
+ scheduleRunLogsByKey.set(makeScheduleRunLogKey(job.id, runId), {
2232
+ stdout: `Triggered at ${nowIso}`,
2233
+ stderr: '',
2234
+ });
2235
+ } catch (error) {
2236
+ run = {
2237
+ id: runId,
2238
+ scheduleId: job.id,
2239
+ teamName: job.project,
2240
+ status: 'failed',
2241
+ scheduledFor: nowIso,
2242
+ startedAt: nowIso,
2243
+ executionStartedAt: nowIso,
2244
+ completedAt: nowIso,
2245
+ durationMs: 0,
2246
+ exitCode: 1,
2247
+ retryCount: 0,
2248
+ error: error instanceof Error ? error.message : String(error),
2249
+ summary: 'Trigger failed',
2250
+ };
2251
+ scheduleRunLogsByKey.set(makeScheduleRunLogKey(job.id, runId), {
2252
+ stdout: '',
2253
+ stderr: run.error ?? 'Trigger failed',
2254
+ });
2255
+ }
2256
+
2257
+ const previousRuns = scheduleRunsById.get(job.id) ?? [];
2258
+ scheduleRunsById.set(job.id, [run, ...previousRuns].slice(0, 100));
2259
+ broadcastSse('schedule:change', {
2260
+ type: run.status === 'failed' ? 'run-failed' : 'run-started',
2261
+ scheduleId: job.id,
2262
+ teamName: job.project,
2263
+ detail: run.status,
2264
+ });
2265
+ return run;
2266
+ } catch (err) {
2267
+ return reply500(err);
2268
+ }
2269
+ });
2270
+
2271
+ app.get<{ Params: { id: string } }>('/api/schedules/:id/runs', async (request) => {
2272
+ const scheduleId = request.params.id;
2273
+ const runs = scheduleRunsById.get(scheduleId) ?? [];
2274
+ if (runs.length > 0) {
2275
+ return runs;
2276
+ }
2277
+
2278
+ try {
2279
+ const jobs = await cc.listCronJobs();
2280
+ const job = jobs.find((item) => item.id === scheduleId);
2281
+ const lastRunAt = normalizeCronLastRun(job?.last_run);
2282
+ if (!job || !lastRunAt) return [];
2283
+ return [{
2284
+ id: `last-${scheduleId}`,
2285
+ scheduleId,
2286
+ teamName: job.project,
2287
+ status: 'completed',
2288
+ scheduledFor: lastRunAt,
2289
+ startedAt: lastRunAt,
2290
+ executionStartedAt: lastRunAt,
2291
+ completedAt: lastRunAt,
2292
+ durationMs: 0,
2293
+ exitCode: 0,
2294
+ retryCount: 0,
2295
+ summary: 'Last run from cc-connect',
2296
+ }];
2297
+ } catch {
2298
+ return [];
2299
+ }
2300
+ });
2301
+
2302
+ app.get<{ Params: { id: string; runId: string } }>(
2303
+ '/api/schedules/:id/runs/:runId/logs',
2304
+ async (request) => {
2305
+ return (
2306
+ scheduleRunLogsByKey.get(makeScheduleRunLogKey(request.params.id, request.params.runId)) ?? {
2307
+ stdout: '',
2308
+ stderr: '',
2309
+ }
2310
+ );
2311
+ }
2312
+ );
2313
+
2314
+ // Browse directories — returns subdirectories at the given path
2315
+ app.post<{ Body: { path?: string; limit?: number } }>('/api/config/browse-folders', async (request) => {
2316
+ const { path: dirPath, limit = 200 } = request.body ?? {};
2317
+ const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
2318
+
2319
+ try {
2320
+ const entries = readdirSync(target, { withFileTypes: true });
2321
+ const dirs = entries
2322
+ .filter((e) => e.isDirectory() && !e.name.startsWith('.'))
2323
+ .slice(0, limit)
2324
+ .map((e) => path.join(target, e.name));
2325
+ return { success: true, data: { path: target, dirs, hasParent: target !== path.dirname(target) } };
2326
+ } catch {
2327
+ return { success: false, error: `无法访问目录: ${target}` };
2328
+ }
2329
+ });
2330
+
2331
+ // POST /api/workspace/list — 文件目录浏览
2332
+ app.post<{ Body: { dirPath?: string } }>('/api/workspace/list', async (request) => {
2333
+ const { dirPath } = request.body ?? {};
2334
+ const target = dirPath && dirPath.trim() ? dirPath.trim() : os.homedir();
2335
+
2336
+ try {
2337
+ const entries = readdirSync(target, { withFileTypes: true });
2338
+ const files = entries
2339
+ .filter((e) => !e.name.startsWith('.'))
2340
+ .slice(0, 500)
2341
+ .map((e) => {
2342
+ const fullPath = path.join(target, e.name);
2343
+ const isDirectory = e.isDirectory();
2344
+ let size = 0;
2345
+ try {
2346
+ const stat = statSync(fullPath);
2347
+ size = stat.size;
2348
+ } catch { /* ignore */ }
2349
+ return {
2350
+ name: e.name,
2351
+ isDirectory,
2352
+ size,
2353
+ ext: isDirectory ? '' : path.extname(e.name).slice(1).toLowerCase(),
2354
+ };
2355
+ });
2356
+ return { path: target, files, hasParent: target !== path.dirname(target) };
2357
+ } catch {
2358
+ return { path: target, files: [], hasParent: false, error: `无法访问目录: ${target}` };
2359
+ }
2360
+ });
2361
+
2362
+ // ===========================================================================
2363
+ // Project Editor API (web mode)
2364
+ // ===========================================================================
2365
+
2366
+ const MAX_EDITOR_DIR_ENTRIES = 2000;
2367
+ const MAX_EDITOR_FILE_BYTES = 2 * 1024 * 1024;
2368
+
2369
+ function resolveEditorRoot(rawRoot: unknown): string {
2370
+ if (typeof rawRoot !== 'string' || rawRoot.trim().length === 0) {
2371
+ throw new Error('root 参数不能为空');
2372
+ }
2373
+ const resolved = path.resolve(rawRoot.trim());
2374
+ if (!_existsSync2(resolved)) {
2375
+ throw new Error(`目录不存在: ${resolved}`);
2376
+ }
2377
+ const st = statSync(resolved);
2378
+ if (!st.isDirectory()) {
2379
+ throw new Error(`不是目录: ${resolved}`);
2380
+ }
2381
+ return resolved;
2382
+ }
2383
+
2384
+ function isPathInsideRoot(root: string, target: string): boolean {
2385
+ const rel = path.relative(root, target);
2386
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
2387
+ }
2388
+
2389
+ function resolveEditorPath(root: string, rawPath: unknown): string {
2390
+ if (typeof rawPath !== 'string' || rawPath.trim().length === 0) {
2391
+ throw new Error('filePath/dirPath 参数不能为空');
2392
+ }
2393
+ const requested = rawPath.trim();
2394
+ const resolved = path.resolve(path.isAbsolute(requested) ? requested : path.join(root, requested));
2395
+ if (!isPathInsideRoot(root, resolved)) {
2396
+ throw new Error('路径超出项目根目录');
2397
+ }
2398
+ return resolved;
2399
+ }
2400
+
2401
+ function detectBinary(buffer: Buffer): boolean {
2402
+ const sampleLength = Math.min(buffer.length, 4096);
2403
+ for (let i = 0; i < sampleLength; i++) {
2404
+ if (buffer[i] === 0) {
2405
+ return true;
2406
+ }
2407
+ }
2408
+ return false;
2409
+ }
2410
+
2411
+ function sendEditorError(
2412
+ reply: { code: (statusCode: number) => { send: (payload: { error: string }) => unknown } },
2413
+ err: unknown
2414
+ ) {
2415
+ const message = err instanceof Error ? err.message : String(err);
2416
+ return reply.code(500).send({ error: message });
2417
+ }
2418
+
2419
+ app.post<{ Body: { root?: unknown } }>('/api/editor/open', async (request, reply) => {
2420
+ try {
2421
+ const root = resolveEditorRoot(request.body?.root);
2422
+ return { ok: true, root };
2423
+ } catch (err) {
2424
+ return sendEditorError(reply, err);
2425
+ }
2426
+ });
2427
+
2428
+ app.post('/api/editor/close', async () => ({ ok: true }));
2429
+
2430
+ app.get<{ Querystring: { root?: unknown; dirPath?: unknown; maxEntries?: string } }>(
2431
+ '/api/editor/readDir',
2432
+ async (request, reply) => {
2433
+ try {
2434
+ const root = resolveEditorRoot(request.query.root);
2435
+ const dirPath = request.query.dirPath
2436
+ ? resolveEditorPath(root, request.query.dirPath)
2437
+ : root;
2438
+ const maxEntriesRaw = Number.parseInt(request.query.maxEntries ?? '', 10);
2439
+ const maxEntries = Number.isFinite(maxEntriesRaw)
2440
+ ? Math.min(Math.max(maxEntriesRaw, 1), MAX_EDITOR_DIR_ENTRIES)
2441
+ : MAX_EDITOR_DIR_ENTRIES;
2442
+ const entries = readdirSync(dirPath, { withFileTypes: true });
2443
+ const sliced = entries.slice(0, maxEntries);
2444
+ const mapped = sliced
2445
+ .map((entry) => {
2446
+ const fullPath = path.join(dirPath, entry.name);
2447
+ let size = 0;
2448
+ try {
2449
+ size = entry.isFile() ? statSync(fullPath).size : 0;
2450
+ } catch {
2451
+ size = 0;
2452
+ }
2453
+ return {
2454
+ name: entry.name,
2455
+ path: fullPath,
2456
+ type: (entry.isDirectory() ? 'directory' : 'file') as 'directory' | 'file',
2457
+ size,
2458
+ };
2459
+ })
2460
+ .sort((a, b) => {
2461
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
2462
+ return a.name.localeCompare(b.name);
2463
+ });
2464
+ return {
2465
+ entries: mapped,
2466
+ truncated: entries.length > maxEntries,
2467
+ };
2468
+ } catch (err) {
2469
+ return sendEditorError(reply, err);
2470
+ }
2471
+ }
2472
+ );
2473
+
2474
+ app.get<{ Querystring: { root?: unknown; filePath?: unknown } }>(
2475
+ '/api/editor/readFile',
2476
+ async (request, reply) => {
2477
+ try {
2478
+ const root = resolveEditorRoot(request.query.root);
2479
+ const filePath = resolveEditorPath(root, request.query.filePath);
2480
+ const st = statSync(filePath);
2481
+ if (!st.isFile()) {
2482
+ throw new Error(`不是文件: ${filePath}`);
2483
+ }
2484
+
2485
+ const fullBuffer = readFileSync(filePath);
2486
+ const truncated = fullBuffer.length > MAX_EDITOR_FILE_BYTES;
2487
+ const readBuffer = truncated ? fullBuffer.subarray(0, MAX_EDITOR_FILE_BYTES) : fullBuffer;
2488
+ const isBinary = detectBinary(readBuffer);
2489
+ return {
2490
+ content: isBinary ? '' : readBuffer.toString('utf-8'),
2491
+ size: st.size,
2492
+ mtimeMs: st.mtimeMs,
2493
+ truncated,
2494
+ encoding: isBinary ? 'binary' : 'utf-8',
2495
+ isBinary,
2496
+ };
2497
+ } catch (err) {
2498
+ return sendEditorError(reply, err);
2499
+ }
2500
+ }
2501
+ );
2502
+
2503
+ app.post<{ Body: { root?: unknown; filePath?: unknown; content?: unknown; baselineMtimeMs?: unknown } }>(
2504
+ '/api/editor/writeFile',
2505
+ async (request, reply) => {
2506
+ try {
2507
+ const root = resolveEditorRoot(request.body?.root);
2508
+ const filePath = resolveEditorPath(root, request.body?.filePath);
2509
+ const content = request.body?.content;
2510
+ if (typeof content !== 'string') {
2511
+ throw new Error('content 必须是字符串');
2512
+ }
2513
+ const baselineRaw = request.body?.baselineMtimeMs;
2514
+ if (typeof baselineRaw === 'number' && Number.isFinite(baselineRaw)) {
2515
+ const currentMtime = statSync(filePath).mtimeMs;
2516
+ if (Math.abs(currentMtime - baselineRaw) > 1) {
2517
+ throw new Error('CONFLICT: file changed on disk');
2518
+ }
2519
+ }
2520
+ writeFileSync(filePath, content, 'utf-8');
2521
+ const st = statSync(filePath);
2522
+ return { mtimeMs: st.mtimeMs, size: st.size };
2523
+ } catch (err) {
2524
+ return sendEditorError(reply, err);
2525
+ }
2526
+ }
2527
+ );
2528
+
2529
+ app.post<{ Body: { root?: unknown; parentDir?: unknown; fileName?: unknown } }>(
2530
+ '/api/editor/createFile',
2531
+ async (request, reply) => {
2532
+ try {
2533
+ const root = resolveEditorRoot(request.body?.root);
2534
+ const parentDir = resolveEditorPath(root, request.body?.parentDir);
2535
+ const fileName = typeof request.body?.fileName === 'string' ? request.body.fileName.trim() : '';
2536
+ if (!fileName) {
2537
+ throw new Error('fileName 不能为空');
2538
+ }
2539
+ const filePath = resolveEditorPath(root, path.join(parentDir, fileName));
2540
+ writeFileSync(filePath, '', { encoding: 'utf-8', flag: 'wx' });
2541
+ const st = statSync(filePath);
2542
+ return { filePath, mtimeMs: st.mtimeMs };
2543
+ } catch (err) {
2544
+ return sendEditorError(reply, err);
2545
+ }
2546
+ }
2547
+ );
2548
+
2549
+ app.post<{ Body: { root?: unknown; parentDir?: unknown; dirName?: unknown } }>(
2550
+ '/api/editor/createDir',
2551
+ async (request, reply) => {
2552
+ try {
2553
+ const root = resolveEditorRoot(request.body?.root);
2554
+ const parentDir = resolveEditorPath(root, request.body?.parentDir);
2555
+ const dirName = typeof request.body?.dirName === 'string' ? request.body.dirName.trim() : '';
2556
+ if (!dirName) {
2557
+ throw new Error('dirName 不能为空');
2558
+ }
2559
+ const dirPath = resolveEditorPath(root, path.join(parentDir, dirName));
2560
+ mkdirSync(dirPath, { recursive: false });
2561
+ return { dirPath };
2562
+ } catch (err) {
2563
+ return sendEditorError(reply, err);
2564
+ }
2565
+ }
2566
+ );
2567
+
2568
+ app.post<{ Body: { root?: unknown; filePath?: unknown } }>('/api/editor/deleteFile', async (request, reply) => {
2569
+ try {
2570
+ const root = resolveEditorRoot(request.body?.root);
2571
+ const filePath = resolveEditorPath(root, request.body?.filePath);
2572
+ rmSync(filePath, { recursive: true, force: false });
2573
+ return { deletedPath: filePath };
2574
+ } catch (err) {
2575
+ return sendEditorError(reply, err);
2576
+ }
2577
+ });
2578
+
2579
+ app.post<{ Body: { root?: unknown; sourcePath?: unknown; destDir?: unknown } }>(
2580
+ '/api/editor/moveFile',
2581
+ async (request, reply) => {
2582
+ try {
2583
+ const root = resolveEditorRoot(request.body?.root);
2584
+ const sourcePath = resolveEditorPath(root, request.body?.sourcePath);
2585
+ const destDir = resolveEditorPath(root, request.body?.destDir);
2586
+ const newPath = resolveEditorPath(root, path.join(destDir, path.basename(sourcePath)));
2587
+ const sourceStat = statSync(sourcePath);
2588
+ renameSync(sourcePath, newPath);
2589
+ return { newPath, isDirectory: sourceStat.isDirectory() };
2590
+ } catch (err) {
2591
+ return sendEditorError(reply, err);
2592
+ }
2593
+ }
2594
+ );
2595
+
2596
+ app.post<{ Body: { root?: unknown; sourcePath?: unknown; newName?: unknown } }>(
2597
+ '/api/editor/renameFile',
2598
+ async (request, reply) => {
2599
+ try {
2600
+ const root = resolveEditorRoot(request.body?.root);
2601
+ const sourcePath = resolveEditorPath(root, request.body?.sourcePath);
2602
+ const newName = typeof request.body?.newName === 'string' ? request.body.newName.trim() : '';
2603
+ if (!newName) {
2604
+ throw new Error('newName 不能为空');
2605
+ }
2606
+ const parentDir = path.dirname(sourcePath);
2607
+ const newPath = resolveEditorPath(root, path.join(parentDir, newName));
2608
+ const sourceStat = statSync(sourcePath);
2609
+ renameSync(sourcePath, newPath);
2610
+ return { newPath, isDirectory: sourceStat.isDirectory() };
2611
+ } catch (err) {
2612
+ return sendEditorError(reply, err);
2613
+ }
2614
+ }
2615
+ );
2616
+
2617
+ app.get<{ Querystring: { root?: unknown } }>('/api/editor/listFiles', async (request, reply) => {
2618
+ try {
2619
+ const root = resolveEditorRoot(request.query.root);
2620
+ const result: { path: string; name: string; relativePath: string }[] = [];
2621
+ const walk = (dirPath: string) => {
2622
+ for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
2623
+ const fullPath = path.join(dirPath, entry.name);
2624
+ if (entry.isDirectory()) {
2625
+ if (entry.name === '.git' || entry.name === 'node_modules') {
2626
+ continue;
2627
+ }
2628
+ walk(fullPath);
2629
+ continue;
2630
+ }
2631
+ if (!entry.isFile()) {
2632
+ continue;
2633
+ }
2634
+ result.push({
2635
+ path: fullPath,
2636
+ name: entry.name,
2637
+ relativePath: path.relative(root, fullPath),
2638
+ });
2639
+ }
2640
+ };
2641
+ walk(root);
2642
+ return result;
2643
+ } catch (err) {
2644
+ return sendEditorError(reply, err);
2645
+ }
2646
+ });
2647
+
2648
+ app.get<{ Querystring: { root?: unknown; filePath?: unknown } }>(
2649
+ '/api/editor/readBinaryPreview',
2650
+ async (request, reply) => {
2651
+ try {
2652
+ const root = resolveEditorRoot(request.query.root);
2653
+ const filePath = resolveEditorPath(root, request.query.filePath);
2654
+ const content = readFileSync(filePath);
2655
+ return {
2656
+ base64: content.toString('base64'),
2657
+ mimeType: 'application/octet-stream',
2658
+ size: content.length,
2659
+ };
2660
+ } catch (err) {
2661
+ return sendEditorError(reply, err);
2662
+ }
2663
+ }
2664
+ );
2665
+
2666
+ app.get('/api/editor/gitStatus', async () => ({
2667
+ files: [],
2668
+ isGitRepo: false,
2669
+ branch: null,
2670
+ }));
2671
+
2672
+ app.post('/api/editor/watchDir', async () => ({ ok: true }));
2673
+ app.post('/api/editor/setWatchedFiles', async () => ({ ok: true }));
2674
+ app.post('/api/editor/setWatchedDirs', async () => ({ ok: true }));
2675
+ app.get('/api/editor/search', async () => ({ results: [], totalMatches: 0, truncated: false }));
2676
+
2677
+ // ===========================================================================
2678
+ // 团队详情页强依赖的 stubs — 返回正确数据结构防止 store 解析失败
2679
+ // ===========================================================================
2680
+
2681
+ // 消息分页 — store 期望 MessagesPage 结构
2682
+ app.get<{ Params: { name: string }; Querystring: { cursor?: string; limit?: string } }>(
2683
+ '/api/teams/:name/messages', async (request) => {
2684
+ const { name } = request.params;
2685
+ const requestedLimit = Number(request.query.limit ?? 50);
2686
+ const limit = Math.min(
2687
+ Math.max(1, Number.isFinite(requestedLimit) ? Math.floor(requestedLimit) : 50),
2688
+ 100
2689
+ );
2690
+ const rawCursor = request.query.cursor;
2691
+ const offset = Math.max(
2692
+ 0,
2693
+ Number.isFinite(Number(rawCursor)) ? Math.floor(Number(rawCursor)) : 0
2694
+ );
2695
+ try {
2696
+ // Keep a bounded history snapshot in memory for pagination safety.
2697
+ const msgs = await svc.readMessages(name, { limit: 5000 });
2698
+ const sessions = await cc.listSessions(name).catch(() => []);
2699
+ const sessionByKey = new Map(sessions.map((session) => [session.session_key, session]));
2700
+ const newestFirstMessages = [...msgs].reverse();
2701
+ const pageSlice = newestFirstMessages.slice(offset, offset + limit);
2702
+ const page = pageSlice.map((m) => {
2703
+ const sessionKey =
2704
+ typeof m.meta?.sessionKey === 'string'
2705
+ ? m.meta.sessionKey
2706
+ : typeof m.meta?.session_key === 'string'
2707
+ ? m.meta.session_key
2708
+ : undefined;
2709
+ const session = sessionKey ? sessionByKey.get(sessionKey) : undefined;
2710
+ return {
2711
+ messageId: m.id,
2712
+ from: m.from,
2713
+ to: m.to,
2714
+ text: m.content,
2715
+ timestamp: m.ts,
2716
+ read: true,
2717
+ source: (m.role === 'user' ? 'user_sent' : 'inbox') as string,
2718
+ session: sessionKey
2719
+ ? {
2720
+ id: session?.id,
2721
+ key: sessionKey,
2722
+ platform: session?.platform,
2723
+ title: session?.name || session?.user_name || session?.chat_name || sessionKey,
2724
+ chatName: session?.chat_name,
2725
+ userName: session?.user_name,
2726
+ }
2727
+ : undefined,
2728
+ };
2729
+ });
2730
+ // feedRevision = count:lastId で変更を確実に検出
2731
+ const lastMsg = msgs[msgs.length - 1];
2732
+ const firstMsg = msgs[0];
2733
+ const feedRevision = `${msgs.length}:${firstMsg?.id ?? '0'}:${lastMsg?.id ?? '0'}`;
2734
+ const nextOffset = offset + page.length;
2735
+ const hasMore = nextOffset < newestFirstMessages.length;
2736
+ return {
2737
+ messages: page,
2738
+ nextCursor: hasMore ? String(nextOffset) : null,
2739
+ hasMore,
2740
+ feedRevision,
2741
+ };
2742
+ } catch {
2743
+ return { messages: [], nextCursor: null, hasMore: false, feedRevision: '0' };
2744
+ }
2745
+ }
2746
+ );
2747
+
2748
+ // 消息 head(messages-head 不是标准路由,storeok调 getMessagesPage 的同路由带 limit)
2749
+ // member-activity-meta
2750
+ app.get<{ Params: { name: string } }>(
2751
+ '/api/teams/:name/member-activity-meta', async (request) => {
2752
+ const { name } = request.params;
2753
+ return {
2754
+ teamName: name,
2755
+ computedAt: new Date().toISOString(),
2756
+ members: {},
2757
+ feedRevision: '0',
2758
+ };
2759
+ }
2760
+ );
2761
+
2762
+ // member-activity — GET /api/teams/:name/member-activity
2763
+ app.get<{ Params: { name: string } }>(
2764
+ '/api/teams/:name/member-activity', async (request) => {
2765
+ const { name } = request.params;
2766
+ return {
2767
+ teamName: name,
2768
+ computedAt: new Date().toISOString(),
2769
+ members: {},
2770
+ feedRevision: '0',
2771
+ };
2772
+ }
2773
+ );
2774
+
2775
+ // member-spawn-statuses — GET /api/teams/:name/member-spawn-statuses
2776
+ app.get<{ Params: { name: string } }>(
2777
+ '/api/teams/:name/member-spawn-statuses', async (request) => {
2778
+ const { name } = request.params;
2779
+ return {
2780
+ statuses: {},
2781
+ runId: null,
2782
+ };
2783
+ }
2784
+ );
2785
+
2786
+ // agent-runtime — GET /api/teams/:name/agent-runtime
2787
+ app.get<{ Params: { name: string } }>(
2788
+ '/api/teams/:name/agent-runtime', async (request) => {
2789
+ const { name } = request.params;
2790
+ return {
2791
+ teamName: name,
2792
+ updatedAt: new Date().toISOString(),
2793
+ runId: null,
2794
+ members: {},
2795
+ };
2796
+ }
2797
+ );
2798
+
2799
+ // lead-activity — GET /api/teams/:name/lead-activity
2800
+ app.get<{ Params: { name: string } }>(
2801
+ '/api/teams/:name/lead-activity', async () => {
2802
+ return { state: 'offline', updatedAt: new Date().toISOString() };
2803
+ }
2804
+ );
2805
+
2806
+ // lead-context — GET /api/teams/:name/lead-context
2807
+ app.get<{ Params: { name: string } }>(
2808
+ '/api/teams/:name/lead-context', async () => {
2809
+ return { usage: null };
2810
+ }
2811
+ );
2812
+
2813
+ // sessions — 从 cc-connect project sessions 获取,转换为前端 Session 格式
2814
+ app.get<{ Params: { name: string } }>(
2815
+ '/api/teams/:name/sessions', async (request) => {
2816
+ try {
2817
+ const sessions = await cc.listSessions(request.params.name);
2818
+ return sessions.map((s) => ({
2819
+ id: s.id,
2820
+ title: s.user_name || s.chat_name || s.name || s.session_key,
2821
+ projectId: request.params.name,
2822
+ sessionKey: s.session_key,
2823
+ platform: s.platform,
2824
+ userName: s.user_name ?? null,
2825
+ chatName: s.chat_name ?? null,
2826
+ active: s.active,
2827
+ live: s.live,
2828
+ historyCount: s.history_count,
2829
+ createdAt: s.created_at,
2830
+ updatedAt: s.updated_at,
2831
+ lastMessage: s.last_message ? {
2832
+ role: s.last_message.role,
2833
+ content: s.last_message.content,
2834
+ timestamp: s.last_message.timestamp,
2835
+ } : null,
2836
+ }));
2837
+ } catch { return []; }
2838
+ }
2839
+ );
2840
+
2841
+ // GET session detail — 通过 cc-connect API 获取会话详情(含历史消息)
2842
+ app.get<{ Params: { name: string; sessionId: string }; Querystring: { history_limit?: string } }>(
2843
+ '/api/teams/:name/sessions/:sessionId', async (request) => {
2844
+ const historyLimit = request.query.history_limit ? parseInt(request.query.history_limit, 10) : 500;
2845
+ const detail = await cc.getSession(request.params.name, request.params.sessionId, historyLimit);
2846
+ return mapCcSessionDetail(detail);
2847
+ }
2848
+ );
2849
+
2850
+ // DELETE session — 通过 cc-connect API 删除指定 session
2851
+ app.delete<{ Params: { name: string; sessionId: string } }>(
2852
+ '/api/teams/:name/sessions/:sessionId', async (request, reply) => {
2853
+ try {
2854
+ await cc.deleteSession(request.params.name, request.params.sessionId);
2855
+ return { ok: true };
2856
+ } catch (err) {
2857
+ return reply.code(500).send({ ok: false, error: err instanceof Error ? err.message : String(err) });
2858
+ }
2859
+ }
2860
+ );
2861
+
2862
+ // runtime/alive — 从 cc-connect 获取真实在线状态
2863
+ app.get('/api/teams/runtime/alive', async () => {
2864
+ try {
2865
+ const projects = await cc.listProjects();
2866
+ return await Promise.all(projects.map(async (p) => {
2867
+ let isAlive = false;
2868
+ try {
2869
+ const detail = await cc.getProject(p.name);
2870
+ isAlive = Array.isArray(detail.platforms) && detail.platforms.some((pl) => pl.connected);
2871
+ } catch { /* degraded */ }
2872
+ return { teamName: p.name, isAlive, runId: p.name };
2873
+ }));
2874
+ } catch { return []; }
2875
+ });
2876
+
2877
+ // process-alive — 查询 cc-connect project 在线状态
2878
+ app.get<{ Params: { name: string } }>(
2879
+ '/api/teams/:name/process-alive', async (request) => {
2880
+ try {
2881
+ const p = await cc.getProject(request.params.name);
2882
+ return Array.isArray(p.platforms) && p.platforms.some((pl) => pl.connected);
2883
+ } catch { return false; }
2884
+ }
2885
+ );
2886
+
2887
+ // process-send — 从 Hermit UI 注入到 harness,不回发到 IM 平台。
2888
+ app.post<{ Params: { name: string }; Body: { text?: string; message?: string } }>(
2889
+ '/api/teams/:name/process-send', async (request, reply) => {
2890
+ try {
2891
+ const text = request.body?.text ?? request.body?.message ?? '';
2892
+ if (text) {
2893
+ await sendHarnessMessageViaBridge({
2894
+ teamName: request.params.name,
2895
+ text,
2896
+ });
2897
+ }
2898
+ return { ok: true };
2899
+ } catch (err) {
2900
+ return reply.code(502).send({
2901
+ ok: false,
2902
+ error: err instanceof Error ? err.message : '发送到 harness 失败',
2903
+ });
2904
+ }
2905
+ }
2906
+ );
2907
+
2908
+ // saved-request — 新版无此概念
2909
+ app.get<{ Params: { name: string } }>(
2910
+ '/api/teams/:name/saved-request', async () => null
2911
+ );
2912
+
2913
+ // kanban state — 返回空看板状态
2914
+ app.get<{ Params: { name: string } }>(
2915
+ '/api/teams/:name/kanban', async (request) => ({
2916
+ teamName: request.params.name,
2917
+ reviewers: [],
2918
+ tasks: {},
2919
+ })
2920
+ );
2921
+
2922
+ // task-change-presence — 返回 {}
2923
+ app.get<{ Params: { name: string } }>(
2924
+ '/api/teams/:name/task-change-presence', async () => ({})
2925
+ );
2926
+
2927
+ // kanban column order — no-op
2928
+ app.post<{ Params: { name: string } }>(
2929
+ '/api/teams/:name/kanban-column-order', async () => ({ ok: true })
2930
+ );
2931
+
2932
+ // teams/tasks (全局任务列表 — 跨所有团队)
2933
+ app.get('/api/teams/tasks', async () => {
2934
+ try {
2935
+ const allTasks: ReturnType<typeof toTeamTask>[] = [];
2936
+ const projects = await cc.listProjects();
2937
+ for (const p of projects) {
2938
+ try {
2939
+ const tasks = activeTasks(await svc.readTasks(p.name));
2940
+ allTasks.push(...tasks.map(toTeamTask));
2941
+ } catch { /* skip */ }
2942
+ }
2943
+ return allTasks;
2944
+ } catch { return []; }
2945
+ });
2946
+
2947
+ // 团队任务子操作 — 全部委托给 svc.patchTask
2948
+ app.post<{ Params: { name: string; id: string } }>(
2949
+ '/api/teams/:name/tasks/:id/request-review', async (request) => {
2950
+ try {
2951
+ const task = await svc.patchTask(request.params.name, request.params.id, { status: 'done' });
2952
+ return { ok: true, data: toTeamTask(task) };
2953
+ } catch { return { ok: true }; }
2954
+ }
2955
+ );
2956
+ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
2957
+ '/api/teams/:name/tasks/:id/kanban', async (request) => {
2958
+ // kanban metadata — stored in board.json via patchTask (no-op for now, column tracked client-side)
2959
+ return { ok: true };
2960
+ }
2961
+ );
2962
+ app.patch<{ Params: { name: string; id: string }; Body: { status?: string } }>(
2963
+ '/api/teams/:name/tasks/:id/status', async (request) => {
2964
+ try {
2965
+ const { status } = request.body ?? {};
2966
+ const task = await svc.patchTask(request.params.name, request.params.id, {
2967
+ status: status ? toTaskStatus(status) : undefined,
2968
+ });
2969
+ return toTeamTask(task);
2970
+ } catch { return { ok: true }; }
2971
+ }
2972
+ );
2973
+ app.patch<{ Params: { name: string; id: string }; Body: { owner?: string } }>(
2974
+ '/api/teams/:name/tasks/:id/owner', async (request) => {
2975
+ try {
2976
+ const body = request.body ?? {};
2977
+ const task = await svc.patchTask(request.params.name, request.params.id, { assignee: body.owner ?? null });
2978
+ if (task.assignee) {
2979
+ svc.dispatchTask(request.params.name, task).catch(() => {});
2980
+ }
2981
+ return toTeamTask(task);
2982
+ } catch { return { ok: true }; }
2983
+ }
2984
+ );
2985
+ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
2986
+ '/api/teams/:name/tasks/:id/fields', async (request) => {
2987
+ try {
2988
+ const body = request.body ?? {};
2989
+ const patch: Record<string, unknown> = {};
2990
+ if (body.subject !== undefined) patch.title = body.subject;
2991
+ if (body.description !== undefined) patch.description = body.description;
2992
+ const task = await svc.patchTask(request.params.name, request.params.id, patch);
2993
+ return toTeamTask(task);
2994
+ } catch { return { ok: true }; }
2995
+ }
2996
+ );
2997
+ app.post<{ Params: { name: string; id: string } }>(
2998
+ '/api/teams/:name/tasks/:id/start', async (request) => {
2999
+ try {
3000
+ const task = await svc.patchTask(request.params.name, request.params.id, { status: 'doing' });
3001
+ if (task.assignee) {
3002
+ svc.dispatchTask(request.params.name, task).catch(() => {});
3003
+ return { notifiedOwner: true };
3004
+ }
3005
+ return { notifiedOwner: false };
3006
+ } catch { return { notifiedOwner: false }; }
3007
+ }
3008
+ );
3009
+ app.post<{ Params: { name: string; id: string } }>(
3010
+ '/api/teams/:name/tasks/:id/start-by-user', async (request) => {
3011
+ try {
3012
+ const task = await svc.patchTask(request.params.name, request.params.id, { status: 'doing' });
3013
+ if (task.assignee) {
3014
+ svc.dispatchTask(request.params.name, task).catch(() => {});
3015
+ return { notifiedOwner: true };
3016
+ }
3017
+ return { notifiedOwner: false };
3018
+ } catch { return { notifiedOwner: false }; }
3019
+ }
3020
+ );
3021
+ app.post<{ Params: { name: string; id: string } }>(
3022
+ '/api/teams/:name/tasks/:id/soft-delete', async (request, reply) => {
3023
+ try {
3024
+ await svc.patchTask(request.params.name, request.params.id, { status: 'done', result: '__deleted__' });
3025
+ return { ok: true };
3026
+ } catch (err) {
3027
+ return reply.code(404).send(reply500(err));
3028
+ }
3029
+ }
3030
+ );
3031
+ app.post<{ Params: { name: string; id: string } }>(
3032
+ '/api/teams/:name/tasks/:id/restore', async (request, reply) => {
3033
+ try {
3034
+ await svc.patchTask(request.params.name, request.params.id, { status: 'todo', result: null });
3035
+ return { ok: true };
3036
+ } catch (err) {
3037
+ return reply.code(404).send(reply500(err));
3038
+ }
3039
+ }
3040
+ );
3041
+ app.get<{ Params: { name: string } }>(
3042
+ '/api/teams/:name/deleted-tasks', async (request) => {
3043
+ try {
3044
+ const tasks = await svc.readTasks(request.params.name);
3045
+ return tasks.filter((t) => t.result === '__deleted__').map(toTeamTask);
3046
+ } catch { return []; }
3047
+ }
3048
+ );
3049
+ app.post<{ Params: { name: string; id: string }; Body: { text?: string } }>(
3050
+ '/api/teams/:name/tasks/:id/comments', async () => ({ ok: true })
3051
+ );
3052
+ app.post<{ Params: { name: string; id: string } }>(
3053
+ '/api/teams/:name/tasks/:id/clarification', async () => ({ ok: true })
3054
+ );
3055
+ app.post<{ Params: { name: string; id: string } }>(
3056
+ '/api/teams/:name/tasks/:id/relationships', async () => ({ ok: true })
3057
+ );
3058
+
3059
+
3060
+ // 成员相关 stubs
3061
+ app.post<{ Params: { name: string } }>(
3062
+ '/api/teams/:name/members', async () => ({ ok: true })
3063
+ );
3064
+ app.delete<{ Params: { name: string; memberName: string } }>(
3065
+ '/api/teams/:name/members/:memberName', async () => ({ ok: true })
3066
+ );
3067
+ app.patch<{ Params: { name: string; memberName: string } }>(
3068
+ '/api/teams/:name/members/:memberName/role', async () => ({ ok: true })
3069
+ );
3070
+ app.post<{ Params: { name: string; memberName: string } }>(
3071
+ '/api/teams/:name/members/:memberName/restart', async () => ({ ok: true })
3072
+ );
3073
+ app.post<{ Params: { name: string; memberName: string } }>(
3074
+ '/api/teams/:name/members/:memberName/skip-launch', async () => ({ ok: true })
3075
+ );
3076
+
3077
+ // claude logs
3078
+ app.get<{ Params: { name: string } }>(
3079
+ '/api/teams/:name/claude-logs', async () => ({ logs: [], total: 0 })
3080
+ );
3081
+
3082
+ // restore / permanent delete
3083
+ app.post<{ Params: { name: string } }>(
3084
+ '/api/teams/:name/restore', async () => ({ ok: true })
3085
+ );
3086
+ app.delete<{ Params: { name: string } }>(
3087
+ '/api/teams/:name/permanent', async () => ({ ok: true })
3088
+ );
3089
+
3090
+ // config operations
3091
+ async function applyTeamConfigUpdate(
3092
+ teamName: string,
3093
+ body: Record<string, unknown>
3094
+ ): Promise<Record<string, unknown>> {
3095
+ const name = typeof body.name === 'string' ? body.name.trim() : '';
3096
+ const description = typeof body.description === 'string' ? body.description.trim() : '';
3097
+ const color = typeof body.color === 'string' ? body.color.trim() : '';
3098
+ const agentType = typeof body.agentType === 'string' ? body.agentType.trim() : '';
3099
+ const workDir = typeof body.workDir === 'string' ? body.workDir.trim() : '';
3100
+ const permissionMode =
3101
+ typeof body.permissionMode === 'string' ? body.permissionMode.trim() : '';
3102
+ const language = typeof body.language === 'string' ? body.language.trim() : '';
3103
+ const managedSources =
3104
+ typeof body.managedSources === 'string' ? body.managedSources.trim() : '';
3105
+ const showContextIndicator =
3106
+ typeof body.showContextIndicator === 'boolean' ? body.showContextIndicator : undefined;
3107
+ const replyFooter = typeof body.replyFooter === 'boolean' ? body.replyFooter : undefined;
3108
+ const injectSender = typeof body.injectSender === 'boolean' ? body.injectSender : undefined;
3109
+ const disabledCommands = Array.isArray(body.disabledCommands)
3110
+ ? normalizeStringArray(body.disabledCommands)
3111
+ : undefined;
3112
+ const platformAllowFrom = body.platformAllowFrom
3113
+ ? normalizePlatformAllowFrom(body.platformAllowFrom)
3114
+ : undefined;
3115
+
3116
+ const localPatch: Record<string, unknown> = {};
3117
+ if (name) localPatch.displayName = name;
3118
+ if (description) localPatch.description = description;
3119
+ if (color) localPatch.color = color;
3120
+ if (agentType) localPatch.harness = agentType;
3121
+ if (workDir) localPatch.workDir = workDir;
3122
+ if (permissionMode) localPatch.permissionMode = permissionMode;
3123
+ if (language) localPatch.language = language;
3124
+ if (managedSources) localPatch.managedSources = managedSources;
3125
+ if (disabledCommands) localPatch.disabledCommands = disabledCommands;
3126
+ if (platformAllowFrom !== undefined) localPatch.platformAllowFrom = platformAllowFrom;
3127
+ if (showContextIndicator !== undefined) localPatch.showContextIndicator = showContextIndicator;
3128
+ if (replyFooter !== undefined) localPatch.replyFooter = replyFooter;
3129
+ if (injectSender !== undefined) localPatch.injectSender = injectSender;
3130
+
3131
+ if (Object.keys(localPatch).length > 0) {
3132
+ try {
3133
+ await svc.updateTeam(teamName, localPatch);
3134
+ } catch {
3135
+ // If the team only exists in cc-connect, create Hermit metadata now so displayName can persist.
3136
+ const project = await cc.getProject(teamName);
3137
+ await svc.createTeam({
3138
+ displayName: name || teamName,
3139
+ bindProject: teamName,
3140
+ harness: agentType || project.agent_type || 'claudecode',
3141
+ workDir: workDir || project.work_dir || '',
3142
+ color: color || undefined,
3143
+ description: description || undefined,
3144
+ createCcProject: false,
3145
+ });
3146
+ await svc.updateTeam(teamName, localPatch);
3147
+ }
3148
+ }
3149
+
3150
+ const ccPatch: Record<string, unknown> = {};
3151
+ if (agentType) ccPatch.agent_type = agentType;
3152
+ if (workDir) ccPatch.work_dir = workDir;
3153
+ if (permissionMode) ccPatch.mode = permissionMode;
3154
+ if (language) ccPatch.language = language;
3155
+ if (managedSources) ccPatch.admin_from = managedSources;
3156
+ if (disabledCommands) ccPatch.disabled_commands = disabledCommands;
3157
+ if (platformAllowFrom !== undefined) ccPatch.platform_allow_from = platformAllowFrom;
3158
+ if (showContextIndicator !== undefined) ccPatch.show_context_indicator = showContextIndicator;
3159
+ if (replyFooter !== undefined) ccPatch.reply_footer = replyFooter;
3160
+ if (injectSender !== undefined) ccPatch.inject_sender = injectSender;
3161
+
3162
+ let ccSyncError: string | null = null;
3163
+ if (Object.keys(ccPatch).length > 0) {
3164
+ try {
3165
+ const updateResult = await cc.updateProject(
3166
+ teamName,
3167
+ ccPatch as Parameters<CcConnectClient['updateProject']>[1]
3168
+ );
3169
+ if (updateResult.restart_required) {
3170
+ await cc.restart();
3171
+ }
3172
+ } catch (err) {
3173
+ ccSyncError = err instanceof Error ? err.message : String(err);
3174
+ }
3175
+ }
3176
+
3177
+ return {
3178
+ name: name || teamName,
3179
+ displayName: name || teamName,
3180
+ description: description || undefined,
3181
+ color: color || undefined,
3182
+ projectPath: workDir || undefined,
3183
+ agentType: agentType || undefined,
3184
+ permissionMode: permissionMode || undefined,
3185
+ language: language || undefined,
3186
+ managedSources: managedSources || undefined,
3187
+ disabledCommands: disabledCommands ?? [],
3188
+ showContextIndicator: showContextIndicator ?? false,
3189
+ replyFooter: replyFooter ?? false,
3190
+ injectSender: injectSender ?? false,
3191
+ platformAllowFrom: platformAllowFrom ?? {},
3192
+ ccSyncError,
3193
+ };
3194
+ }
3195
+
3196
+ app.get<{ Params: { name: string } }>(
3197
+ '/api/teams/:name/config', async (request, reply) => {
3198
+ try {
3199
+ const name = request.params.name;
3200
+ const p = await cc.getProject(name);
3201
+ // local metadata overlay
3202
+ let color = 'blue';
3203
+ let description = '';
3204
+ let language = '';
3205
+ let managedSources = '*';
3206
+ let disabledCommands: string[] = [];
3207
+ let showContextIndicator = false;
3208
+ let replyFooter = false;
3209
+ let injectSender = false;
3210
+ let permissionMode = 'default';
3211
+ let platformAllowFrom: Record<string, string> = {};
3212
+ try {
3213
+ const meta = await svc.readTeamManifest(name);
3214
+ color = meta.color ?? color;
3215
+ description = meta.description ?? description;
3216
+ language = meta.language ?? language;
3217
+ managedSources = meta.managedSources ?? managedSources;
3218
+ disabledCommands = normalizeStringArray(meta.disabledCommands);
3219
+ showContextIndicator = meta.showContextIndicator ?? showContextIndicator;
3220
+ replyFooter = meta.replyFooter ?? replyFooter;
3221
+ injectSender = meta.injectSender ?? injectSender;
3222
+ permissionMode = meta.permissionMode ?? permissionMode;
3223
+ platformAllowFrom = normalizePlatformAllowFrom(meta.platformAllowFrom);
3224
+ } catch { /* ok */ }
3225
+ const projectSettings = (p.settings ?? {}) as Record<string, unknown>;
3226
+ const resolvedLanguage =
3227
+ typeof projectSettings.language === 'string' && projectSettings.language.trim().length > 0
3228
+ ? projectSettings.language.trim()
3229
+ : language;
3230
+ const resolvedManagedSources =
3231
+ typeof projectSettings.admin_from === 'string' && projectSettings.admin_from.trim().length > 0
3232
+ ? projectSettings.admin_from.trim()
3233
+ : managedSources;
3234
+ const resolvedDisabledCommands =
3235
+ Array.isArray(projectSettings.disabled_commands) &&
3236
+ normalizeStringArray(projectSettings.disabled_commands).length > 0
3237
+ ? normalizeStringArray(projectSettings.disabled_commands)
3238
+ : disabledCommands;
3239
+ const resolvedShowContextIndicator =
3240
+ typeof projectSettings.show_context_indicator === 'boolean'
3241
+ ? projectSettings.show_context_indicator
3242
+ : showContextIndicator;
3243
+ const resolvedReplyFooter =
3244
+ typeof projectSettings.reply_footer === 'boolean'
3245
+ ? projectSettings.reply_footer
3246
+ : replyFooter;
3247
+ const resolvedInjectSender =
3248
+ typeof projectSettings.inject_sender === 'boolean'
3249
+ ? projectSettings.inject_sender
3250
+ : injectSender;
3251
+ const resolvedPlatformAllowFrom = (() => {
3252
+ const normalized = normalizePlatformAllowFrom(projectSettings.platform_allow_from);
3253
+ if (Object.keys(normalized).length > 0) {
3254
+ return normalized;
3255
+ }
3256
+ return platformAllowFrom;
3257
+ })();
3258
+ const resolvedPermissionMode =
3259
+ typeof p.agent_mode === 'string' && p.agent_mode.trim().length > 0
3260
+ ? p.agent_mode.trim()
3261
+ : permissionMode;
3262
+ return {
3263
+ name,
3264
+ color,
3265
+ projectPath: p.work_dir ?? '',
3266
+ description,
3267
+ agentType: p.agent_type,
3268
+ workDir: p.work_dir ?? '',
3269
+ language: resolvedLanguage,
3270
+ managedSources: resolvedManagedSources,
3271
+ disabledCommands: resolvedDisabledCommands,
3272
+ showContextIndicator: resolvedShowContextIndicator,
3273
+ replyFooter: resolvedReplyFooter,
3274
+ injectSender: resolvedInjectSender,
3275
+ permissionMode: resolvedPermissionMode,
3276
+ platformAllowFrom: resolvedPlatformAllowFrom,
3277
+ settings: {
3278
+ ...projectSettings,
3279
+ language: resolvedLanguage,
3280
+ admin_from: resolvedManagedSources,
3281
+ disabled_commands: resolvedDisabledCommands,
3282
+ show_context_indicator: resolvedShowContextIndicator,
3283
+ reply_footer: resolvedReplyFooter,
3284
+ inject_sender: resolvedInjectSender,
3285
+ platform_allow_from: resolvedPlatformAllowFrom,
3286
+ },
3287
+ };
3288
+ } catch { return reply.code(404).send({ error: 'not found' }); }
3289
+ }
3290
+ );
3291
+ app.patch<{ Params: { name: string } }>(
3292
+ '/api/teams/:name/config', async (request, reply) => {
3293
+ try {
3294
+ const data = await applyTeamConfigUpdate(
3295
+ request.params.name,
3296
+ (request.body as Record<string, unknown>) ?? {}
3297
+ );
3298
+ return data;
3299
+ } catch (err) {
3300
+ return reply.code(400).send(reply500(err));
3301
+ }
3302
+ }
3303
+ );
3304
+
3305
+ // provisioning stubs (新版无 provisioning 概念)
3306
+ app.post('/api/teams/provisioning/prepare', async () => ({
3307
+ runId: null,
3308
+ warnings: [],
3309
+ }));
3310
+ app.get<{ Params: { runId: string } }>(
3311
+ '/api/teams/provisioning/:runId', async () => ({
3312
+ runId: '',
3313
+ phase: 'done',
3314
+ progress: 100,
3315
+ message: '',
3316
+ done: true,
3317
+ error: null,
3318
+ })
3319
+ );
3320
+ app.post<{ Params: { runId: string } }>(
3321
+ '/api/teams/provisioning/:runId/cancel', async () => ({ ok: true })
3322
+ );
3323
+
3324
+ // 团队创建已由上方 /api/teams/create 处理(cc-connect 直接调用)
3325
+
3326
+ // templates stubs
3327
+ app.get('/api/teams/templates', async () => ({ sources: [], templates: [] }));
3328
+ app.post('/api/teams/templates/save', async () => ({ sources: [], templates: [] }));
3329
+ app.post('/api/teams/templates/refresh', async () => ({ sources: [], templates: [] }));
3330
+
3331
+ // replace members
3332
+ app.put<{ Params: { name: string } }>(
3333
+ '/api/teams/:name/members', async () => ({ ok: true })
3334
+ );
3335
+
3336
+ // draft
3337
+ app.delete<{ Params: { name: string } }>(
3338
+ '/api/teams/:name/draft', async () => ({ ok: true })
3339
+ );
3340
+
3341
+ // send-message — 从 Hermit 会话面板注入到 harness,不使用 Management /send(那会回发到 IM)。
3342
+ app.post<{ Params: { name: string }; Body: { member?: string; text?: string; content?: string; summary?: string; sessionKey?: string } }>(
3343
+ '/api/teams/:name/send-message', async (request, reply) => {
3344
+ const teamName = request.params.name;
3345
+ const text = request.body?.text ?? request.body?.content ?? '';
3346
+ if (!text.trim()) return { ok: true, messageId: null };
3347
+
3348
+ const msgId = `hermit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3349
+
3350
+ // 使用固定格式 session key,保证 reply 事件能正确映射回 teamName
3351
+ const requestedSessionKey =
3352
+ typeof request.body?.sessionKey === 'string' ? request.body.sessionKey.trim() : '';
3353
+ let sessionKey = requestedSessionKey;
3354
+
3355
+ try {
3356
+ sessionKey = await sendHarnessMessageViaBridge({
3357
+ teamName,
3358
+ text,
3359
+ sessionKey,
3360
+ msgId,
3361
+ });
3362
+ } catch (err) {
3363
+ return reply.code(502).send({
3364
+ ok: false,
3365
+ error: err instanceof Error ? err.message : '发送到 harness 失败',
3366
+ });
3367
+ }
3368
+
3369
+ // 本地存储用户消息
3370
+ const userMsg = await svc.appendMessage(teamName, {
3371
+ from: 'user',
3372
+ to: teamName,
3373
+ role: 'user',
3374
+ content: text,
3375
+ meta: { sessionKey },
3376
+ }).catch(() => null);
3377
+
3378
+ // 广播 SSE 让前端触发消息刷新
3379
+ broadcastSse('team-change', { type: 'inbox', teamName });
3380
+
3381
+ return {
3382
+ ok: true,
3383
+ deliveredToInbox: true,
3384
+ messageId: userMsg?.id ?? msgId,
3385
+ runtimeDelivery: {
3386
+ attempted: true,
3387
+ delivered: true,
3388
+ },
3389
+ };
3390
+ }
3391
+ );
3392
+
3393
+
3394
+
3395
+ // ===========================================================================
3396
+ // 路由别名 — 修正前端调用路径与服务端路径的不匹配
3397
+ // ===========================================================================
3398
+
3399
+ // requestReview: 前端调用 /tasks/:id/review,服务端原路由是 /tasks/:id/request-review
3400
+ app.post<{ Params: { name: string; id: string } }>(
3401
+ '/api/teams/:name/tasks/:id/review', async (request) => {
3402
+ try {
3403
+ const task = await svc.patchTask(request.params.name, request.params.id, { status: 'done' });
3404
+ return { ok: true, data: toTeamTask(task) };
3405
+ } catch { return { ok: true }; }
3406
+ }
3407
+ );
3408
+
3409
+ // updateKanban: 前端调用 PATCH /kanban/:taskId
3410
+ app.patch<{ Params: { name: string; id: string }; Body: Record<string, unknown> }>(
3411
+ '/api/teams/:name/kanban/:id', async () => ({ ok: true })
3412
+ );
3413
+
3414
+ // updateKanbanColumnOrder: 前端调用 PUT /kanban/column-order
3415
+ app.put<{ Params: { name: string } }>(
3416
+ '/api/teams/:name/kanban/column-order', async () => ({ ok: true })
3417
+ );
3418
+
3419
+ // updateConfig: 前端调用 PUT /config(服务端原有 PATCH,补充 PUT 别名)
3420
+ app.put<{ Params: { name: string } }>(
3421
+ '/api/teams/:name/config', async (request, reply) => {
3422
+ try {
3423
+ const data = await applyTeamConfigUpdate(
3424
+ request.params.name,
3425
+ (request.body as Record<string, unknown>) ?? {}
3426
+ );
3427
+ return data;
3428
+ } catch (err) {
3429
+ return reply.code(400).send(reply500(err));
3430
+ }
3431
+ }
3432
+ );
3433
+
3434
+ // skipMemberForLaunch: 前端调用 /members/:memberName/skip
3435
+ app.post<{ Params: { name: string; memberName: string } }>(
3436
+ '/api/teams/:name/members/:memberName/skip', async () => ({ ok: true })
3437
+ );
3438
+
3439
+ // setTaskClarification: 前端调用 POST /task-clarification/:taskId
3440
+ app.post<{ Params: { name: string; taskId: string } }>(
3441
+ '/api/teams/:name/task-clarification/:taskId', async () => ({ ok: true })
3442
+ );
3443
+
3444
+ // removeTaskRelationship: 前端调用 DELETE /tasks/:id/relationships
3445
+ app.delete<{ Params: { name: string; id: string } }>(
3446
+ '/api/teams/:name/tasks/:id/relationships', async () => ({ ok: true })
3447
+ );
3448
+
3449
+ // ===========================================================================
3450
+ // 缺失的 stub 路由 — 返回空数据防止前端 404 崩溃
3451
+ // ===========================================================================
3452
+
3453
+ // createConfig
3454
+ app.post('/api/teams/config', async () => ({ ok: true }));
3455
+
3456
+ // kill-process
3457
+ app.post<{ Params: { name: string }; Body: { pid?: number } }>(
3458
+ '/api/teams/:name/kill-process', async () => ({ ok: true })
3459
+ );
3460
+
3461
+ // member-logs
3462
+ app.get<{ Params: { name: string; memberName: string } }>(
3463
+ '/api/teams/:name/member-logs/:memberName', async () => []
3464
+ );
3465
+
3466
+ // task-logs
3467
+ app.get<{ Params: { name: string; taskId: string } }>(
3468
+ '/api/teams/:name/task-logs/:taskId', async () => []
3469
+ );
3470
+
3471
+ // activity
3472
+ app.get<{ Params: { name: string } }>(
3473
+ '/api/teams/:name/activity', async () => []
3474
+ );
3475
+
3476
+ // task-activity-detail
3477
+ app.get<{ Params: { name: string } }>(
3478
+ '/api/teams/:name/task-activity-detail', async () => ({ entries: [] })
3479
+ );
3480
+
3481
+ // task-log-stream-summary
3482
+ app.get<{ Params: { name: string; taskId: string } }>(
3483
+ '/api/teams/:name/task-log-stream-summary/:taskId', async () => ({ chunks: [] })
3484
+ );
3485
+
3486
+ // task-log-stream
3487
+ app.get<{ Params: { name: string; taskId: string } }>(
3488
+ '/api/teams/:name/task-log-stream/:taskId', async () => ({ chunks: [] })
3489
+ );
3490
+
3491
+ // exact-log-summaries
3492
+ app.get<{ Params: { name: string; taskId: string } }>(
3493
+ '/api/teams/:name/exact-log-summaries/:taskId', async () => ({ logs: [] })
3494
+ );
3495
+
3496
+ // exact-log-detail
3497
+ app.get<{ Params: { name: string; taskId: string } }>(
3498
+ '/api/teams/:name/exact-log-detail/:taskId', async () => ({ lines: [] })
3499
+ );
3500
+
3501
+ // member-stats
3502
+ app.get<{ Params: { name: string; memberName: string } }>(
3503
+ '/api/teams/:name/member-stats/:memberName', async () => ({
3504
+ linesAdded: 0,
3505
+ linesRemoved: 0,
3506
+ filesTouched: [],
3507
+ fileStats: {},
3508
+ toolUsage: {},
3509
+ inputTokens: 0,
3510
+ outputTokens: 0,
3511
+ cacheReadTokens: 0,
3512
+ costUsd: 0,
3513
+ tasksCompleted: 0,
3514
+ messageCount: 0,
3515
+ totalDurationMs: 0,
3516
+ sessionCount: 0,
3517
+ computedAt: new Date().toISOString(),
3518
+ })
3519
+ );
3520
+
3521
+ // tool-approval stubs
3522
+ app.post<{ Params: { name: string } }>(
3523
+ '/api/teams/:name/tool-approval/respond', async () => ({ ok: true })
3524
+ );
3525
+ app.post<{ Params: { name: string } }>(
3526
+ '/api/teams/:name/tool-approval/settings', async () => ({ ok: true })
3527
+ );
3528
+ app.post('/api/teams/tool-approval/read-file', async () => ({ content: '' }));
3529
+
3530
+ // validate-cli-args
3531
+ app.post('/api/teams/validate-cli-args', async () => ({ valid: true, args: [], errors: [] }));
3532
+
3533
+ // cross-team stubs
3534
+ app.post('/api/cross-team/send', async () => ({ ok: true }));
3535
+ app.get('/api/cross-team/targets', async () => []);
3536
+ app.get<{ Params: { name: string } }>(
3537
+ '/api/cross-team/outbox/:name', async () => []
3538
+ );
3539
+
3540
+ // review stubs
3541
+ app.get<{ Params: { name: string; memberName: string } }>(
3542
+ '/api/teams/:name/review/agent-changes/:memberName', async (request) => ({
3543
+ teamName: request.params.name,
3544
+ memberName: request.params.memberName,
3545
+ files: [],
3546
+ totalLinesAdded: 0,
3547
+ totalLinesRemoved: 0,
3548
+ totalFiles: 0,
3549
+ computedAt: new Date().toISOString(),
3550
+ })
3551
+ );
3552
+ app.get<{ Params: { name: string; taskId: string } }>(
3553
+ '/api/teams/:name/review/task-changes/:taskId', async () => ({ changes: [] })
3554
+ );
3555
+ app.get<{ Params: { name: string; memberName: string } }>(
3556
+ '/api/teams/:name/review/change-stats/:memberName', async () => ({ stats: {} })
3557
+ );
3558
+ app.get<{ Params: { name: string } }>(
3559
+ '/api/teams/:name/review/file-content', async () => ({ content: '' })
3560
+ );
3561
+ app.post<{ Params: { name: string } }>(
3562
+ '/api/teams/:name/review/apply-decisions', async () => ({ ok: true })
3563
+ );
3564
+ app.post('/api/teams/review/check-conflict', async () => ({ conflict: false }));
3565
+ app.post('/api/teams/review/preview-reject', async () => ({ preview: '' }));
3566
+ app.post('/api/teams/review/save-edited-file', async () => ({ ok: true }));
3567
+ app.post('/api/teams/review/decisions/load', async () => ({ decisions: {} }));
3568
+ app.post('/api/teams/review/decisions/save', async () => ({ ok: true }));
3569
+ app.post('/api/teams/review/decisions/clear', async () => ({ ok: true }));
3570
+ app.get('/api/teams/review/git-file-log', async () => ({ log: [] }));
3571
+
3572
+ // ===========================================================================
3573
+ // SSE 推送端点 — 前端 EventSource 连接此处接收实时事件
3574
+ // ===========================================================================
3575
+
3576
+ app.get('/api/events', (request, reply) => {
3577
+ reply.raw.writeHead(200, {
3578
+ 'Content-Type': 'text/event-stream; charset=utf-8',
3579
+ 'Cache-Control': 'no-cache, no-transform',
3580
+ Connection: 'keep-alive',
3581
+ 'X-Accel-Buffering': 'no',
3582
+ });
3583
+
3584
+ const client: SseClient = {
3585
+ res: reply.raw,
3586
+ id: `sse-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
3587
+ };
3588
+ sseClients.add(client);
3589
+
3590
+ // 握手
3591
+ reply.raw.write(`event: hello\ndata: {"ok":true}\n\n`);
3592
+
3593
+ // keep-alive
3594
+ const ka = setInterval(() => {
3595
+ try {
3596
+ reply.raw.write(': keep-alive\n\n');
3597
+ } catch {
3598
+ clearInterval(ka);
3599
+ sseClients.delete(client);
3600
+ }
3601
+ }, 15_000);
3602
+
3603
+ request.raw.on('close', () => {
3604
+ clearInterval(ka);
3605
+ sseClients.delete(client);
3606
+ });
3607
+
3608
+ return reply.hijack();
3609
+ });
3610
+
3611
+ const SSE_FALLBACK_RE = /^\/api\/(.*\/(events|stream|notifications\/stream))$/;
3612
+
3613
+ app.setNotFoundHandler((request, reply) => {
3614
+ const u = request.url;
3615
+ if (!u.startsWith('/api/')) {
3616
+ return reply.code(404).type('text/plain').send('not found');
3617
+ }
3618
+
3619
+ request.log.info({ method: request.method, url: u }, '[stub]');
3620
+
3621
+ if (request.method === 'GET' && SSE_FALLBACK_RE.test(u)) {
3622
+ reply.raw.writeHead(200, {
3623
+ 'Content-Type': 'text/event-stream; charset=utf-8',
3624
+ 'Cache-Control': 'no-cache, no-transform',
3625
+ Connection: 'keep-alive',
3626
+ 'X-Accel-Buffering': 'no',
3627
+ });
3628
+ reply.raw.write(`event: hello\ndata: {"ok":true}\n\n`);
3629
+ const ka = setInterval(() => {
3630
+ try {
3631
+ reply.raw.write(': keep-alive\n\n');
3632
+ } catch {
3633
+ clearInterval(ka);
3634
+ }
3635
+ }, 15000);
3636
+ request.raw.on('close', () => clearInterval(ka));
3637
+ return reply.hijack();
3638
+ }
3639
+
3640
+ if (request.method === 'GET') return [];
3641
+ return { ok: true };
3642
+ });
3643
+
3644
+ // ===========================================================================
3645
+ // Static resources(vite build 产物)— 必须最后注册,放在 setNotFoundHandler 之后
3646
+ // ===========================================================================
3647
+
3648
+ import { existsSync } from 'node:fs';
3649
+ if (existsSync(STATIC_DIR)) {
3650
+ await app.register(staticPlugin, {
3651
+ root: STATIC_DIR,
3652
+ prefix: '/',
3653
+ decorateReply: false,
3654
+ });
3655
+ } else {
3656
+ app.get('/', async (request, reply) => {
3657
+ if (request.url.startsWith('/api/')) return;
3658
+ reply
3659
+ .code(503)
3660
+ .type('text/plain')
3661
+ .send(`UI not built. Run: pnpm build:web (output → ${STATIC_DIR})`);
3662
+ });
3663
+ }
3664
+
3665
+ // ===========================================================================
3666
+ // Helpers
3667
+ // ===========================================================================
3668
+
3669
+ function reply500(err: unknown) {
3670
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
3671
+ }
3672
+
3673
+ // ===========================================================================
3674
+ // Start
3675
+ // ===========================================================================
3676
+
3677
+ // 启动 cc-connect Bridge WebSocket 连接(注册 platform=hermit adapter)
3678
+ bridge.start();
3679
+
3680
+ try {
3681
+ await app.listen({ host: HOST, port: PORT });
3682
+ app.log.info(
3683
+ `cc-connect: ${process.env.CC_CONNECT_BASE_URL ?? 'http://127.0.0.1:9820'}`
3684
+ );
3685
+ app.log.info(
3686
+ `bridge: ${process.env.CC_CONNECT_BRIDGE_URL ?? 'ws://127.0.0.1:9810/bridge/ws'}`
3687
+ );
3688
+ app.log.info(`static: ${STATIC_DIR}`);
3689
+ } catch (err) {
3690
+ app.log.error(err);
3691
+ process.exit(1);
3692
+ }
3693
+
3694
+ // graceful shutdown
3695
+ const shutdown = async () => {
3696
+ try {
3697
+ bridge.dispose?.();
3698
+ await app.close();
3699
+ process.exit(0);
3700
+ } catch {
3701
+ process.exit(1);
3702
+ }
3703
+ };
3704
+ process.on('SIGINT', shutdown);
3705
+ process.on('SIGTERM', shutdown);