@yancyyu/openhermit 1.6.41 → 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-Br0X83Jf.js → ProjectEditorOverlay-C98qSs7-.js} +1 -1
  4. package/dist-renderer/assets/{TeamGraphOverlay-DHMTbZPZ.js → TeamGraphOverlay-CsBbZwcL.js} +1 -1
  5. package/dist-renderer/assets/{_basePickBy-DzIiX7yH.js → _basePickBy-ZOyLWjMK.js} +1 -1
  6. package/dist-renderer/assets/{_baseUniq-6hZuzTLU.js → _baseUniq-DBb726rt.js} +1 -1
  7. package/dist-renderer/assets/{arc-CXgO6fx_.js → arc-CdiTaR_R.js} +1 -1
  8. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DKWgtDHr.js → architectureDiagram-VXUJARFQ-Cz3sc5TH.js} +1 -1
  9. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DOMUcC40.js → blockDiagram-VD42YOAC-DE4c-KJ3.js} +1 -1
  10. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B_k2L7qX.js → c4Diagram-YG6GDRKO-CmTMDTrV.js} +1 -1
  11. package/dist-renderer/assets/channel-KTpqi9eT.js +1 -0
  12. package/dist-renderer/assets/{chunk-4BX2VUAB-BeD_ccFy.js → chunk-4BX2VUAB-rhHy3tFl.js} +1 -1
  13. package/dist-renderer/assets/{chunk-55IACEB6-ClZfkA5w.js → chunk-55IACEB6-fLZBzuo_.js} +1 -1
  14. package/dist-renderer/assets/{chunk-B4BG7PRW-5XluxXsn.js → chunk-B4BG7PRW-DOzxQhim.js} +1 -1
  15. package/dist-renderer/assets/{chunk-DI55MBZ5-BzIjjNVm.js → chunk-DI55MBZ5-COQCcXC5.js} +1 -1
  16. package/dist-renderer/assets/{chunk-FMBD7UC4-HgH3MK_H.js → chunk-FMBD7UC4-IKU9U_Y4.js} +1 -1
  17. package/dist-renderer/assets/{chunk-QN33PNHL-WeC5T3Ba.js → chunk-QN33PNHL-D6WV154X.js} +1 -1
  18. package/dist-renderer/assets/{chunk-QZHKN3VN-Cu1ApHfW.js → chunk-QZHKN3VN-D90_2DQp.js} +1 -1
  19. package/dist-renderer/assets/{chunk-TZMSLE5B-BOhlynJM.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-DGZSihDQ.js → cose-bilkent-S5V4N54A-6WiK6U2P.js} +1 -1
  24. package/dist-renderer/assets/{dagre-6UL2VRFP-CnxwCbku.js → dagre-6UL2VRFP-DF4MMuTn.js} +1 -1
  25. package/dist-renderer/assets/{diagram-PSM6KHXK-DsIhoxdI.js → diagram-PSM6KHXK-CcF1eZ7E.js} +1 -1
  26. package/dist-renderer/assets/{diagram-QEK2KX5R-Cmh9KUF5.js → diagram-QEK2KX5R-DYlOVPQB.js} +1 -1
  27. package/dist-renderer/assets/{diagram-S2PKOQOG-CKxV456A.js → diagram-S2PKOQOG-BHXWsZOP.js} +1 -1
  28. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-EnvYjOjc.js → erDiagram-Q2GNP2WA-GjmuBx8d.js} +1 -1
  29. package/dist-renderer/assets/{flowDiagram-NV44I4VS-BmNeWY_A.js → flowDiagram-NV44I4VS-BuS7YVHk.js} +1 -1
  30. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D30fyK-u.js → ganttDiagram-JELNMOA3-3Teu5tAa.js} +1 -1
  31. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-CrUNiYg1.js → gitGraphDiagram-V2S2FVAM-BiLdCYu5.js} +1 -1
  32. package/dist-renderer/assets/{graph-CY1gTfTb.js → graph-CDP_R8ct.js} +1 -1
  33. package/dist-renderer/assets/{index-CaEbzwAU.js → index-BSZdT-g-.js} +1 -1
  34. package/dist-renderer/assets/{index-D5K-SjBG.js → index-BhWvMqsz.js} +1 -1
  35. package/dist-renderer/assets/{index-9_hO4N1e.js → index-C2_AupSj.js} +1 -1
  36. package/dist-renderer/assets/{index-59r209c1.js → index-C5ujiwAR.js} +580 -588
  37. package/dist-renderer/assets/index-CIS2CTK9.css +1 -0
  38. package/dist-renderer/assets/{index-DMR9B1UP.js → index-CVNjLwkq.js} +1 -1
  39. package/dist-renderer/assets/{index-BC2hXmg_.js → index-CwG3se0q.js} +1 -1
  40. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-By_XUlcD.js → infoDiagram-HS3SLOUP-DLHUFo72.js} +1 -1
  41. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BM1LJE9m.js → journeyDiagram-XKPGCS4Q-BE07RpJD.js} +1 -1
  42. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DHIW3aTA.js → kanban-definition-3W4ZIXB7-DDHZy4NB.js} +1 -1
  43. package/dist-renderer/assets/{layout-DAKiL_Mo.js → layout-5nA5wUxO.js} +1 -1
  44. package/dist-renderer/assets/{linear-DwOaRYea.js → linear-BtF1i2qN.js} +1 -1
  45. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-b7bJ2cha.js → mindmap-definition-VGOIOE7T-Z1Ui9Sqy.js} +1 -1
  46. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-DxyL9Zr2.js → pieDiagram-ADFJNKIX-LCjxckWv.js} +1 -1
  47. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CR33pHlF.js → quadrantDiagram-AYHSOK5B-BOwKjSco.js} +1 -1
  48. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-BAiSRSlh.js → requirementDiagram-UZGBJVZJ-pChP8Znd.js} +1 -1
  49. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8JmDjoa.js → sankeyDiagram-TZEHDZUN-DifZ2qpo.js} +1 -1
  50. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-c1d0Wi1m.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-nT8BiH2O.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-DpoRepUA.js → timeline-definition-IT6M3QCI-CPgokIo8.js} +1 -1
  55. package/dist-renderer/assets/{treemap-GDKQZRPO-C41UJeIH.js → treemap-GDKQZRPO-DAVqSR9L.js} +1 -1
  56. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-KMjGARKN.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 +1731 -539
  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 +744 -0
  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 +132 -52
  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 +144 -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 +189 -57
  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 +43 -3
  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-D0XS_akr.js +0 -1
  274. package/dist-renderer/assets/classDiagram-2ON5EDUG-D13Ffs0U.js +0 -1
  275. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D13Ffs0U.js +0 -1
  276. package/dist-renderer/assets/clone-B1ZrxI1D.js +0 -1
  277. package/dist-renderer/assets/index-iyjkpSus.css +0 -32
  278. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Dmibmlso.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,508 @@
