@yancyyu/openhermit 1.6.42 → 1.6.43

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 (281) hide show
  1. package/README.md +98 -89
  2. package/bin/hermit.mjs +96 -0
  3. package/dist-renderer/assets/{ProjectEditorOverlay-DlFQ6mai.js → ProjectEditorOverlay-C98qSs7-.js} +1 -1
  4. package/dist-renderer/assets/{TeamGraphOverlay-D2TPMPGR.js → TeamGraphOverlay-CsBbZwcL.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-Cmd0RHLQ.js → _basePickBy-ZOyLWjMK.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-BI_iy8ea.js → _baseUniq-DBb726rt.js} +1 -1
  7. package/dist-renderer/assets/{arc-NzW2mjTP.js → arc-CdiTaR_R.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-Bzq85AYv.js → architectureDiagram-VXUJARFQ-Cz3sc5TH.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-D1PvYS-b.js → blockDiagram-VD42YOAC-DE4c-KJ3.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-D49RKzPC.js → c4Diagram-YG6GDRKO-CmTMDTrV.js} +1 -1
  11. package/dist-renderer/assets/channel-KTpqi9eT.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-fmI_MQmQ.js → chunk-4BX2VUAB-rhHy3tFl.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-Xsv9RCXZ.js → chunk-55IACEB6-fLZBzuo_.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-BE1KO8Um.js → chunk-B4BG7PRW-DOzxQhim.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-tqJ7Mv7f.js → chunk-DI55MBZ5-COQCcXC5.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-DMD45MVJ.js → chunk-FMBD7UC4-IKU9U_Y4.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-DOhGrz-q.js → chunk-QN33PNHL-D6WV154X.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-D8yDgJdD.js → chunk-QZHKN3VN-D90_2DQp.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-BcsEDu7A.js → chunk-TZMSLE5B-BQEil57G.js} +1 -1
  20. package/dist-renderer/assets/classDiagram-2ON5EDUG-lpzulY5X.js +1 -0
  21. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-lpzulY5X.js +1 -0
  22. package/dist-renderer/assets/clone-CriGymY9.js +1 -0
  23. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DlSqGHMX.js → cose-bilkent-S5V4N54A-6WiK6U2P.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-BTT9tSAx.js → dagre-6UL2VRFP-DF4MMuTn.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-Du-U-mK2.js → diagram-PSM6KHXK-CcF1eZ7E.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-jFdHeKas.js → diagram-QEK2KX5R-DYlOVPQB.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-DKLNK2bu.js → diagram-S2PKOQOG-BHXWsZOP.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CZxHgIIo.js → erDiagram-Q2GNP2WA-GjmuBx8d.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-v4XStCD0.js → flowDiagram-NV44I4VS-BuS7YVHk.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-DJjD_BEL.js → ganttDiagram-JELNMOA3-3Teu5tAa.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-BNy-jr03.js → gitGraphDiagram-V2S2FVAM-BiLdCYu5.js} +1 -1
  32. package/dist-renderer/assets/{graph-DDTrn6je.js → graph-CDP_R8ct.js} +1 -1
  33. package/dist-renderer/assets/{index-BBp78BAu.js → index-BSZdT-g-.js} +1 -1
  34. package/dist-renderer/assets/{index-eotrJaYy.js → index-BhWvMqsz.js} +1 -1
  35. package/dist-renderer/assets/{index-D8_B-cfs.js → index-C2_AupSj.js} +1 -1
  36. package/dist-renderer/assets/{index-BQrwHZ-k.js → index-C5ujiwAR.js} +580 -588
  37. package/dist-renderer/assets/index-CIS2CTK9.css +1 -0
  38. package/dist-renderer/assets/{index-CRKQSG9S.js → index-CVNjLwkq.js} +1 -1
  39. package/dist-renderer/assets/{index-DR6Wz52b.js → index-CwG3se0q.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DqnOsuza.js → infoDiagram-HS3SLOUP-DLHUFo72.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DTobaO1d.js → journeyDiagram-XKPGCS4Q-BE07RpJD.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-HbwVOvWc.js → kanban-definition-3W4ZIXB7-DDHZy4NB.js} +1 -1
  43. package/dist-renderer/assets/{layout--VYmTcw2.js → layout-5nA5wUxO.js} +1 -1
  44. package/dist-renderer/assets/{linear-BsJh89Mr.js → linear-BtF1i2qN.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-BZqUZePd.js → mindmap-definition-VGOIOE7T-Z1Ui9Sqy.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-B1q_nH6P.js → pieDiagram-ADFJNKIX-LCjxckWv.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-UD8QhSEu.js → quadrantDiagram-AYHSOK5B-BOwKjSco.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BA_i7Nw8.js → requirementDiagram-UZGBJVZJ-pChP8Znd.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CMTnX-2d.js → sankeyDiagram-TZEHDZUN-DifZ2qpo.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BQXDB615.js → sequenceDiagram-WL72ISMW-CJg-WYyY.js} +1 -1
  51. package/dist-renderer/assets/{splashScene-D0YB9uxm.js → splashScene-94xWCzLA.js} +1 -1
  52. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BAsPXy6X.js → stateDiagram-FKZM4ZOC-DWHOoFdv.js} +1 -1
  53. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-CGYZOoMb.js +1 -0
  54. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BdasmVkC.js → timeline-definition-IT6M3QCI-CPgokIo8.js} +1 -1
  55. package/dist-renderer/assets/{treemap-GDKQZRPO-BkKQqIui.js → treemap-GDKQZRPO-DAVqSR9L.js} +1 -1
  56. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-EAlPHOdx.js → xychartDiagram-PRI3JC2R-CCOcGbrD.js} +1 -1
  57. package/dist-renderer/chat-community-qr.jpg +0 -0
  58. package/dist-renderer/fonts/Agave-Bold.ttf +0 -0
  59. package/dist-renderer/fonts/Agave-Regular.ttf +0 -0
  60. package/dist-renderer/icon.png +0 -0
  61. package/dist-renderer/icon.rar +0 -0
  62. package/dist-renderer/index.html +3 -3
  63. package/package.json +21 -26
  64. package/src/features/worker-society/core/application/WorkerSocietyService.test.ts +802 -0
  65. package/src/features/worker-society/core/application/WorkerSocietyService.ts +428 -0
  66. package/src/features/worker-society/core/application/fakes.ts +101 -0
  67. package/src/features/worker-society/core/application/ports.ts +70 -0
  68. package/src/features/worker-society/core/domain/models/society.ts +141 -0
  69. package/src/features/worker-society/core/domain/policies/societyPolicies.test.ts +739 -0
  70. package/src/features/worker-society/core/domain/policies/societyPolicies.ts +496 -0
  71. package/src/features/worker-society/main/adapters/input/societyMcp.test.ts +317 -0
  72. package/src/features/worker-society/main/adapters/input/societyMcp.ts +257 -0
  73. package/src/features/worker-society/main/adapters/input/societyRoutes.test.ts +695 -0
  74. package/src/features/worker-society/main/adapters/input/societyRoutes.ts +194 -0
  75. package/src/features/worker-society/main/composition/societyComposition.test.ts +74 -0
  76. package/src/features/worker-society/main/composition/societyComposition.ts +70 -0
  77. package/src/features/worker-society/main/composition/workerSocietyPlugin.test.ts +69 -0
  78. package/src/features/worker-society/main/composition/workerSocietyPlugin.ts +67 -0
  79. package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.test.ts +132 -0
  80. package/src/features/worker-society/main/infrastructure/crossTeamMessageGateway.ts +84 -0
  81. package/src/features/worker-society/main/infrastructure/fsStores.test.ts +216 -0
  82. package/src/features/worker-society/main/infrastructure/fsStores.ts +113 -0
  83. package/src/features/worker-society/main/infrastructure/mergingProfileStore.test.ts +195 -0
  84. package/src/features/worker-society/main/infrastructure/mergingProfileStore.ts +96 -0
  85. package/src/features/worker-society/renderer/SocietyGraph.tsx +166 -0
  86. package/src/features/worker-society/renderer/SocietyNodeLabels.tsx +139 -0
  87. package/src/features/worker-society/renderer/SocietyNodeOverlay.tsx +339 -0
  88. package/src/features/worker-society/renderer/SocietyView.tsx +437 -0
  89. package/src/features/worker-society/renderer/index.ts +11 -0
  90. package/src/features/worker-society/renderer/societyApi.test.ts +259 -0
  91. package/src/features/worker-society/renderer/societyApi.ts +144 -0
  92. package/src/features/worker-society/renderer/societyGraphAdapter.test.ts +321 -0
  93. package/src/features/worker-society/renderer/societyGraphAdapter.ts +240 -0
  94. package/src/features/worker-society/renderer/societyOverlayActions.test.ts +57 -0
  95. package/src/features/worker-society/renderer/societyOverlayActions.ts +49 -0
  96. package/src/features/worker-society/renderer/societyStore.test.ts +218 -0
  97. package/src/features/worker-society/renderer/societyStore.ts +146 -0
  98. package/src/features/worker-society/renderer/societyViewUtils.test.ts +81 -0
  99. package/src/features/worker-society/renderer/societyViewUtils.ts +68 -0
  100. package/src/main/ipc/extensions.ts +27 -0
  101. package/src/main/server.ts +1709 -534
  102. package/src/main/services/ccConnect/CcConnectBridge.ts +26 -11
  103. package/src/main/services/ccConnect/CcConnectClient.ts +9 -2
  104. package/src/main/services/ccConnect/workDirReconcile.test.ts +57 -0
  105. package/src/main/services/ccConnect/workDirReconcile.ts +36 -0
  106. package/src/main/services/direct-cli/DirectCliSessionManager.test.ts +397 -0
  107. package/src/main/services/direct-cli/DirectCliSessionManager.ts +508 -0
  108. package/src/main/services/direct-cli/DirectCliSessionStore.test.ts +79 -0
  109. package/src/main/services/direct-cli/DirectCliSessionStore.ts +97 -0
  110. package/src/main/services/direct-cli/__tests__/directCliMessageId.test.ts +40 -0
  111. package/src/main/services/direct-cli/directCliMessageId.ts +21 -0
  112. package/src/main/services/direct-cli/index.ts +17 -0
  113. package/src/main/services/extensions/capability-packs/CapabilityPackLoaderService.ts +637 -0
  114. package/src/main/services/extensions/catalog/PluginCatalogService.ts +2 -2
  115. package/src/main/services/loop-assets/LoopAssetsScannerService.ts +657 -0
  116. package/src/main/services/runtime/providerAwareCliEnv.ts +33 -5
  117. package/src/main/services/session-intelligence/LocalSessionScanner.ts +156 -71
  118. package/src/main/services/session-intelligence/SessionUsageParser.ts +103 -8
  119. package/src/main/services/session-intelligence/UsageTelemetryService.ts +11 -0
  120. package/src/main/services/session-intelligence/__tests__/teamSessionListMapper.test.ts +104 -0
  121. package/src/main/services/session-intelligence/teamSessionListMapper.ts +78 -0
  122. package/src/main/services/system-manager/AdminLoopInitializer.ts +95 -0
  123. package/src/main/services/system-manager/BuiltinWorkflowSeeder.ts +679 -74
  124. package/src/main/services/system-manager/SystemManagerConfigService.ts +19 -1
  125. package/src/main/services/system-manager/WorkflowPromptService.ts +58 -5
  126. package/src/main/services/system-manager/__tests__/AdminLoopInitializer.test.ts +129 -0
  127. package/src/main/services/system-manager/__tests__/SystemManagerConfigService.test.ts +60 -0
  128. package/src/main/services/teams-mvp/CollaborationBoardService.ts +2 -0
  129. package/src/main/services/teams-mvp/OpsRunbookContext.ts +60 -0
  130. package/src/main/services/teams-mvp/TaskDispatchService.test.ts +305 -0
  131. package/src/main/services/teams-mvp/TaskDispatchService.ts +250 -131
  132. package/src/main/services/teams-mvp/TeamProvisioningService.ts +12 -2
  133. package/src/main/services/teams-mvp/TeamWorkspaceService.test.ts +207 -0
  134. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +104 -51
  135. package/src/main/services/teams-mvp/index.ts +6 -0
  136. package/src/main/utils/externalPlatformSessionRouting.ts +92 -0
  137. package/src/main/utils/toolApprovalRules.ts +151 -0
  138. package/src/renderer/App.tsx +24 -89
  139. package/src/renderer/api/httpClient.ts +115 -37
  140. package/src/renderer/api/providers.ts +5 -16
  141. package/src/renderer/components/chat/CommunityChatView.tsx +81 -0
  142. package/src/renderer/components/dashboard/DashboardView.tsx +130 -84
  143. package/src/renderer/components/extensions/ExtensionStoreView.tsx +39 -5
  144. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +2 -1
  145. package/src/renderer/components/extensions/capability-packs/CapabilityPacksPanel.tsx +170 -0
  146. package/src/renderer/components/layout/PaneContent.tsx +10 -2
  147. package/src/renderer/components/layout/SortableTab.tsx +4 -0
  148. package/src/renderer/components/layout/TabBarActions.tsx +13 -16
  149. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +4 -135
  150. package/src/renderer/components/schedules/SchedulesView.tsx +22 -14
  151. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +7 -6
  152. package/src/renderer/components/settings/SettingsTabs.tsx +24 -21
  153. package/src/renderer/components/settings/SettingsView.tsx +22 -13
  154. package/src/renderer/components/settings/components/SettingRow.tsx +13 -5
  155. package/src/renderer/components/settings/components/SettingsSectionCard.tsx +53 -0
  156. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +10 -6
  157. package/src/renderer/components/settings/components/SettingsSelect.tsx +12 -9
  158. package/src/renderer/components/settings/components/SettingsToggle.tsx +6 -5
  159. package/src/renderer/components/settings/components/index.ts +1 -0
  160. package/src/renderer/components/settings/sections/AdvancedSection.tsx +78 -59
  161. package/src/renderer/components/settings/sections/CliStatusSection.tsx +32 -44
  162. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  163. package/src/renderer/components/settings/sections/GeneralSection.tsx +216 -186
  164. package/src/renderer/components/settings/sections/PlatformsSection.tsx +25 -17
  165. package/src/renderer/components/settings/sections/TaskBusSection.tsx +63 -22
  166. package/src/renderer/components/sidebar/SidebarSessions.tsx +120 -80
  167. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +1 -1
  168. package/src/renderer/components/splash/splashScene.ts +6 -2
  169. package/src/renderer/components/system-manager/SystemManagerView.tsx +169 -255
  170. package/src/renderer/components/tasks/TasksView.tsx +63 -37
  171. package/src/renderer/components/team/CcSessionsSection.tsx +124 -89
  172. package/src/renderer/components/team/HarnessBrandLogos.tsx +318 -0
  173. package/src/renderer/components/team/HarnessSelect.tsx +25 -26
  174. package/src/renderer/components/team/TeamDetailView.tsx +137 -153
  175. package/src/renderer/components/team/TeamEmptyState.tsx +9 -37
  176. package/src/renderer/components/team/TeamListView.tsx +143 -30
  177. package/src/renderer/components/team/__tests__/CcSessionsSection.hasLocalFile.test.tsx +128 -0
  178. package/src/renderer/components/team/activity/ActivityItem.tsx +21 -9
  179. package/src/renderer/components/team/activity/ActivityTimeline.tsx +2 -2
  180. package/src/renderer/components/team/dialogs/AdvancedCliSection.tsx +1 -1
  181. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +13 -10
  182. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +156 -83
  183. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +9 -157
  184. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +19 -15
  185. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +48 -10
  186. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +11 -12
  187. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +39 -37
  188. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +434 -64
  189. package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +12 -10
  190. package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +2 -2
  191. package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.bindProject.test.tsx +399 -0
  192. package/src/renderer/components/team/dialogs/__tests__/CreateTeamDialog.chineseRepro.test.tsx +253 -0
  193. package/src/renderer/components/team/dialogs/platformAllowUtils.ts +91 -0
  194. package/src/renderer/components/team/dialogs/teammateRuntimeCompatibility.tsx +1 -1
  195. package/src/renderer/components/team/kanban/KanbanTaskCard.test.tsx +41 -0
  196. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +41 -86
  197. package/src/renderer/components/team/loop-console/LoopCommandComposer.tsx +310 -0
  198. package/src/renderer/components/team/loop-console/LoopConsolePanel.tsx +372 -0
  199. package/src/renderer/components/team/loop-console/loopSendIntent.test.ts +85 -0
  200. package/src/renderer/components/team/loop-console/loopSendIntent.ts +221 -0
  201. package/src/renderer/components/team/loop-console/useLeadSessionToolActivity.ts +74 -0
  202. package/src/renderer/components/team/loop-console/useLoopCommandSuggestions.ts +165 -0
  203. package/src/renderer/components/team/loop-console/useLoopConsoleController.ts +266 -0
  204. package/src/renderer/components/team/members/LeadModelRow.test.tsx +1 -1
  205. package/src/renderer/components/team/members/LeadModelRow.tsx +5 -3
  206. package/src/renderer/components/team/members/MemberDetailDialog.tsx +11 -0
  207. package/src/renderer/components/team/members/MemberDetailStats.tsx +13 -3
  208. package/src/renderer/components/team/members/MemberDraftRow.test.tsx +1 -1
  209. package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
  210. package/src/renderer/components/team/members/MemberMessagesTab.tsx +2 -2
  211. package/src/renderer/components/team/members/MemberStatsTab.tsx +1 -1
  212. package/src/renderer/components/team/messages/MessageComposer.tsx +150 -44
  213. package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +2 -2
  214. package/src/renderer/components/team/messages/MessagesPanel.tsx +34 -28
  215. package/src/renderer/components/team/schedule/CcCronScheduleDialog.tsx +6 -6
  216. package/src/renderer/components/team/taskLogs/ExactTaskLogCard.tsx +2 -2
  217. package/src/renderer/components/team/taskLogs/TaskLogStreamSection.tsx +1 -1
  218. package/src/renderer/components/terminal/TerminalPanel.tsx +2 -3
  219. package/src/renderer/components/ui/MentionableTextarea.tsx +5 -1
  220. package/src/renderer/constants/teamColors.ts +5 -5
  221. package/src/renderer/hooks/useExtensionsTabState.ts +1 -1
  222. package/src/renderer/hooks/useMentionDetection.ts +5 -1
  223. package/src/renderer/hooks/useProjectWorkflowCommands.ts +57 -0
  224. package/src/renderer/hooks/useTeamSuggestions.ts +2 -0
  225. package/src/renderer/index.css +19 -2
  226. package/src/renderer/main.tsx +7 -1
  227. package/src/renderer/store/index.ts +18 -1
  228. package/src/renderer/store/slices/extensionsSlice.ts +83 -0
  229. package/src/renderer/store/slices/tabSlice.ts +61 -0
  230. package/src/renderer/store/slices/teamSlice.ts +138 -9
  231. package/src/renderer/types/mention.ts +8 -0
  232. package/src/renderer/types/tabs.ts +3 -1
  233. package/src/renderer/utils/__tests__/bindProjectSlug.test.ts +69 -0
  234. package/src/renderer/utils/__tests__/groupTransformer.test.ts +148 -0
  235. package/src/renderer/utils/__tests__/initialRoute.test.ts +101 -0
  236. package/src/renderer/utils/__tests__/leadToolActivity.test.ts +124 -0
  237. package/src/renderer/utils/__tests__/mergeTeamMessages.test.ts +81 -0
  238. package/src/renderer/utils/__tests__/teamMessageFiltering.test.ts +213 -0
  239. package/src/renderer/utils/__tests__/teamMessageKey.test.ts +75 -0
  240. package/src/renderer/utils/__tests__/workflowCommandExecution.test.ts +173 -0
  241. package/src/renderer/utils/__tests__/workflowCommandSuggestions.test.ts +59 -0
  242. package/src/renderer/utils/bindProjectSlug.ts +57 -0
  243. package/src/renderer/utils/capabilityCommandExecution.ts +113 -0
  244. package/src/renderer/utils/initialRoute.ts +89 -0
  245. package/src/renderer/utils/leadToolActivity.ts +117 -0
  246. package/src/renderer/utils/loopShortcutSuggestions.ts +106 -0
  247. package/src/renderer/utils/mentionSuggestions.ts +1 -1
  248. package/src/renderer/utils/slashCommandRegistry.ts +231 -0
  249. package/src/renderer/utils/teamMentionDirective.ts +31 -0
  250. package/src/renderer/utils/workflowCommandExecution.ts +96 -0
  251. package/src/renderer/utils/workflowCommandSuggestions.ts +49 -0
  252. package/src/shared/types/api.ts +79 -4
  253. package/src/shared/types/ccConnect.ts +1 -0
  254. package/src/shared/types/extensions/api.ts +19 -0
  255. package/src/shared/types/extensions/capabilityPack.ts +118 -0
  256. package/src/shared/types/extensions/index.ts +29 -1
  257. package/src/shared/types/index.ts +6 -0
  258. package/src/shared/types/loopAssets.ts +54 -0
  259. package/src/shared/types/providers.ts +0 -16
  260. package/src/shared/types/systemManager.ts +26 -1
  261. package/src/shared/types/team.ts +41 -5
  262. package/src/shared/types/terminal.ts +2 -36
  263. package/src/shared/types/worker.test.ts +28 -0
  264. package/src/shared/types/worker.ts +3 -0
  265. package/src/shared/utils/__tests__/effortLevels.test.ts +88 -0
  266. package/src/shared/utils/__tests__/providerBackend.test.ts +88 -0
  267. package/src/shared/utils/__tests__/providerLaunchArgs.test.ts +220 -0
  268. package/src/shared/utils/claudeStreamJson.test.ts +187 -0
  269. package/src/shared/utils/claudeStreamJson.ts +153 -0
  270. package/src/shared/utils/providerLaunchArgs.ts +217 -0
  271. package/src/shared/utils/slashCommands.ts +10 -0
  272. package/src/types/node-pty.d.ts +8 -0
  273. package/dist-renderer/assets/channel-Ch7JrfUu.js +0 -1
  274. package/dist-renderer/assets/classDiagram-2ON5EDUG-z9I4AnFy.js +0 -1
  275. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-z9I4AnFy.js +0 -1
  276. package/dist-renderer/assets/clone-Dfi1Jx6l.js +0 -1
  277. package/dist-renderer/assets/index-iyjkpSus.css +0 -32
  278. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-DTUIBfce.js +0 -1
  279. package/src/main/services/system-manager/SystemManagerPtyService.ts +0 -233
  280. package/src/renderer/components/common/TerminalPane.tsx +0 -213
  281. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +0 -292