1
+ /**
2
+ * Direct-CLI execution layer for in-app Loop sessions and team-member DMs.
3
+ *
4
+ * Hermit spawns the local `claude` CLI directly as a long-lived stream-json subprocess
5
+ * (one per session key), bypassing the cc-connect sidecar entirely for these surfaces.
6
+ * cc-connect stays in charge of external IM (Feishu/WeChat). Running claude directly in
7
+ * the work_dir removes the project/work_dir/platform misconfiguration that surfaced as
8
+ * "❌ 错误: 启动 Agent 会话失败".
9
+ *
10
+ * Each subprocess writes the standard `~/.claude/projects/<encoded-cwd>/<id>.jsonl`, so
11
+ * the existing tool-activity / chunk / context views (LocalSessionScanner) keep working
12
+ * with no changes. We only relay the live stream over SSE for token-level display.
13
+ */
14
+
15
+ import { EventEmitter } from 'events';
16
+
17
+ import type { ChildProcess, SpawnOptions } from 'child_process';
18
+ import { randomUUID } from 'crypto';
19
+
20
+ import { classifyClaudeStreamLine, type ClaudeStreamLine } from '@shared/utils/claudeStreamJson';
21
+
22
+ import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
23
+ import { spawnCli } from '@main/utils/childProcess';
24
+ import { type DirectCliSessionRepository, DirectCliSessionStore } from './DirectCliSessionStore';
25
+
26
+ /** Args mirror the cc-connect claudecode invocation that this replaces. */
27
+ export interface ClaudeStreamArgsOptions {
28
+ resumeSessionId?: string;
29
+ appendSystemPrompt?: string;
30
+ verbose?: boolean;
31
+ /** Provider-resolved args (model, effort, flags) from buildProviderAwareCliEnv. */
32
+ providerArgs?: string[];
33
+ }
34
+
35
+ /**
36
+ * Build the argv for `claude --output-format stream-json ...`. Pure + tested separately
37
+ * so the spawn wiring never needs to launch a real process to verify its flags.
38
+ */
39
+ export function buildClaudeStreamArgs(options: ClaudeStreamArgsOptions = {}): string[] {
40
+ const args = [
41
+ '--output-format',
42
+ 'stream-json',
43
+ '--input-format',
44
+ 'stream-json',
45
+ '--permission-prompt-tool',
46
+ 'stdio',
47
+ ];
48
+ // --verbose makes claude flush assistant events as they stream (granular deltas). It only
49
+ // conflicts with --output-format stream-json when a router_url is set, which we never do.
50
+ if (options.verbose !== false) args.push('--verbose');
51
+ if (options.resumeSessionId?.trim()) {
52
+ args.push('--resume', options.resumeSessionId.trim());
53
+ }
54
+ if (options.appendSystemPrompt?.trim()) {
55
+ args.push('--append-system-prompt', options.appendSystemPrompt.trim());
56
+ }
57
+ if (options.providerArgs?.length) {
58
+ args.push(...options.providerArgs);
59
+ }
60
+ return args;
61
+ }
62
+
63
+ /**
64
+ * Format a user turn as the NDJSON line claude's stream-json stdin expects.
65
+ * Mirrors what cc-connect writes to the harness stdin.
66
+ */
67
+ export function formatClaudeStdinUserTurn(text: string): string {
68
+ return (
69
+ JSON.stringify({
70
+ type: 'user',
71
+ message: {
72
+ role: 'user',
73
+ content: [{ type: 'text', text }],
74
+ },
75
+ }) + '\n'
76
+ );
77
+ }
78
+
79
+ export interface DirectCliSpawnParams {
80
+ sessionKey: string;
81
+ workDir: string;
82
+ resumeSessionId?: string;
83
+ appendSystemPrompt?: string;
84
+ model?: string | null;
85
+ providerId?: string;
86
+ providerBackendId?: string | null;
87
+ verbose?: boolean;
88
+ }
89
+
90
+ export interface DirectCliSendParams {
91
+ text: string;
92
+ /** Optimistic id used to route stream deltas to the right in-progress message. */
93
+ messageId: string;
94
+ /** cwd used to (lazily) spawn the subprocess if the session doesn't exist yet. */
95
+ workDir: string;
96
+ }
97
+
98
+ export type DirectCliEvent =
99
+ | { kind: 'init'; sessionKey: string; sessionId: string; model?: string }
100
+ | { kind: 'delta'; sessionKey: string; messageId: string; text: string }
101
+ | { kind: 'thinking'; sessionKey: string; messageId: string; text: string }
102
+ | { kind: 'tool'; sessionKey: string; messageId: string; toolName: string; toolInput: unknown }
103
+ | {
104
+ kind: 'complete';
105
+ sessionKey: string;
106
+ messageId: string;
107
+ text: string;
108
+ sessionId?: string;
109
+ }
110
+ | {
111
+ kind: 'permission-request';
112
+ sessionKey: string;
113
+ /** Stable per-spawn id; changes when the subprocess is respawned so stale approvals
114
+ * can be dismissed by runId after a stop→launch race. */
115
+ runId: string;
116
+ requestId: string;
117
+ subtype?: string;
118
+ toolName?: string;
119
+ toolInput?: Record<string, unknown>;
120
+ }
121
+ | { kind: 'error'; sessionKey: string; messageId?: string; error: string };
122
+
123
+ interface CliSessionHandle {
124
+ child: ChildProcess;
125
+ sessionId?: string;
126
+ /** Per-spawn id threaded onto permission-request events so stale approvals are
127
+ * dismissible after a respawn. */
128
+ runId: string;
129
+ activeMessageId?: string;
130
+ /** Accumulated assistant text for the in-flight turn (fallback if result has none). */
131
+ accumulatedText: string;
132
+ /** Half-finished stdout line pending a newline. */
133
+ stdoutBuffer: string;
134
+ /** True after the process exited; guards against writing to a dead stdin. */
135
+ closed: boolean;
136
+ }
137
+
138
+ /** Spawn function signature (mockable in tests). */
139
+ export type DirectCliSpawnFn = (
140
+ binaryPath: string,
141
+ args: string[],
142
+ options: SpawnOptions
143
+ ) => ChildProcess;
144
+
145
+ /** Provider env resolver (mockable in tests). */
146
+ export interface DirectCliEnvResolver {
147
+ (params: {
148
+ binaryPath: string | null;
149
+ providerId?: string;
150
+ providerBackendId?: string | null;
151
+ model?: string | null;
152
+ projectPath?: string;
153
+ }): Promise<{ env: NodeJS.ProcessEnv; providerArgs: string[] }>;
154
+ }
155
+
156
+ export interface DirectCliSessionManagerOptions {
157
+ spawnFn?: DirectCliSpawnFn;
158
+ envResolver?: DirectCliEnvResolver;
159
+ binaryResolver?: typeof ClaudeBinaryResolver;
160
+ store?: DirectCliSessionRepository;
161
+ }
162
+
163
+ const DEFAULT_ENV_RESOLVER: DirectCliEnvResolver = async (params) => {
164
+ // Imported lazily so the manager module stays cheap and unit-testable without the
165
+ // credential/provider service graph.
166
+ const { buildProviderAwareCliEnv } = await import('@main/services/runtime/providerAwareCliEnv');
167
+ const result = await buildProviderAwareCliEnv({
168
+ binaryPath: params.binaryPath,
169
+ providerId: params.providerId,
170
+ providerBackendId: params.providerBackendId ?? null,
171
+ model: params.model ?? null,
172
+ projectPath: params.projectPath,
173
+ });
174
+ return { env: result.env, providerArgs: result.providerArgs };
175
+ };
176
+
177
+ export class DirectCliSessionManager extends EventEmitter {
178
+ private readonly sessions = new Map<string, CliSessionHandle>();
179
+
180
+ /** In-flight ensureSession promises dedupe concurrent callers for the same key. */
181
+ private readonly ensuring = new Map<string, Promise<void>>();
182
+
183
+ private readonly spawnFn: DirectCliSpawnFn;
184
+
185
+ private readonly envResolver: DirectCliEnvResolver;
186
+
187
+ private readonly binaryResolver: typeof ClaudeBinaryResolver;
188
+
189
+ private readonly store: DirectCliSessionRepository;
190
+
191
+ constructor(options: DirectCliSessionManagerOptions = {}) {
192
+ super();
193
+ this.spawnFn =
194
+ options.spawnFn ?? ((binaryPath, args, opts) => spawnCli(binaryPath, args, opts));
195
+ this.envResolver = options.envResolver ?? DEFAULT_ENV_RESOLVER;
196
+ this.binaryResolver = options.binaryResolver ?? ClaudeBinaryResolver;
197
+ this.store = options.store ?? new DirectCliSessionStore();
198
+ }
199
+
200
+ has(sessionKey: string): boolean {
201
+ return this.sessions.has(sessionKey);
202
+ }
203
+
204
+ getSessionId(sessionKey: string): string | undefined {
205
+ return this.sessions.get(sessionKey)?.sessionId ?? this.store.get(sessionKey);
206
+ }
207
+
208
+ /**
209
+ * Ensure a subprocess exists for `sessionKey`, spawning lazily. Resolves once the
210
+ * process is running (NOT once claude is ready — the first `session-init` event
211
+ * signals readiness). Safe to call concurrently; duplicate callers await the same spawn.
212
+ */
213
+ async ensureSession(params: DirectCliSpawnParams): Promise<void> {
214
+ const sessionKey = params.sessionKey.trim();
215
+ if (this.sessions.has(sessionKey)) return;
216
+ const inFlight = this.ensuring.get(sessionKey);
217
+ if (inFlight) return inFlight;
218
+
219
+ const promise = this.spawnSession(sessionKey, params)
220
+ .catch((err) => {
221
+ // Surface the failure to SSE listeners, then re-throw so callers (send) know the
222
+ // spawn failed and the session is unusable.
223
+ const error = err instanceof Error ? err.message : String(err);
224
+ this.emit('event', { kind: 'error', sessionKey, error } satisfies DirectCliEvent);
225
+ throw err;
226
+ })
227
+ .finally(() => {
228
+ // Clear the in-flight guard so a later retry can spawn again.
229
+ this.ensuring.delete(sessionKey);
230
+ });
231
+ this.ensuring.set(sessionKey, promise);
232
+ await promise;
233
+ }
234
+
235
+ private async spawnSession(sessionKey: string, params: DirectCliSpawnParams): Promise<void> {
236
+ const workDir = params.workDir.trim();
237
+ if (!workDir) throw new Error('direct-cli: workDir is required to spawn a agent session');
238
+
239
+ const binaryPath = await this.binaryResolver.resolve();
240
+ if (!binaryPath) {
241
+ throw new Error('未找到本地 claude CLI,无法启动直连会话');
242
+ }
243
+
244
+ // Prefer a persisted session id (resume continuity across Hermit restarts); fall back
245
+ // to the caller-provided resumeSessionId only if the store has nothing yet.
246
+ const resumeSessionId = this.store.get(sessionKey) ?? params.resumeSessionId;
247
+
248
+ const { env, providerArgs } = await this.envResolver({
249
+ binaryPath,
250
+ providerId: params.providerId,
251
+ providerBackendId: params.providerBackendId,
252
+ model: params.model ?? null,
253
+ projectPath: workDir,
254
+ });
255
+
256
+ const args = buildClaudeStreamArgs({
257
+ resumeSessionId,
258
+ appendSystemPrompt: params.appendSystemPrompt,
259
+ verbose: params.verbose,
260
+ providerArgs,
261
+ });
262
+
263
+ const child = this.spawnFn(binaryPath, args, {
264
+ cwd: workDir,
265
+ env,
266
+ stdio: ['pipe', 'pipe', 'pipe'],
267
+ });
268
+
269
+ const handle: CliSessionHandle = {
270
+ child,
271
+ runId: randomUUID(),
272
+ accumulatedText: '',
273
+ stdoutBuffer: '',
274
+ closed: false,
275
+ };
276
+ this.sessions.set(sessionKey, handle);
277
+ this.attachListeners(sessionKey, handle);
278
+ }
279
+
280
+ private attachListeners(sessionKey: string, handle: CliSessionHandle): void {
281
+ const { child } = handle;
282
+ if (typeof child.stdout?.setEncoding === 'function') child.stdout.setEncoding('utf-8');
283
+ child.stdout?.on('data', (chunk: string) => this.onStdout(sessionKey, handle, chunk));
284
+ if (typeof child.stderr?.setEncoding === 'function') child.stderr.setEncoding('utf-8');
285
+ child.stderr?.on('data', () => {
286
+ // Stderr is informational (claude progress/debug). Not surfaced as message content.
287
+ });
288
+ child.on('error', (err) => {
289
+ handle.closed = true;
290
+ this.emit('event', {
291
+ kind: 'error',
292
+ sessionKey,
293
+ error: err.message,
294
+ } satisfies DirectCliEvent);
295
+ });
296
+ child.on('exit', (code) => {
297
+ handle.closed = true;
298
+ // Flush any trailing stdout line that never got a newline.
299
+ if (handle.stdoutBuffer.trim()) {
300
+ this.processLine(sessionKey, handle, handle.stdoutBuffer);
301
+ handle.stdoutBuffer = '';
302
+ }
303
+ // Resolve any in-flight turn so the renderer's optimistic bubble can't hang
304
+ // forever. If a `result` already arrived, `activeMessageId` was cleared and
305
+ // nothing fires here. A clean exit (code 0) with no `result` (e.g. claude
306
+ // bailed after a permission prompt) still needs a terminal `complete`.
307
+ if (handle.activeMessageId) {
308
+ if (code !== null && code !== 0) {
309
+ this.emit('event', {
310
+ kind: 'error',
311
+ sessionKey,
312
+ messageId: handle.activeMessageId,
313
+ error: `claude 进程退出(code ${code})`,
314
+ } satisfies DirectCliEvent);
315
+ } else if (code === 0) {
316
+ this.emit('event', {
317
+ kind: 'complete',
318
+ sessionKey,
319
+ messageId: handle.activeMessageId,
320
+ text: handle.accumulatedText,
321
+ sessionId: handle.sessionId,
322
+ } satisfies DirectCliEvent);
323
+ }
324
+ handle.activeMessageId = undefined;
325
+ handle.accumulatedText = '';
326
+ }
327
+ this.sessions.delete(sessionKey);
328
+ });
329
+ }
330
+
331
+ private onStdout(sessionKey: string, handle: CliSessionHandle, chunk: string): void {
332
+ handle.stdoutBuffer += chunk;
333
+ let newlineIndex = handle.stdoutBuffer.indexOf('\n');
334
+ while (newlineIndex !== -1) {
335
+ const line = handle.stdoutBuffer.slice(0, newlineIndex);
336
+ handle.stdoutBuffer = handle.stdoutBuffer.slice(newlineIndex + 1);
337
+ this.processLine(sessionKey, handle, line);
338
+ newlineIndex = handle.stdoutBuffer.indexOf('\n');
339
+ }
340
+ }
341
+
342
+ private processLine(sessionKey: string, handle: CliSessionHandle, rawLine: string): void {
343
+ const classified: ClaudeStreamLine | null = classifyClaudeStreamLine(rawLine);
344
+ if (!classified) return;
345
+
346
+ switch (classified.type) {
347
+ case 'session-init': {
348
+ handle.sessionId = classified.sessionId;
349
+ this.store.set(sessionKey, classified.sessionId);
350
+ this.emit('event', {
351
+ kind: 'init',
352
+ sessionKey,
353
+ sessionId: classified.sessionId,
354
+ model: classified.model,
355
+ } satisfies DirectCliEvent);
356
+ break;
357
+ }
358
+ case 'assistant': {
359
+ const messageId = handle.activeMessageId ?? '';
360
+ for (const block of classified.blocks) {
361
+ if (block.kind === 'text' && block.text) {
362
+ handle.accumulatedText += block.text;
363
+ this.emit('event', {
364
+ kind: 'delta',
365
+ sessionKey,
366
+ messageId,
367
+ text: block.text,
368
+ } satisfies DirectCliEvent);
369
+ } else if (block.kind === 'thinking' && block.text) {
370
+ this.emit('event', {
371
+ kind: 'thinking',
372
+ sessionKey,
373
+ messageId,
374
+ text: block.text,
375
+ } satisfies DirectCliEvent);
376
+ } else if (block.kind === 'tool-use') {
377
+ this.emit('event', {
378
+ kind: 'tool',
379
+ sessionKey,
380
+ messageId,
381
+ toolName: block.toolName ?? 'Unknown',
382
+ toolInput: block.toolInput,
383
+ } satisfies DirectCliEvent);
384
+ }
385
+ }
386
+ break;
387
+ }
388
+ case 'result': {
389
+ const messageId = handle.activeMessageId ?? '';
390
+ const text = classified.text || handle.accumulatedText;
391
+ this.emit('event', {
392
+ kind: 'complete',
393
+ sessionKey,
394
+ messageId,
395
+ text,
396
+ sessionId: classified.sessionId ?? handle.sessionId,
397
+ } satisfies DirectCliEvent);
398
+ handle.activeMessageId = undefined;
399
+ handle.accumulatedText = '';
400
+ break;
401
+ }
402
+ case 'control-request': {
403
+ // A tool needs interactive approval (`--permission-prompt-tool stdio`). Surface it
404
+ // so server.ts can render the approval sheet and write the control_response back.
405
+ // Without this the CLI blocks on stdin forever and the turn never emits `result`.
406
+ if (classified.requestId) {
407
+ this.emit('event', {
408
+ kind: 'permission-request',
409
+ sessionKey,
410
+ runId: handle.runId,
411
+ requestId: classified.requestId,
412
+ subtype: classified.subtype,
413
+ toolName: classified.toolName,
414
+ toolInput: classified.toolInput,
415
+ } satisfies DirectCliEvent);
416
+ }
417
+ break;
418
+ }
419
+ case 'unknown':
420
+ case 'parse-error':
421
+ default:
422
+ // parse-errors/unknown lines are ignored to avoid flooding the feed with raw stdout.
423
+ break;
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Send a user turn to an existing (or about-to-be-spawned) session and tag the
429
+ * resulting stream with `messageId` until the `result` event arrives.
430
+ */
431
+ async send(sessionKey: string, params: DirectCliSendParams): Promise<void> {
432
+ const key = sessionKey.trim();
433
+ await this.ensureSession({ sessionKey: key, workDir: params.workDir });
434
+ const handle = this.sessions.get(key);
435
+ if (!handle) {
436
+ throw new Error(`direct-cli: session ${key} is not running`);
437
+ }
438
+ if (handle.closed || !handle.child.stdin || handle.child.stdin.destroyed) {
439
+ throw new Error(`direct-cli: session ${key} stdin is closed`);
440
+ }
441
+ handle.activeMessageId = params.messageId;
442
+ handle.accumulatedText = '';
443
+ handle.child.stdin.write(formatClaudeStdinUserTurn(params.text));
444
+ }
445
+
446
+ /** Per-spawn run id for a live session (for dismissing stale approvals on respawn). */
447
+ getRunId(sessionKey: string): string | undefined {
448
+ return this.sessions.get(sessionKey.trim())?.runId;
449
+ }
450
+
451
+ /**
452
+ * Answer a `permission-request` (control_request) by writing a `control_response` line to
453
+ * the subprocess stdin. This unblocks the turn so the CLI can run the tool (allow) or
454
+ * skip it (deny) and eventually emit the `result` that persists the reply.
455
+ *
456
+ * `updatedInput` carries the user's answers for `AskUserQuestion` (mirrors the multi-agent
457
+ * reference impl: allow responses pass `{...toolInput, answers}` so the CLI delivers them
458
+ * without re-prompting). Omit it for ordinary Allow.
459
+ */
460
+ respondPermission(
461
+ sessionKey: string,
462
+ requestId: string,
463
+ allow: boolean,
464
+ message?: string,
465
+ updatedInput?: Record<string, unknown>
466
+ ): void {
467
+ const handle = this.sessions.get(sessionKey.trim());
468
+ if (!handle) {
469
+ throw new Error(`direct-cli: session ${sessionKey.trim()} is not running`);
470
+ }
471
+ if (handle.closed || !handle.child.stdin || handle.child.stdin.destroyed) {
472
+ throw new Error(`direct-cli: session ${sessionKey.trim()} stdin is closed`);
473
+ }
474
+ // Wire format verified against the working multi-agent reference impl:
475
+ // { type:'control_response', response:{ subtype:'success', request_id, response:{behavior, ...} } }
476
+ const innerResponse: Record<string, unknown> = allow
477
+ ? { behavior: 'allow', updatedInput: updatedInput ?? {} }
478
+ : { behavior: 'deny', message: message ?? 'User denied' };
479
+ const response = {
480
+ type: 'control_response',
481
+ response: {
482
+ subtype: 'success',
483
+ request_id: requestId,
484
+ response: innerResponse,
485
+ },
486
+ };
487
+ handle.child.stdin.write(JSON.stringify(response) + '\n');
488
+ }
489
+
490
+ kill(sessionKey: string): void {
491
+ const handle = this.sessions.get(sessionKey.trim());
492
+ if (!handle) return;
493
+ handle.closed = true;
494
+ try {
495
+ handle.child.kill('SIGTERM');
496
+ } catch {
497
+ // Best effort.
498
+ }
499
+ this.sessions.delete(sessionKey.trim());
500
+ }
501
+
502
+ /** Reap every live subprocess. Call on app before-quit. */
503
+ shutdown(): void {
504
+ for (const key of Array.from(this.sessions.keys())) {
505
+ this.kill(key);
506
+ }
507
+ }
508
+ }
@@ -0,0 +1,79 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { DirectCliSessionStore } from './DirectCliSessionStore';
8
+
9
+ describe('DirectCliSessionStore', () => {
10
+ let tmpDir: string;
11
+ let filePath: string;
12
+
13
+ beforeEach(() => {
14
+ tmpDir = mkdtempSync(path.join(os.tmpdir(), 'direct-cli-store-'));
15
+ filePath = path.join(tmpDir, 'sessions.json');
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(tmpDir, { recursive: true, force: true });
20
+ });
21
+
22
+ it('returns undefined for unknown keys on a fresh store (no file)', () => {
23
+ const store = new DirectCliSessionStore(filePath);
24
+ expect(store.get('team-x:lead')).toBeUndefined();
25
+ expect(store.has('team-x:lead')).toBe(false);
26
+ });
27
+
28
+ it('persists a session id and reads it back within the same instance', () => {
29
+ const store = new DirectCliSessionStore(filePath);
30
+ store.set('team-x:lead', 'claude-sid-1');
31
+ expect(store.get('team-x:lead')).toBe('claude-sid-1');
32
+ expect(store.has('team-x:lead')).toBe(true);
33
+ });
34
+
35
+ it('survives across instances — round-trips through the JSON file', () => {
36
+ new DirectCliSessionStore(filePath).set('team-x:lead', 'claude-sid-1');
37
+ new DirectCliSessionStore(filePath).set('team-x:member:爬虫', 'claude-sid-2');
38
+
39
+ const reopened = new DirectCliSessionStore(filePath);
40
+ expect(reopened.get('team-x:lead')).toBe('claude-sid-1');
41
+ expect(reopened.get('team-x:member:爬虫')).toBe('claude-sid-2');
42
+ expect(reopened.has('team-x:lead')).toBe(true);
43
+
44
+ // File is valid JSON we can read directly.
45
+ const onDisk = JSON.parse(readFileSync(filePath, 'utf-8'));
46
+ expect(onDisk).toEqual({
47
+ 'team-x:lead': 'claude-sid-1',
48
+ 'team-x:member:爬虫': 'claude-sid-2',
49
+ });
50
+ });
51
+
52
+ it('deletes a key and removes it from the file', () => {
53
+ const store = new DirectCliSessionStore(filePath);
54
+ store.set('team-x:lead', 'claude-sid-1');
55
+ store.delete('team-x:lead');
56
+ expect(store.get('team-x:lead')).toBeUndefined();
57
+ expect(store.has('team-x:lead')).toBe(false);
58
+ expect(JSON.parse(readFileSync(filePath, 'utf-8'))).toEqual({});
59
+ });
60
+
61
+ it('ignores empty / whitespace-only keys or session ids', () => {
62
+ const store = new DirectCliSessionStore(filePath);
63
+ store.set('', 'sid');
64
+ store.set(' ', 'sid');
65
+ store.set('team-x:lead', ' ');
66
+ store.set('team-x:lead', '');
67
+ expect(store.all()).toEqual({});
68
+ });
69
+
70
+ it('recovers from a corrupt sessions.json instead of throwing', () => {
71
+ writeFileSync(filePath, '{ not valid json', 'utf-8');
72
+ const store = new DirectCliSessionStore(filePath);
73
+ expect(store.get('anything')).toBeUndefined();
74
+ // A subsequent set should rewrite a clean file.
75
+ store.set('team-x:lead', 'sid-after-corruption');
76
+ expect(store.get('team-x:lead')).toBe('sid-after-corruption');
77
+ expect(() => JSON.parse(readFileSync(filePath, 'utf-8'))).not.toThrow();
78
+ });
79
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Persists the `DirectCliSessionManager`'s `sessionKey → claude session_id` mapping so
3
+ * a direct-CLI session can `--resume` the same claude conversation across Hermit restarts.
4
+ *
5
+ * Lives behind a small repository interface (CLAUDE.md storage guidance: the manager
6
+ * depends on this abstraction, not on the file format). The default implementation is a
7
+ * single JSON file under `~/.hermit/direct-cli/sessions.json`, mirroring how other Hermit
8
+ * stores (collaboration board, team manifests) persist under `~/.hermit`.
9
+ */
10
+
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
12
+ import os from 'os';
13
+ import path from 'path';
14
+
15
+ const HERMIT_HOME = process.env.HERMIT_HOME ?? path.join(os.homedir(), '.hermit');
16
+ export const DEFAULT_DIRECT_CLI_SESSIONS_FILE = path.join(
17
+ HERMIT_HOME,
18
+ 'direct-cli',
19
+ 'sessions.json'
20
+ );
21
+
22
+ /** Repository interface the {@link DirectCliSessionManager} depends on. */
23
+ export interface DirectCliSessionRepository {
24
+ get(sessionKey: string): string | undefined;
25
+ set(sessionKey: string, sessionId: string): void;
26
+ has(sessionKey: string): boolean;
27
+ delete(sessionKey: string): void;
28
+ }
29
+
30
+ export class DirectCliSessionStore implements DirectCliSessionRepository {
31
+ private readonly filePath: string;
32
+
33
+ private cache: Record<string, string> | undefined;
34
+
35
+ constructor(filePath: string = DEFAULT_DIRECT_CLI_SESSIONS_FILE) {
36
+ this.filePath = filePath;
37
+ }
38
+
39
+ get(sessionKey: string): string | undefined {
40
+ return this.load()[sessionKey];
41
+ }
42
+
43
+ set(sessionKey: string, sessionId: string): void {
44
+ const trimmedKey = sessionKey.trim();
45
+ const trimmedId = sessionId.trim();
46
+ if (!trimmedKey || !trimmedId) return;
47
+ const data = this.load();
48
+ data[trimmedKey] = trimmedId;
49
+ this.persist(data);
50
+ }
51
+
52
+ has(sessionKey: string): boolean {
53
+ return this.get(sessionKey) !== undefined;
54
+ }
55
+
56
+ delete(sessionKey: string): void {
57
+ const data = this.load();
58
+ if (delete data[sessionKey.trim()]) {
59
+ this.persist(data);
60
+ }
61
+ }
62
+
63
+ /** Test helper: full mapping snapshot. */
64
+ all(): Record<string, string> {
65
+ return { ...this.load() };
66
+ }
67
+
68
+ private load(): Record<string, string> {
69
+ if (this.cache) return this.cache;
70
+ try {
71
+ if (existsSync(this.filePath)) {
72
+ const raw = readFileSync(this.filePath, 'utf-8');
73
+ const parsed: unknown = raw.trim() ? JSON.parse(raw) : {};
74
+ this.cache =
75
+ parsed && typeof parsed === 'object' && !Array.isArray(parsed)
76
+ ? (parsed as Record<string, string>)
77
+ : {};
78
+ } else {
79
+ this.cache = {};
80
+ }
81
+ } catch {
82
+ // Corrupt or unreadable file — start fresh rather than crash the manager.
83
+ this.cache = {};
84
+ }
85
+ return this.cache;
86
+ }
87
+
88
+ private persist(data: Record<string, string>): void {
89
+ this.cache = data;
90
+ try {
91
+ mkdirSync(path.dirname(this.filePath), { recursive: true });
92
+ writeFileSync(this.filePath, JSON.stringify(data, null, 2), 'utf-8');
93
+ } catch {
94
+ // Persistence is best-effort: a failed write only costs resume continuity.
95
+ }
96
+ }
97
+ }