@@ -0,0 +1,207 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ import { TeamWorkspaceService } from './TeamWorkspaceService';
7
+ import type { GroupMessage } from './TeamWorkspaceService';
8
+
9
+ // CLAUDE.md 把 "server-side appendMessage ID propagation" 列为零覆盖的关键回归路径:
10
+ // 去重管线按 id 去重,若 appendMessage 重新生成 id,同一条逻辑消息会得到不同 id →
11
+ // 重复消息回归。这里直测真实文件 IO 实现(TaskDispatchService.test.ts 用的是 FakeWorkspace,
12
+ // 完全绕过真实落盘)。
13
+ const PREV_HERMIT_HOME = process.env.HERMIT_HOME;
14
+ let tmpHome = '';
15
+
16
+ function svc() {
17
+ return new TeamWorkspaceService();
18
+ }
19
+
20
+ afterAll(() => {
21
+ if (PREV_HERMIT_HOME === undefined) delete process.env.HERMIT_HOME;
22
+ else process.env.HERMIT_HOME = PREV_HERMIT_HOME;
23
+ });
24
+
25
+ beforeEach(() => {
26
+ tmpHome = path.join(
27
+ os.tmpdir(),
28
+ `hermit-tws-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
29
+ );
30
+ process.env.HERMIT_HOME = tmpHome;
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await fs.promises.rm(tmpHome, { recursive: true, force: true });
35
+ });
36
+
37
+ describe('TeamWorkspaceService.appendMessage — message pipeline', () => {
38
+ it('preserves a caller-supplied id (dedup guarantee: never regenerates)', async () => {
39
+ const out = await svc().appendMessage('alpha', { id: 'fixed-1', from: 'user', content: 'hi' });
40
+ expect(out.id).toBe('fixed-1');
41
+ // 第二条用相同显式 id 仍各自落盘(去重发生在渲染层,service 不负责丢弃)。
42
+ await svc().appendMessage('alpha', { id: 'fixed-1', from: 'user', content: 'dup' });
43
+ const msgs = await svc().readMessages('alpha');
44
+ expect(msgs.map((m) => m.id)).toEqual(['fixed-1', 'fixed-1']);
45
+ expect(msgs.map((m) => m.content)).toEqual(['hi', 'dup']);
46
+ });
47
+
48
+ it('generates an m_-prefixed id when none is supplied', async () => {
49
+ const out = await svc().appendMessage('alpha', { from: 'agent-7', content: 'x' });
50
+ expect(out.id).toMatch(/^m_/);
51
+ expect(out.id.length).toBeGreaterThan(4);
52
+ });
53
+
54
+ it('stamps a server-side ISO ts and defaults `to` to "team"', async () => {
55
+ const out = await svc().appendMessage('alpha', { from: 'user', content: 'x' });
56
+ expect(out.ts).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
57
+ expect(out.to).toBe('team');
58
+ });
59
+
60
+ it('derives role from `from` and honors an explicit role override', async () => {
61
+ const s = svc();
62
+ const user = await s.appendMessage('alpha', { from: 'user', content: 'u' });
63
+ const agent = await s.appendMessage('alpha', { from: 'agent-7', content: 'a' });
64
+ const system = await s.appendMessage('alpha', {
65
+ from: 'agent-7',
66
+ role: 'system',
67
+ content: 's',
68
+ });
69
+ expect(user.role).toBe('user');
70
+ expect(agent.role).toBe('agent');
71
+ expect(system.role).toBe('system');
72
+ });
73
+
74
+ it('passes meta through (null default + object preserved)', async () => {
75
+ const s = svc();
76
+ const noMeta = await s.appendMessage('alpha', { from: 'user', content: 'a' });
77
+ const withMeta = await s.appendMessage('alpha', {
78
+ from: 'user',
79
+ content: 'b',
80
+ meta: { taskId: 't_1', mentions: ['@d'] },
81
+ });
82
+ expect(noMeta.meta).toBeNull();
83
+ expect(withMeta.meta).toEqual({ taskId: 't_1', mentions: ['@d'] });
84
+ });
85
+
86
+ it('persists each message as its own newline-delimited JSONL line', async () => {
87
+ const s = svc();
88
+ await s.appendMessage('alpha', { id: 'a', from: 'user', content: 'first' });
89
+ await s.appendMessage('alpha', { id: 'b', from: 'user', content: 'second' });
90
+ const file = path.join(tmpHome, 'teams', 'alpha', 'messages', 'group.jsonl');
91
+ const raw = await fs.promises.readFile(file, 'utf8');
92
+ const lines = raw.split('\n').filter(Boolean);
93
+ expect(lines).toHaveLength(2);
94
+ expect(JSON.parse(lines[0]).id).toBe('a');
95
+ expect(JSON.parse(lines[1]).id).toBe('b');
96
+ });
97
+
98
+ it('readMessages round-trips appended messages sorted by ts', async () => {
99
+ const s = svc();
100
+ await s.appendMessage('alpha', { id: 'm1', from: 'user', content: 'one' });
101
+ await s.appendMessage('alpha', { id: 'm2', from: 'user', content: 'two' });
102
+ await s.appendMessage('alpha', { id: 'm3', from: 'user', content: 'three' });
103
+ const msgs = await s.readMessages('alpha');
104
+ expect(msgs.map((m) => m.id)).toEqual(['m1', 'm2', 'm3']);
105
+ for (let i = 1; i < msgs.length; i++) {
106
+ expect(msgs[i].ts.localeCompare(msgs[i - 1].ts)).toBeGreaterThanOrEqual(0);
107
+ }
108
+ });
109
+
110
+ it('readMessages returns [] for an external platform session slug', async () => {
111
+ await expect(svc().readMessages('feishu:some-session')).resolves.toEqual([]);
112
+ });
113
+
114
+ it('appendMessage rejects an external platform session slug', async () => {
115
+ await expect(
116
+ svc().appendMessage('feishu:some-session', { from: 'user', content: 'x' })
117
+ ).rejects.toThrow(/外部平台/);
118
+ });
119
+
120
+ it('survives a corrupted JSONL line on read (skips unparseable lines)', async () => {
121
+ const s = svc();
122
+ await s.appendMessage('alpha', { id: 'good', from: 'user', content: 'ok' });
123
+ const file = path.join(tmpHome, 'teams', 'alpha', 'messages', 'group.jsonl');
124
+ await fs.promises.appendFile(file, '{ not valid json\n');
125
+ const msgs = await s.readMessages('alpha');
126
+ expect(msgs.map((m: GroupMessage) => m.id)).toEqual(['good']);
127
+ });
128
+ });
129
+
130
+ describe('TeamWorkspaceService task board', () => {
131
+ it('createTask requires a title', async () => {
132
+ await expect(svc().createTask('alpha', { title: '' } as never)).rejects.toThrow('title');
133
+ });
134
+
135
+ it('createTask assigns incrementing order within a column and round-trips', async () => {
136
+ const s = svc();
137
+ const t1 = await s.createTask('alpha', { title: 'A' });
138
+ const t2 = await s.createTask('alpha', { title: 'B' });
139
+ const t3 = await s.createTask('alpha', { title: 'C', status: 'done' });
140
+ expect(t1.id).toMatch(/^t_/);
141
+ expect(t1.order).toBe(0);
142
+ expect(t2.order).toBe(1); // same 'todo' column increments
143
+ expect(t3.order).toBe(0); // different column ('done') starts fresh
144
+ expect(t3.status).toBe('done');
145
+ const tasks = await s.readTasks('alpha');
146
+ expect(tasks.map((t) => t.title).sort()).toEqual(['A', 'B', 'C']);
147
+ });
148
+
149
+ it('patchTask updates fields, bumps updatedAt, and pins id/teamSlug', async () => {
150
+ const s = svc();
151
+ const t = await s.createTask('alpha', { title: 'A' });
152
+ const patched = await s.patchTask('alpha', t.id, { status: 'doing', assignee: 'w1' });
153
+ expect(patched.status).toBe('doing');
154
+ expect(patched.assignee).toBe('w1');
155
+ expect(patched.id).toBe(t.id);
156
+ expect(patched.teamSlug).toBe('alpha');
157
+ expect(patched.updatedAt).not.toBe(t.updatedAt);
158
+ });
159
+
160
+ it('patchTask throws for an unknown id', async () => {
161
+ await expect(svc().patchTask('alpha', 'nope', { status: 'done' })).rejects.toThrow('not found');
162
+ });
163
+
164
+ it('deleteTask returns true when removed, false when absent, and persists', async () => {
165
+ const s = svc();
166
+ const t = await s.createTask('alpha', { title: 'A' });
167
+ expect(await s.deleteTask('alpha', 'nope')).toBe(false);
168
+ expect(await s.deleteTask('alpha', t.id)).toBe(true);
169
+ expect(await s.readTasks('alpha')).toEqual([]);
170
+ });
171
+
172
+ // F-4 (gstack-QA TEAM-010-007): a cross-team dispatch writes a received task
173
+ // onto the TARGET team's board via createOrReuseReceivedTask → createTask. The
174
+ // target team's kanban (GET /api/teams/:name/tasks → readTasks) must reflect it
175
+ // — read and write share resolveStorageSlug + teamRoot, so this is the
176
+ // read-after-write guarantee that the reported "API returns []" contradicted.
177
+ // (The cited route /api/teams/:name/board does not exist; the real route is
178
+ // /tasks. This test pins target-side visibility with the dispatchMeta intact.)
179
+ it('a dispatched received task is visible on the target team board (dispatchMeta round-trip)', async () => {
180
+ const s = svc();
181
+ const dispatchMeta = {
182
+ dispatchId: 'loop-cross-team-test-1',
183
+ originTeam: 'team-jcve',
184
+ targetTeam: 'team-4',
185
+ status: 'received' as const,
186
+ dispatchedAt: '2026-06-14T08:26:32.118Z',
187
+ receivedAt: '2026-06-14T08:26:32.118Z',
188
+ };
189
+ const created = await s.createTask('team-4', {
190
+ title: '[TEAM-010-005] cross-team dispatch arrival',
191
+ description: '@team-4 ...',
192
+ status: 'todo',
193
+ dispatchMeta,
194
+ });
195
+
196
+ // Re-read from disk the same way the board endpoint does.
197
+ const tasks = await s.readTasks('team-4');
198
+ expect(tasks).toHaveLength(1);
199
+ expect(tasks[0].id).toBe(created.id);
200
+ expect(tasks[0].status).toBe('todo');
201
+ expect(tasks[0].dispatchMeta).toEqual(dispatchMeta);
202
+
203
+ // A second, unrelated read of a different team must NOT bleed the task over
204
+ // (guards against a slug-misroute false visible/invisible).
205
+ expect(await s.readTasks('team-jcve')).toEqual([]);
206
+ });
207
+ });
@@ -12,11 +12,11 @@
12
12
  * └─ tasks/board.json # 任务看板
13
13
  */
14
14
 
15
+ import { resolveExternalPlatformSessionTeamSlug } from '@main/utils/externalPlatformSessionRouting';
16
+ import { createLogger } from '@shared/utils/logger';
15
17
  import * as fs from 'fs';
16
- import * as path from 'path';
17
18
  import * as os from 'os';
18
-
19
- import { createLogger } from '@shared/utils/logger';
19
+ import * as path from 'path';
20
20
 
21
21
  const logger = createLogger('TeamWorkspace');
22
22
 
@@ -33,7 +33,7 @@ export interface TeamManifest {
33
33
  bindProject: string;
34
34
  /** agent 类型,用于 MCP 配置注入等 harness 特定逻辑 */
35
35
  harness: string;
36
- /** agent 工作目录(cc-connect project work_dir) */
36
+ /** agent runtime 工作目录(cc-connect project work_dir) */
37
37
  workDir: string;
38
38
  color?: string;
39
39
  description?: string;
@@ -91,6 +91,7 @@ export interface GroupMessage {
91
91
  }
92
92
 
93
93
  export interface AppendGroupMessageInput {
94
+ id?: string;
94
95
  from: string;
95
96
  to?: string;
96
97
  role?: GroupMessage['role'];
@@ -136,6 +137,18 @@ export function toSlug(input: string, fallback = 'team'): string {
136
137
  return ascii || fallback;
137
138
  }
138
139
 
140
+ export function isValidBindProject(value: string): boolean {
141
+ return /^[a-z0-9][a-z0-9_-]*$/.test(value);
142
+ }
143
+
144
+ function assertValidBindProject(value: string): void {
145
+ if (!isValidBindProject(value)) {
146
+ throw new Error(
147
+ 'bindProject must contain only lowercase ASCII letters, digits, hyphens, and underscores, and start with a letter or digit'
148
+ );
149
+ }
150
+ }
151
+
139
152
  export function teamsRoot(): string {
140
153
  return path.join(hermitHome(), 'teams');
141
154
  }
@@ -144,6 +157,10 @@ export function teamRoot(teamSlug: string): string {
144
157
  return path.join(teamsRoot(), teamSlug);
145
158
  }
146
159
 
160
+ function isExternalPlatformSlug(teamSlug: string): boolean {
161
+ return /^(feishu|lark|weixin|telegram|discord|slack):/.test(teamSlug);
162
+ }
163
+
147
164
  export function groupSessionKey(teamSlug: string): string {
148
165
  return `hermit:${teamSlug}:session`;
149
166
  }
@@ -179,16 +196,46 @@ async function writeJson(p: string, data: unknown): Promise<void> {
179
196
  // ---------------------------------------------------------------------------
180
197
 
181
198
  export class TeamWorkspaceService {
199
+ private async readTeamManifestByStorageSlug(storageSlug: string): Promise<TeamManifest> {
200
+ const root = teamRoot(storageSlug);
201
+ const manifest = await readJson<TeamManifest | null>(path.join(root, 'team.json'), null);
202
+ if (!manifest) {
203
+ if (!(await pathExists(root))) {
204
+ throw new Error(`团队 "${storageSlug}" 不存在 (${root})`);
205
+ }
206
+ const stat = await fs.promises.stat(root).catch(() => null);
207
+ return {
208
+ schemaVersion: 2,
209
+ slug: storageSlug,
210
+ displayName: storageSlug,
211
+ bindProject: storageSlug,
212
+ harness: 'claudecode',
213
+ workDir: '',
214
+ collaboration: true,
215
+ rootPath: root,
216
+ createdAt: (stat?.birthtime ?? stat?.mtime ?? new Date()).toISOString(),
217
+ };
218
+ }
219
+ return manifest;
220
+ }
221
+
182
222
  private async resolveStorageSlug(teamSlug: string): Promise<string> {
183
223
  if (await pathExists(path.join(teamRoot(teamSlug), 'team.json'))) {
184
224
  return teamSlug;
185
225
  }
186
- const match = (await this.listTeams()).find((manifest) => manifest.bindProject === teamSlug);
187
- return match?.slug ?? teamSlug;
226
+ const teams = await this.listTeams();
227
+ const directMatch = teams.find((manifest) => manifest.bindProject === teamSlug);
228
+ if (directMatch) return directMatch.slug;
229
+ if (isExternalPlatformSlug(teamSlug)) {
230
+ const platformMatch = resolveExternalPlatformSessionTeamSlug(teamSlug, teams);
231
+ if (platformMatch) return platformMatch;
232
+ }
233
+ return teamSlug;
188
234
  }
189
235
 
190
- private async createUniqueStorageSlug(displayName: string): Promise<string> {
191
- const baseSlug = toSlug(displayName);
236
+ private async createUniqueStorageSlug(bindProject: string): Promise<string> {
237
+ assertValidBindProject(bindProject);
238
+ const baseSlug = bindProject;
192
239
  let slug = baseSlug;
193
240
  let suffix = 2;
194
241
  while (await pathExists(path.join(teamRoot(slug), 'team.json'))) {
@@ -201,11 +248,14 @@ export class TeamWorkspaceService {
201
248
  async createTeam(
202
249
  input: CreateTeamInput
203
250
  ): Promise<{ slug: string; root: string; manifest: TeamManifest }> {
204
- if (!input.displayName) throw new Error('displayName is required');
205
- if (!input.bindProject) throw new Error('bindProject is required');
251
+ const displayName = input.displayName.trim();
252
+ const bindProject = input.bindProject.trim();
253
+ if (!displayName) throw new Error('displayName is required');
254
+ if (!bindProject) throw new Error('bindProject is required');
206
255
  if (!input.workDir) throw new Error('workDir is required');
256
+ assertValidBindProject(bindProject);
207
257
 
208
- const slug = await this.createUniqueStorageSlug(input.bindProject);
258
+ const slug = await this.createUniqueStorageSlug(bindProject);
209
259
  const root = teamRoot(slug);
210
260
 
211
261
  await fs.promises.mkdir(root, { recursive: true });
@@ -215,8 +265,8 @@ export class TeamWorkspaceService {
215
265
  const manifest: TeamManifest = {
216
266
  schemaVersion: 2,
217
267
  slug,
218
- displayName: input.displayName,
219
- bindProject: input.bindProject,
268
+ displayName,
269
+ bindProject,
220
270
  harness: input.harness,
221
271
  workDir: input.workDir,
222
272
  color: input.color,
@@ -230,43 +280,17 @@ export class TeamWorkspaceService {
230
280
  };
231
281
 
232
282
  await writeJson(path.join(root, 'team.json'), manifest);
233
- logger.info(`created team ${slug} → cc-project:${input.bindProject}`);
283
+ logger.info(`created team ${slug} → cc-project:${bindProject}`);
234
284
  return { slug, root, manifest };
235
285
  }
236
286
 
237
287
  async readTeamManifest(teamSlug: string): Promise<TeamManifest> {
238
- const root = teamRoot(teamSlug);
239
- const manifest = await readJson<TeamManifest | null>(path.join(root, 'team.json'), null);
240
- if (!manifest) {
241
- if (!(await pathExists(root))) {
242
- throw new Error(`团队 "${teamSlug}" 不存在 (${root})`);
243
- }
244
- const stat = await fs.promises.stat(root).catch(() => null);
245
- return {
246
- schemaVersion: 2,
247
- slug: teamSlug,
248
- displayName: teamSlug,
249
- bindProject: teamSlug,
250
- harness: 'claudecode',
251
- workDir: '',
252
- collaboration: true,
253
- rootPath: root,
254
- createdAt: (stat?.birthtime ?? stat?.mtime ?? new Date()).toISOString(),
255
- };
256
- }
257
- return manifest;
288
+ const storageSlug = await this.resolveStorageSlug(teamSlug);
289
+ return this.readTeamManifestByStorageSlug(storageSlug);
258
290
  }
259
291
 
260
292
  async readTeamManifestByProject(projectName: string): Promise<TeamManifest> {
261
- try {
262
- return await this.readTeamManifest(projectName);
263
- } catch {
264
- const match = (await this.listTeams()).find(
265
- (manifest) => manifest.bindProject === projectName
266
- );
267
- if (match) return match;
268
- throw new Error(`团队 "${projectName}" 不存在 (${teamsRoot()})`);
269
- }
293
+ return this.readTeamManifest(projectName);
270
294
  }
271
295
 
272
296
  async listTeams(): Promise<TeamManifest[]> {
@@ -278,7 +302,7 @@ export class TeamWorkspaceService {
278
302
  if (!e.isDirectory()) continue;
279
303
  if (e.name.startsWith('.')) continue;
280
304
  try {
281
- out.push(await this.readTeamManifest(e.name));
305
+ out.push(await this.readTeamManifestByStorageSlug(e.name));
282
306
  } catch {
283
307
  // skip broken dirs
284
308
  }
@@ -326,19 +350,23 @@ export class TeamWorkspaceService {
326
350
  if (opts.deleteFiles) {
327
351
  await fs.promises.rm(root, { recursive: true, force: true });
328
352
  } else {
329
- const archive = path.join(teamsRoot(), `.archived-${teamSlug}-${Date.now()}`);
353
+ const archive = path.join(teamsRoot(), `.archived-${manifest.slug}-${Date.now()}`);
330
354
  await fs.promises.rename(root, archive);
331
355
  }
332
- logger.info(`deleted team ${teamSlug} (deleteFiles=${opts.deleteFiles ?? false})`);
356
+ logger.info(`deleted team ${manifest.slug} (deleteFiles=${opts.deleteFiles ?? false})`);
333
357
  }
334
358
 
335
359
  // ---- 消息记录 ----
336
360
 
337
361
  async appendMessage(teamSlug: string, msg: AppendGroupMessageInput): Promise<GroupMessage> {
338
- const file = path.join(teamRoot(teamSlug), 'messages', 'group.jsonl');
362
+ const storageSlug = await this.resolveStorageSlug(teamSlug);
363
+ if (storageSlug === teamSlug && isExternalPlatformSlug(teamSlug)) {
364
+ throw new Error(`外部平台 session_key 不能作为 Hermit team slug 写入消息: ${teamSlug}`);
365
+ }
366
+ const file = path.join(teamRoot(storageSlug), 'messages', 'group.jsonl');
339
367
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
340
368
  const entry: GroupMessage = {
341
- id: `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
369
+ id: msg.id || `m_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
342
370
  ts: new Date().toISOString(),
343
371
  from: msg.from,
344
372
  to: msg.to || 'team',
@@ -350,9 +378,7 @@ export class TeamWorkspaceService {
350
378
  return entry;
351
379
  }
352
380
 
353
- async readMessages(teamSlug: string, opts: { limit?: number } = {}): Promise<GroupMessage[]> {
354
- const limit = opts.limit ?? 200;
355
- const file = path.join(teamRoot(teamSlug), 'messages', 'group.jsonl');
381
+ private async readGroupMessageFile(file: string): Promise<GroupMessage[]> {
356
382
  let raw: string;
357
383
  try {
358
384
  raw = await fs.promises.readFile(file, 'utf8');
@@ -369,6 +395,33 @@ export class TeamWorkspaceService {
369
395
  /* skip */
370
396
  }
371
397
  }
398
+ return all;
399
+ }
400
+
401
+ private async findLegacyExternalMessageFiles(storageSlug: string): Promise<string[]> {
402
+ const dir = teamsRoot();
403
+ if (!(await pathExists(dir))) return [];
404
+ const manifests = await this.listTeams();
405
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
406
+ return entries
407
+ .filter((entry) => entry.isDirectory() && isExternalPlatformSlug(entry.name))
408
+ .filter(
409
+ (entry) => resolveExternalPlatformSessionTeamSlug(entry.name, manifests) === storageSlug
410
+ )
411
+ .map((entry) => path.join(dir, entry.name, 'messages', 'group.jsonl'));
412
+ }
413
+
414
+ async readMessages(teamSlug: string, opts: { limit?: number } = {}): Promise<GroupMessage[]> {
415
+ const limit = opts.limit ?? 200;
416
+ const storageSlug = await this.resolveStorageSlug(teamSlug);
417
+ if (storageSlug === teamSlug && isExternalPlatformSlug(teamSlug)) return [];
418
+
419
+ const files = [
420
+ path.join(teamRoot(storageSlug), 'messages', 'group.jsonl'),
421
+ ...(await this.findLegacyExternalMessageFiles(storageSlug)),
422
+ ];
423
+ const all = (await Promise.all(files.map((file) => this.readGroupMessageFile(file)))).flat();
424
+ all.sort((a, b) => a.ts.localeCompare(b.ts));
372
425
  return all.length <= limit ? all : all.slice(all.length - limit);
373
426
  }
374
427
 
@@ -25,5 +25,11 @@ export type {
25
25
  } from './TeamWorkspaceService';
26
26
 
27
27
  export { TeamProvisioningService } from './TeamProvisioningService';
28
+ export {
29
+ HERMIT_OPS_GUIDE_URL,
30
+ buildHermitOpsRunbookContext,
31
+ buildMemberWorkflowWithOpsContext,
32
+ removeHermitOpsRunbookContext,
33
+ } from './OpsRunbookContext';
28
34
 
29
35
  export { CollaborationBoardService } from './CollaborationBoardService';
@@ -0,0 +1,92 @@
1
+ import type { TeamManifest } from '@main/services/teams-mvp/TeamWorkspaceService';
2
+
3
+ const EXTERNAL_PLATFORM_SESSION_RE = /^(feishu|lark|weixin|telegram|discord|slack):/;
4
+ const FEISHU_LARK_KEYS = new Set(['feishu', 'lark']);
5
+
6
+ export interface ExternalPlatformSessionKey {
7
+ platform: string;
8
+ chatId?: string;
9
+ userId?: string;
10
+ }
11
+
12
+ export function isExternalPlatformSessionKey(sessionKey: string): boolean {
13
+ return EXTERNAL_PLATFORM_SESSION_RE.test(sessionKey);
14
+ }
15
+
16
+ export function parseExternalPlatformSessionKey(
17
+ sessionKey: string
18
+ ): ExternalPlatformSessionKey | null {
19
+ if (!isExternalPlatformSessionKey(sessionKey)) return null;
20
+ const [platform, chatId, userId] = sessionKey.split(':');
21
+ return {
22
+ platform,
23
+ chatId: chatId?.trim() || undefined,
24
+ userId: userId?.trim() || undefined,
25
+ };
26
+ }
27
+
28
+ function getPlatformAllowValue(
29
+ record: Record<string, string> | undefined,
30
+ platform: string
31
+ ): string {
32
+ if (!record) return '';
33
+ if (FEISHU_LARK_KEYS.has(platform)) {
34
+ return record[platform] ?? record[platform === 'feishu' ? 'lark' : 'feishu'] ?? '';
35
+ }
36
+ return record[platform] ?? '';
37
+ }
38
+
39
+ function matchesAllowList(allowList: string, value: string | undefined): boolean {
40
+ const trimmed = allowList.trim();
41
+ if (!trimmed) return false;
42
+ if (trimmed === '*') return true;
43
+ if (!value) return false;
44
+ return trimmed
45
+ .split(/[\s,]+/)
46
+ .map((entry) => entry.trim())
47
+ .filter(Boolean)
48
+ .includes(value);
49
+ }
50
+
51
+ function scoreManifestForSession(
52
+ manifest: TeamManifest,
53
+ parsed: ExternalPlatformSessionKey
54
+ ): number {
55
+ const allowChat = getPlatformAllowValue(manifest.platformAllowChat, parsed.platform);
56
+ const allowFrom = getPlatformAllowValue(manifest.platformAllowFrom, parsed.platform);
57
+ let score = 0;
58
+
59
+ if (allowChat) {
60
+ if (!matchesAllowList(allowChat, parsed.chatId)) return 0;
61
+ score += allowChat.trim() === '*' ? 1 : 4;
62
+ }
63
+
64
+ if (allowFrom) {
65
+ if (!matchesAllowList(allowFrom, parsed.userId)) return 0;
66
+ score += allowFrom.trim() === '*' ? 1 : 3;
67
+ }
68
+
69
+ return score;
70
+ }
71
+
72
+ /**
73
+ * Resolves an external platform session key (for example `feishu:{chat}:{user}`)
74
+ * to a Hermit-managed team slug using local allow-list metadata. Returns null
75
+ * when no team matches or when multiple teams are equally plausible.
76
+ */
77
+ export function resolveExternalPlatformSessionTeamSlug(
78
+ sessionKey: string,
79
+ manifests: readonly TeamManifest[]
80
+ ): string | null {
81
+ const parsed = parseExternalPlatformSessionKey(sessionKey);
82
+ if (!parsed) return null;
83
+
84
+ const ranked = manifests
85
+ .map((manifest) => ({ manifest, score: scoreManifestForSession(manifest, parsed) }))
86
+ .filter((entry) => entry.score > 0)
87
+ .sort((a, b) => b.score - a.score);
88
+
89
+ if (ranked.length === 0) return null;
90
+ if (ranked.length > 1 && ranked[0].score === ranked[1].score) return null;
91
+ return ranked[0].manifest.slug || ranked[0].manifest.bindProject;
92
+ }