@yancyyu/openhermit 1.6.38 → 1.6.40

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 (243) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-CemDOX-3.js +58 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-ZEDfZyHb.js → TeamGraphOverlay-hPY770Db.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CIhniz70.js → _basePickBy-BHHrJT1i.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-cKAW4Q8I.js → _baseUniq-CWErBtke.js} +1 -1
  5. package/dist-renderer/assets/{arc-YmNsoDXW.js → arc-C_o2_Uv8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DHEls2sX.js → architectureDiagram-VXUJARFQ-DUW0LI3t.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-Bpwf1Sbg.js → blockDiagram-VD42YOAC-CWbCE9hQ.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-B0IaQ4w5.js → c4Diagram-YG6GDRKO-BjLadrfV.js} +1 -1
  9. package/dist-renderer/assets/channel-DyP9YlCF.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLk-hcFc.js → chunk-4BX2VUAB-CPnvjZl9.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-1XRmX_Zm.js → chunk-55IACEB6-OlL47yXQ.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-1waH1DAD.js → chunk-B4BG7PRW-DTasjbm8.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-BqpZBtrN.js → chunk-DI55MBZ5-C5_Xaqkk.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-Bly7vVym.js → chunk-FMBD7UC4-NdoM4DMR.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-Ci2QWBAs.js → chunk-QN33PNHL-C8Fybejy.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-YCqFW7d-.js → chunk-QZHKN3VN-E98TYFXJ.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-B0xGXInl.js → chunk-TZMSLE5B-h4lFgkIq.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-BqffFTae.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-BqffFTae.js +1 -0
  20. package/dist-renderer/assets/clone-MPcKWs2O.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-DxcFNQKT.js → cose-bilkent-S5V4N54A-DtQ7fkrs.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DPo_RfZY.js → dagre-6UL2VRFP-CN-nL_z4.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-U3hQsFe4.js → diagram-PSM6KHXK-DVJtqmm-.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-OrwrAy0V.js → diagram-QEK2KX5R-DlxHxyXh.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CXATPWVw.js → diagram-S2PKOQOG-7dpzO6x6.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-B0e8AfMF.js → erDiagram-Q2GNP2WA-GP1TqsHi.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-CXfzA4jJ.js → flowDiagram-NV44I4VS-C7ZLETuH.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-CMr08qVl.js → ganttDiagram-JELNMOA3-CvPB68dH.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-vYFHpPmy.js → gitGraphDiagram-V2S2FVAM-B5yOm3w7.js} +1 -1
  30. package/dist-renderer/assets/{graph-DOe5j8dH.js → graph-smeyY1YZ.js} +1 -1
  31. package/dist-renderer/assets/{index-BySQS7AB.js → index-BJx8XvG1.js} +1 -1
  32. package/dist-renderer/assets/{index-C_okzZXP.js → index-CQaXUAua.js} +1 -1
  33. package/dist-renderer/assets/{index-VJ-MM9xa.js → index-CajRpxO2.js} +1 -1
  34. package/dist-renderer/assets/{index-V7dAKPqd.js → index-ChG4rE-E.js} +587 -705
  35. package/dist-renderer/assets/index-DUd0uw9C.css +32 -0
  36. package/dist-renderer/assets/{index-CzWxVCRL.js → index-IhmXZWqf.js} +1 -1
  37. package/dist-renderer/assets/{index-B2Dy7M2G.js → index-x_JkoDRH.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D_WubR0B.js → infoDiagram-HS3SLOUP-D-hWRQGY.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-w9ca-1TI.js → journeyDiagram-XKPGCS4Q-Bb6W8rUG.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-Jg9p6_pN.js → kanban-definition-3W4ZIXB7-CnHdUX0q.js} +1 -1
  41. package/dist-renderer/assets/{layout-B-z3y17c.js → layout-pqss_zkI.js} +1 -1
  42. package/dist-renderer/assets/{linear-D-RTX5UW.js → linear-B1mFITNh.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-CDQmHOYP.js → mindmap-definition-VGOIOE7T-DTD9q7-D.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-D_odsQL7.js → pieDiagram-ADFJNKIX-Df3mhrn7.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-BRsmYWSA.js → quadrantDiagram-AYHSOK5B-B1FZ09vH.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-ChNE_BOV.js → requirementDiagram-UZGBJVZJ-aEO78thZ.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-C8FtpwKc.js → sankeyDiagram-TZEHDZUN-6Ui--jp-.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-DmLCzNcc.js → sequenceDiagram-WL72ISMW-DF4Q1cAM.js} +1 -1
  49. package/dist-renderer/assets/splashScene-D0YB9uxm.js +17 -0
  50. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-WJBm4bhu.js → stateDiagram-FKZM4ZOC-BqA2BI8C.js} +1 -1
  51. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-Cs2ZtUD2.js +1 -0
  52. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-BXs_hOJs.js → timeline-definition-IT6M3QCI-DoOkw_A8.js} +1 -1
  53. package/dist-renderer/assets/{treemap-GDKQZRPO-o04MA0G9.js → treemap-GDKQZRPO-DUe26QdD.js} +1 -1
  54. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-Czj69XRd.js → xychartDiagram-PRI3JC2R-BKCnj5Xn.js} +1 -1
  55. package/dist-renderer/index.html +20 -53
  56. package/package.json +25 -18
  57. package/src/main/ipc/extensions.ts +2 -1
  58. package/src/main/server.ts +873 -221
  59. package/src/main/services/extensions/ExtensionFacadeService.ts +2 -5
  60. package/src/main/services/extensions/catalog/PluginCatalogService.ts +4 -2
  61. package/src/main/services/session-intelligence/ConversationTelemetryService.ts +1101 -0
  62. package/src/main/services/session-intelligence/LocalSessionScanner.ts +512 -0
  63. package/src/main/services/session-intelligence/SessionUsageParser.ts +4 -4
  64. package/src/main/services/system-manager/SystemManagerConfigService.ts +122 -0
  65. package/src/main/services/system-manager/SystemManagerPtyService.ts +233 -0
  66. package/src/main/services/system-manager/WorkflowPromptService.ts +75 -0
  67. package/src/main/services/teams-mvp/TaskDispatchService.ts +5 -6
  68. package/src/main/services/teams-mvp/TeamProvisioningService.ts +39 -2
  69. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +22 -4
  70. package/src/main/utils/teamProjectResolution.ts +15 -0
  71. package/src/renderer/App.tsx +8 -4
  72. package/src/renderer/api/httpClient.ts +68 -18
  73. package/src/renderer/api/providers.ts +23 -2
  74. package/src/renderer/assets/participant-avatars/01.svg +3 -0
  75. package/src/renderer/assets/participant-avatars/02.svg +3 -0
  76. package/src/renderer/assets/participant-avatars/03.svg +3 -0
  77. package/src/renderer/assets/participant-avatars/04.svg +3 -0
  78. package/src/renderer/assets/participant-avatars/05.svg +3 -0
  79. package/src/renderer/assets/participant-avatars/06.svg +3 -0
  80. package/src/renderer/assets/participant-avatars/07.svg +3 -0
  81. package/src/renderer/assets/participant-avatars/08.svg +3 -0
  82. package/src/renderer/assets/participant-avatars/09.svg +3 -0
  83. package/src/renderer/assets/participant-avatars/10.svg +3 -0
  84. package/src/renderer/assets/participant-avatars/11.svg +3 -0
  85. package/src/renderer/assets/participant-avatars/12.svg +3 -0
  86. package/src/renderer/assets/participant-avatars/13.svg +3 -0
  87. package/src/renderer/components/chat/ChatHistoryItem.tsx +1 -1
  88. package/src/renderer/components/chat/items/SubagentItem.tsx +2 -2
  89. package/src/renderer/components/chat/viewers/MermaidDiagram.tsx +2 -2
  90. package/src/renderer/components/common/ErrorBoundary.tsx +1 -1
  91. package/src/renderer/components/common/TerminalPane.tsx +213 -0
  92. package/src/renderer/components/dashboard/CliStatusBanner.tsx +7 -7
  93. package/src/renderer/components/dashboard/DashboardView.tsx +9 -36
  94. package/src/renderer/components/extensions/ExtensionStoreView.tsx +7 -126
  95. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  96. package/src/renderer/components/extensions/common/ExtensionToast.tsx +3 -3
  97. package/src/renderer/components/extensions/common/SourceBadge.tsx +1 -1
  98. package/src/renderer/components/extensions/mcp/McpLibraryEnableDialog.tsx +305 -0
  99. package/src/renderer/components/extensions/mcp/McpLibraryEntryDialog.tsx +418 -0
  100. package/src/renderer/components/extensions/mcp/McpLibraryPanel.tsx +404 -0
  101. package/src/renderer/components/extensions/plugins/CategoryChips.tsx +1 -1
  102. package/src/renderer/components/extensions/plugins/PluginCard.tsx +6 -6
  103. package/src/renderer/components/extensions/plugins/PluginDetailDialog.tsx +2 -2
  104. package/src/renderer/components/extensions/plugins/PluginsPanel.tsx +34 -21
  105. package/src/renderer/components/extensions/skills/SkillEditorDialog.tsx +1 -1
  106. package/src/renderer/components/extensions/skills/SkillsLibraryPanel.tsx +335 -0
  107. package/src/renderer/components/layout/PaneContent.tsx +8 -1
  108. package/src/renderer/components/layout/PaneResizeHandle.tsx +2 -2
  109. package/src/renderer/components/layout/Sidebar.tsx +13 -56
  110. package/src/renderer/components/layout/SortableTab.tsx +22 -33
  111. package/src/renderer/components/layout/TabBar.tsx +1 -1
  112. package/src/renderer/components/layout/TabContextMenu.tsx +1 -1
  113. package/src/renderer/components/report/sections/CostSection.tsx +2 -2
  114. package/src/renderer/components/report/sections/InsightsSection.tsx +1 -1
  115. package/src/renderer/components/runtime/ProviderRuntimeBackendSelector.tsx +2 -2
  116. package/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +768 -157
  117. package/src/renderer/components/schedules/SchedulesView.tsx +51 -462
  118. package/src/renderer/components/schedules/calendar/CalendarDayView.tsx +173 -0
  119. package/src/renderer/components/schedules/calendar/CalendarEventBlock.tsx +113 -0
  120. package/src/renderer/components/schedules/calendar/CalendarHeader.tsx +148 -0
  121. package/src/renderer/components/schedules/calendar/CalendarMonthView.tsx +142 -0
  122. package/src/renderer/components/schedules/calendar/CalendarWeekView.tsx +219 -0
  123. package/src/renderer/components/schedules/calendar/ScheduleCalendarBoard.tsx +41 -0
  124. package/src/renderer/components/schedules/calendar/TeamGanttView.tsx +405 -0
  125. package/src/renderer/components/schedules/calendar/computeOccurrences.ts +234 -0
  126. package/src/renderer/components/schedules/calendar/index.ts +2 -0
  127. package/src/renderer/components/schedules/calendar/types.ts +44 -0
  128. package/src/renderer/components/search/CommandPalette.tsx +4 -4
  129. package/src/renderer/components/settings/SettingsTabs.tsx +50 -55
  130. package/src/renderer/components/settings/SettingsView.tsx +30 -35
  131. package/src/renderer/components/settings/components/SettingsSectionHeader.tsx +5 -1
  132. package/src/renderer/components/settings/components/SettingsSelect.tsx +5 -3
  133. package/src/renderer/components/settings/components/SettingsToggle.tsx +2 -2
  134. package/src/renderer/components/settings/sections/AdvancedSection.tsx +11 -42
  135. package/src/renderer/components/settings/sections/CliStatusSection.tsx +72 -113
  136. package/src/renderer/components/settings/sections/ConfigEditorDialog.tsx +1 -1
  137. package/src/renderer/components/settings/sections/GeneralSection.tsx +11 -3
  138. package/src/renderer/components/settings/sections/HarnessSection.tsx +18 -14
  139. package/src/renderer/components/settings/sections/PlatformsSection.tsx +3 -3
  140. package/src/renderer/components/settings/sections/TaskBusSection.tsx +33 -40
  141. package/src/renderer/components/settings/sections/index.ts +0 -1
  142. package/src/renderer/components/sidebar/SessionFiltersPopover.tsx +1 -1
  143. package/src/renderer/components/sidebar/SessionItem.tsx +3 -3
  144. package/src/renderer/components/sidebar/SidebarSessions.tsx +184 -6
  145. package/src/renderer/components/sidebar/SidebarTaskItem.tsx +4 -4
  146. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +40 -5
  147. package/src/renderer/components/splash/splashScene.ts +121 -929
  148. package/src/renderer/components/system-manager/FolderBrowser.tsx +163 -0
  149. package/src/renderer/components/system-manager/SystemManagerView.tsx +351 -0
  150. package/src/renderer/components/tasks/TasksView.tsx +112 -134
  151. package/src/renderer/components/team/CcSessionsSection.tsx +431 -89
  152. package/src/renderer/components/team/ClaudeLogsFilterPopover.tsx +1 -1
  153. package/src/renderer/components/team/ClaudeLogsPanel.tsx +1 -1
  154. package/src/renderer/components/team/CollapsibleTeamSection.tsx +17 -32
  155. package/src/renderer/components/team/ProcessesSection.tsx +2 -2
  156. package/src/renderer/components/team/TaskTooltip.tsx +2 -2
  157. package/src/renderer/components/team/TeamDetailView.tsx +319 -123
  158. package/src/renderer/components/team/TeamListFilterPopover.tsx +1 -1
  159. package/src/renderer/components/team/TeamListView.tsx +109 -124
  160. package/src/renderer/components/team/TeamSessionsSection.tsx +6 -6
  161. package/src/renderer/components/team/UnreadCommentsBadge.tsx +1 -1
  162. package/src/renderer/components/team/activity/ActivityItem.tsx +9 -9
  163. package/src/renderer/components/team/activity/ActivityTimeline.tsx +5 -5
  164. package/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +3 -3
  165. package/src/renderer/components/team/activity/ReplyQuoteBlock.tsx +4 -4
  166. package/src/renderer/components/team/dialogs/CreateTaskDialog.tsx +4 -4
  167. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +84 -306
  168. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +259 -342
  169. package/src/renderer/components/team/dialogs/GlobalTaskDetailDialog.tsx +1 -1
  170. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +18 -16
  171. package/src/renderer/components/team/dialogs/PlatformBindingDialog.tsx +221 -0
  172. package/src/renderer/components/team/dialogs/PlatformManualForm.tsx +8 -1
  173. package/src/renderer/components/team/dialogs/PlatformSetupQR.tsx +5 -5
  174. package/src/renderer/components/team/dialogs/RuntimeConfigDialog.tsx +361 -0
  175. package/src/renderer/components/team/dialogs/SendMessageDialog.tsx +6 -6
  176. package/src/renderer/components/team/dialogs/SkipPermissionsCheckbox.tsx +6 -6
  177. package/src/renderer/components/team/dialogs/StatusHistoryTimeline.tsx +1 -1
  178. package/src/renderer/components/team/dialogs/TaskAttachments.tsx +1 -1
  179. package/src/renderer/components/team/dialogs/TaskCommentInput.tsx +6 -6
  180. package/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +4 -4
  181. package/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +3 -3
  182. package/src/renderer/components/team/dialogs/platformMeta.ts +122 -11
  183. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +17 -5
  184. package/src/renderer/components/team/editor/EditorFileTree.tsx +4 -4
  185. package/src/renderer/components/team/editor/EditorSearchPanel.tsx +1 -1
  186. package/src/renderer/components/team/editor/MarkdownSplitView.tsx +1 -1
  187. package/src/renderer/components/team/editor/NewFileDialog.tsx +1 -1
  188. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +1 -1
  189. package/src/renderer/components/team/editor/SearchInFilesPanel.tsx +1 -1
  190. package/src/renderer/components/team/kanban/KanbanBoard.tsx +9 -9
  191. package/src/renderer/components/team/kanban/KanbanFilterPopover.tsx +4 -4
  192. package/src/renderer/components/team/kanban/KanbanSearchInput.tsx +1 -1
  193. package/src/renderer/components/team/kanban/KanbanSortPopover.tsx +5 -5
  194. package/src/renderer/components/team/kanban/KanbanTaskCard.tsx +4 -4
  195. package/src/renderer/components/team/members/MemberCard.tsx +14 -47
  196. package/src/renderer/components/team/members/MemberDetailDialog.tsx +3 -95
  197. package/src/renderer/components/team/members/MemberDetailStats.tsx +50 -65
  198. package/src/renderer/components/team/members/MemberDraftRow.tsx +1 -1
  199. package/src/renderer/components/team/members/MemberStatsTab.tsx +2 -2
  200. package/src/renderer/components/team/members/MemberWorkspaceTab.tsx +1 -1
  201. package/src/renderer/components/team/messages/MessageComposer.tsx +10 -112
  202. package/src/renderer/components/team/messages/MessagesFilterPopover.tsx +1 -1
  203. package/src/renderer/components/team/messages/MessagesPanel.tsx +136 -119
  204. package/src/renderer/components/team/review/ChangeReviewDialog.tsx +1 -1
  205. package/src/renderer/components/team/schedule/ScheduleStatusBadge.tsx +3 -3
  206. package/src/renderer/components/team/sidebar/TeamSidebarRail.tsx +4 -4
  207. package/src/renderer/components/team/tasks/TaskRow.tsx +1 -1
  208. package/src/renderer/components/team/tools/AddMcpInline.tsx +27 -17
  209. package/src/renderer/components/team/tools/McpChip.tsx +6 -3
  210. package/src/renderer/components/team/tools/SkillChip.tsx +3 -3
  211. package/src/renderer/components/team/tools/ToolsSection.tsx +418 -70
  212. package/src/renderer/components/ui/MemberSelect.tsx +2 -2
  213. package/src/renderer/components/ui/MentionSuggestionList.tsx +2 -2
  214. package/src/renderer/components/ui/MentionableTextarea.tsx +3 -3
  215. package/src/renderer/hooks/useExtensionsTabState.ts +3 -114
  216. package/src/renderer/index.css +56 -39
  217. package/src/renderer/index.html +17 -50
  218. package/src/renderer/store/index.ts +2 -1
  219. package/src/renderer/store/slices/scheduleSlice.ts +1 -1
  220. package/src/renderer/store/slices/teamSlice.ts +45 -168
  221. package/src/renderer/utils/claudeCodeOnlyProviders.ts +3 -10
  222. package/src/renderer/utils/memberHelpers.ts +5 -17
  223. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +4 -2
  224. package/src/renderer/utils/providerSlashCommands.ts +0 -5
  225. package/src/renderer/utils/scheduleFormatters.ts +3 -1
  226. package/src/renderer/utils/teamMessageFiltering.ts +14 -1
  227. package/src/renderer/utils/teamModelAvailability.ts +18 -2
  228. package/src/shared/types/api.ts +121 -2
  229. package/src/shared/types/ccConnect.ts +2 -0
  230. package/src/shared/types/index.ts +3 -0
  231. package/src/shared/types/systemManager.ts +49 -0
  232. package/src/shared/types/team.ts +29 -0
  233. package/src/shared/types/terminal.ts +4 -2
  234. package/src/shared/utils/extensionNormalizers.ts +15 -8
  235. package/src/shared/utils/providerExtensionCapabilities.ts +2 -2
  236. package/dist-renderer/assets/ProjectEditorOverlay-lJZi-9Hp.js +0 -52
  237. package/dist-renderer/assets/channel-yIlSKy0e.js +0 -1
  238. package/dist-renderer/assets/classDiagram-2ON5EDUG-24fHez0s.js +0 -1
  239. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-24fHez0s.js +0 -1
  240. package/dist-renderer/assets/clone-BTNuUva-.js +0 -1
  241. package/dist-renderer/assets/index-Bi6nrZ4z.css +0 -1
  242. package/dist-renderer/assets/splashScene-C8lWNnm4.js +0 -1
  243. package/dist-renderer/assets/stateDiagram-v2-4FDKWEC3-_m6iPPUR.js +0 -1
@@ -0,0 +1,512 @@
1
+ /**
2
+ * LocalSessionScanner — scans local JSONL session files for a team's workDir
3
+ * and returns lightweight summaries + on-demand message detail with pagination.
4
+ *
5
+ * Replaces the cc-connect dependency for team session listing/detail.
6
+ * Reuses patterns from SessionUsageParser (walkJsonl, streaming parse, stat caching).
7
+ */
8
+
9
+ import { createReadStream } from 'node:fs';
10
+ import { readdir, stat } from 'node:fs/promises';
11
+ import { createInterface } from 'node:readline';
12
+ import * as path from 'node:path';
13
+
14
+ import { getProjectsBasePath, encodePath } from '@main/utils/pathDecoder';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export interface LocalSessionSummary {
21
+ id: string;
22
+ title: string;
23
+ projectId: string;
24
+ messageCount: number;
25
+ userMessageCount: number;
26
+ assistantMessageCount: number;
27
+ inputTokens: number;
28
+ outputTokens: number;
29
+ cacheReadTokens: number;
30
+ cacheCreationTokens: number;
31
+ model: string;
32
+ active: boolean;
33
+ live: boolean;
34
+ startTime: string | null;
35
+ endTime: string | null;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }
39
+
40
+ export interface LocalSessionMessage {
41
+ role: 'user' | 'assistant';
42
+ content: string;
43
+ timestamp: string;
44
+ }
45
+
46
+ export interface LocalSessionDetail {
47
+ id: string;
48
+ name: string;
49
+ sessionKey: string;
50
+ agentType: string;
51
+ active: boolean;
52
+ live: boolean;
53
+ historyCount: number;
54
+ createdAt: string;
55
+ updatedAt: string;
56
+ platform: string;
57
+ history: LocalSessionMessage[];
58
+ model: string;
59
+ inputTokens: number;
60
+ outputTokens: number;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Internal types
65
+ // ---------------------------------------------------------------------------
66
+
67
+ interface SummaryCacheEntry {
68
+ size: number;
69
+ mtimeMs: number;
70
+ summary: LocalSessionSummary;
71
+ }
72
+
73
+ interface PartialSummary {
74
+ title: string;
75
+ model: string;
76
+ messageCount: number;
77
+ userMessageCount: number;
78
+ assistantMessageCount: number;
79
+ inputTokens: number;
80
+ outputTokens: number;
81
+ cacheReadTokens: number;
82
+ cacheCreationTokens: number;
83
+ startTime: string | null;
84
+ endTime: string | null;
85
+ lastRole: string;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Constants
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const ACTIVE_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutes
93
+ const SUMMARY_SCAN_LINES = 200; // Read first N lines for lightweight summary
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Helpers (adapted from SessionUsageParser & ConversationTelemetryService)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Extract readable text from any content block type.
101
+ * Handles: text, thinking, tool_use, tool_result, image.
102
+ * Mirrors ConversationTelemetryService.extractText().
103
+ */
104
+ function extractText(content: unknown): string {
105
+ if (typeof content === 'string') return content;
106
+ if (!Array.isArray(content)) return '';
107
+
108
+ const parts: string[] = [];
109
+ for (const block of content) {
110
+ if (!block || typeof block !== 'object') continue;
111
+ const b = block as Record<string, unknown>;
112
+ if (b.type === 'text' && typeof b.text === 'string') {
113
+ parts.push(b.text);
114
+ } else if (b.type === 'thinking' && typeof b.thinking === 'string') {
115
+ parts.push('[Thinking omitted]');
116
+ } else if (b.type === 'tool_use' && typeof b.name === 'string') {
117
+ parts.push(`[Tool: ${b.name}${toolInputSummary(b.name, b.input)}]`);
118
+ } else if (b.type === 'tool_result') {
119
+ const nested = b.content;
120
+ if (typeof nested === 'string') parts.push(nested);
121
+ else if (Array.isArray(nested)) parts.push(extractText(nested));
122
+ } else if (b.type === 'image') {
123
+ parts.push('[Image]');
124
+ }
125
+ }
126
+ return parts.filter(Boolean).join('\n');
127
+ }
128
+
129
+ /**
130
+ * Produce a short human-readable summary of a tool_use input.
131
+ * e.g. "Read /src/main/server.ts", "Edit /src/main/server.ts", "Bash pnpm test"
132
+ */
133
+ function toolInputSummary(toolName: string, input: unknown): string {
134
+ if (!input || typeof input !== 'object') return '';
135
+ const inp = input as Record<string, unknown>;
136
+
137
+ // Common patterns by tool name
138
+ const fileKey = inp.file_path ?? inp.filePath ?? inp.path;
139
+ const cmdKey = inp.command ?? inp.description;
140
+
141
+ if (typeof fileKey === 'string') {
142
+ // Show only the last 2 path segments to keep it short
143
+ const short = fileKey.split('/').slice(-2).join('/');
144
+ return ` ${short}`;
145
+ }
146
+ if (typeof cmdKey === 'string') {
147
+ return ` ${cmdKey.slice(0, 80)}`;
148
+ }
149
+
150
+ // Generic: show first string-valued field
151
+ for (const value of Object.values(inp)) {
152
+ if (typeof value === 'string' && value.length > 0) {
153
+ return ` ${value.slice(0, 80)}`;
154
+ }
155
+ }
156
+ return '';
157
+ }
158
+
159
+ function extractFirstUserText(content: unknown): string {
160
+ const text = extractText(content);
161
+ return text.slice(0, 200).trim();
162
+ }
163
+
164
+ function smartTitle(text: string, maxLen = 90): string {
165
+ if (!text) return '';
166
+ let t = text;
167
+ if (t.startsWith('@') && t.includes(' ')) {
168
+ t = t.slice(t.indexOf(' ') + 1).trim();
169
+ }
170
+ let cut = t.length;
171
+ for (const sep of ['\n', '。', '?', '!', ';']) {
172
+ const i = t.indexOf(sep);
173
+ if (5 < i && i < maxLen && i < cut) cut = i;
174
+ }
175
+ return t.slice(0, cut < t.length ? cut : maxLen).trim();
176
+ }
177
+
178
+ async function* walkJsonl(dir: string): AsyncGenerator<string> {
179
+ let entries;
180
+ try {
181
+ entries = await readdir(dir, { withFileTypes: true });
182
+ } catch {
183
+ return;
184
+ }
185
+ for (const entry of entries) {
186
+ const full = path.join(dir, entry.name);
187
+ if (entry.isDirectory()) {
188
+ yield* walkJsonl(full);
189
+ } else if (entry.isFile() && entry.name.endsWith('.jsonl') && !entry.name.startsWith('agent_')) {
190
+ yield full;
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Quickly scan the first N lines of a JSONL file to extract a lightweight summary.
197
+ * This avoids parsing the entire file for the session list.
198
+ */
199
+ async function scanSummaryLines(
200
+ filePath: string,
201
+ sessionId: string,
202
+ projectId: string
203
+ ): Promise<PartialSummary | null> {
204
+ const result: PartialSummary = {
205
+ title: '',
206
+ model: '',
207
+ messageCount: 0,
208
+ userMessageCount: 0,
209
+ assistantMessageCount: 0,
210
+ inputTokens: 0,
211
+ outputTokens: 0,
212
+ cacheReadTokens: 0,
213
+ cacheCreationTokens: 0,
214
+ startTime: null,
215
+ endTime: null,
216
+ lastRole: '',
217
+ };
218
+
219
+ let lineCount = 0;
220
+
221
+ const rl = createInterface({
222
+ input: createReadStream(filePath, 'utf-8'),
223
+ crlfDelay: Infinity,
224
+ });
225
+
226
+ for await (const rawLine of rl) {
227
+ lineCount++;
228
+ if (lineCount > SUMMARY_SCAN_LINES) break;
229
+
230
+ const line = rawLine.trim();
231
+ if (!line) continue;
232
+
233
+ let obj: Record<string, unknown>;
234
+ try {
235
+ obj = JSON.parse(line) as Record<string, unknown>;
236
+ } catch {
237
+ continue;
238
+ }
239
+
240
+ const msg = obj.message as Record<string, unknown> | undefined;
241
+ let role: string | undefined;
242
+ let content: unknown;
243
+ let usage: Record<string, unknown> | undefined;
244
+ let ts: string | undefined;
245
+
246
+ if (msg && typeof msg === 'object') {
247
+ role = msg.role as string | undefined;
248
+ content = msg.content;
249
+ usage = msg.usage as Record<string, unknown> | undefined;
250
+ ts = (obj.timestamp ?? msg.timestamp) as string | undefined;
251
+ } else if (obj.type === 'user' || obj.type === 'assistant') {
252
+ role = obj.type as string;
253
+ content = obj.content;
254
+ ts = obj.timestamp as string | undefined;
255
+ }
256
+
257
+ if (!role || !ts) continue;
258
+
259
+ result.messageCount++;
260
+ if (!result.startTime) result.startTime = ts;
261
+ result.endTime = ts;
262
+ result.lastRole = role;
263
+
264
+ if (role === 'user') {
265
+ result.userMessageCount++;
266
+ if (!result.title && content) {
267
+ result.title = smartTitle(extractFirstUserText(content));
268
+ }
269
+ } else if (role === 'assistant') {
270
+ result.assistantMessageCount++;
271
+
272
+ // Extract model
273
+ if (!result.model) {
274
+ const model = msg?.model ?? obj.model;
275
+ if (typeof model === 'string' && model) {
276
+ result.model = model;
277
+ }
278
+ }
279
+
280
+ // Accumulate token usage
281
+ if (usage && typeof usage === 'object') {
282
+ result.inputTokens += Number(usage.input_tokens ?? 0) || 0;
283
+ result.outputTokens += Number(usage.output_tokens ?? 0) || 0;
284
+ result.cacheReadTokens += Number(usage.cache_read_input_tokens ?? 0) || 0;
285
+ result.cacheCreationTokens += Number(usage.cache_creation_input_tokens ?? 0) || 0;
286
+ }
287
+ }
288
+ }
289
+
290
+ if (result.messageCount === 0) return null;
291
+ return result;
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // LocalSessionScanner class
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export class LocalSessionScanner {
299
+ private summaryCache = new Map<string, SummaryCacheEntry>();
300
+
301
+ /**
302
+ * Resolve the JSONL directory for a given workDir.
303
+ * workDir is the absolute filesystem path (e.g., "/Users/name/project").
304
+ * The JSONL files live at ~/.claude/projects/{encoded-workDir}/
305
+ */
306
+ private resolveJsonlDir(workDir: string): string {
307
+ const projectsBase = getProjectsBasePath();
308
+ const encoded = encodePath(workDir);
309
+ return path.join(projectsBase, encoded);
310
+ }
311
+
312
+ /**
313
+ * Scan all JSONL session files for a team's workDir and return lightweight summaries.
314
+ * Uses file stat caching to skip unchanged files on subsequent calls.
315
+ */
316
+ async scanSummaries(workDir: string, projectId: string): Promise<LocalSessionSummary[]> {
317
+ const jsonlDir = this.resolveJsonlDir(workDir);
318
+ const summaries: LocalSessionSummary[] = [];
319
+ const now = Date.now();
320
+
321
+ for await (const filePath of walkJsonl(jsonlDir)) {
322
+ let fileStat;
323
+ try {
324
+ fileStat = await stat(filePath);
325
+ } catch {
326
+ continue;
327
+ }
328
+
329
+ // Check cache
330
+ const cached = this.summaryCache.get(filePath);
331
+ if (cached && cached.size === fileStat.size && cached.mtimeMs === fileStat.mtimeMs) {
332
+ summaries.push(cached.summary);
333
+ continue;
334
+ }
335
+
336
+ const sessionId = path.basename(filePath, '.jsonl');
337
+ const partial = await scanSummaryLines(filePath, sessionId, projectId);
338
+ if (!partial) continue;
339
+
340
+ const mtimeMs = fileStat.mtimeMs;
341
+ const active = now - mtimeMs < ACTIVE_THRESHOLD_MS;
342
+ const live = active && partial.lastRole === 'assistant';
343
+
344
+ // For a more accurate messageCount and token totals, we need the full file.
345
+ // But the first SUMMARY_SCAN_LINES is a good approximation for the list view.
346
+ // We'll mark the count as approximate if we stopped early.
347
+ const summary: LocalSessionSummary = {
348
+ id: sessionId,
349
+ title: partial.title || sessionId,
350
+ projectId,
351
+ messageCount: partial.messageCount,
352
+ userMessageCount: partial.userMessageCount,
353
+ assistantMessageCount: partial.assistantMessageCount,
354
+ inputTokens: partial.inputTokens,
355
+ outputTokens: partial.outputTokens,
356
+ cacheReadTokens: partial.cacheReadTokens,
357
+ cacheCreationTokens: partial.cacheCreationTokens,
358
+ model: partial.model,
359
+ active,
360
+ live,
361
+ startTime: partial.startTime,
362
+ endTime: partial.endTime,
363
+ createdAt: fileStat.birthtime?.toISOString() ?? new Date(mtimeMs).toISOString(),
364
+ updatedAt: new Date(mtimeMs).toISOString(),
365
+ };
366
+
367
+ this.summaryCache.set(filePath, {
368
+ size: fileStat.size,
369
+ mtimeMs,
370
+ summary,
371
+ });
372
+
373
+ summaries.push(summary);
374
+ }
375
+
376
+ // Sort by endTime descending (most recent first)
377
+ summaries.sort((a, b) => {
378
+ const ta = a.endTime ? Date.parse(a.endTime) : 0;
379
+ const tb = b.endTime ? Date.parse(b.endTime) : 0;
380
+ return tb - ta;
381
+ });
382
+
383
+ return summaries;
384
+ }
385
+
386
+ /**
387
+ * Read a single session's detail with paginated message history.
388
+ * Messages are returned in chronological order.
389
+ * offset=0, limit=200 returns the first 200 messages.
390
+ * offset=200, limit=200 returns messages 201-400.
391
+ */
392
+ async readSessionDetail(
393
+ workDir: string,
394
+ sessionId: string,
395
+ options?: { offset?: number; limit?: number }
396
+ ): Promise<LocalSessionDetail | null> {
397
+ const jsonlDir = this.resolveJsonlDir(workDir);
398
+ const filePath = path.join(jsonlDir, `${sessionId}.jsonl`);
399
+
400
+ // Check file exists
401
+ let fileStat;
402
+ try {
403
+ fileStat = await stat(filePath);
404
+ } catch {
405
+ return null;
406
+ }
407
+
408
+ const offset = options?.offset ?? 0;
409
+ const limit = options?.limit ?? 200;
410
+
411
+ const messages: LocalSessionMessage[] = [];
412
+ let totalMessages = 0;
413
+ let model = '';
414
+ let inputTokens = 0;
415
+ let outputTokens = 0;
416
+ let firstTs: string | null = null;
417
+ let lastTs: string | null = null;
418
+
419
+ const rl = createInterface({
420
+ input: createReadStream(filePath, 'utf-8'),
421
+ crlfDelay: Infinity,
422
+ });
423
+
424
+ for await (const rawLine of rl) {
425
+ const line = rawLine.trim();
426
+ if (!line) continue;
427
+
428
+ let obj: Record<string, unknown>;
429
+ try {
430
+ obj = JSON.parse(line) as Record<string, unknown>;
431
+ } catch {
432
+ continue;
433
+ }
434
+
435
+ const msg = obj.message as Record<string, unknown> | undefined;
436
+ let role: string | undefined;
437
+ let content: unknown;
438
+ let usage: Record<string, unknown> | undefined;
439
+ let ts: string | undefined;
440
+
441
+ if (msg && typeof msg === 'object') {
442
+ role = msg.role as string | undefined;
443
+ content = msg.content;
444
+ usage = msg.usage as Record<string, unknown> | undefined;
445
+ ts = (obj.timestamp ?? msg.timestamp) as string | undefined;
446
+ } else if (obj.type === 'user' || obj.type === 'assistant') {
447
+ role = obj.type as string;
448
+ content = obj.content;
449
+ ts = obj.timestamp as string | undefined;
450
+ }
451
+
452
+ if (!role || !ts) continue;
453
+ if (role !== 'user' && role !== 'assistant') continue;
454
+
455
+ totalMessages++;
456
+
457
+ if (!firstTs) firstTs = ts;
458
+ lastTs = ts;
459
+
460
+ // Extract model from first assistant message
461
+ if (role === 'assistant' && !model) {
462
+ const m = msg?.model ?? obj.model;
463
+ if (typeof m === 'string' && m) model = m;
464
+ }
465
+
466
+ // Accumulate tokens
467
+ if (role === 'assistant' && usage && typeof usage === 'object') {
468
+ inputTokens += Number(usage.input_tokens ?? 0) || 0;
469
+ outputTokens += Number(usage.output_tokens ?? 0) || 0;
470
+ }
471
+
472
+ // Collect messages within the page range
473
+ if (totalMessages > offset && messages.length < limit) {
474
+ messages.push({
475
+ role: role as 'user' | 'assistant',
476
+ content: extractText(content),
477
+ timestamp: ts,
478
+ });
479
+ }
480
+ }
481
+
482
+ if (totalMessages === 0) return null;
483
+
484
+ const now = Date.now();
485
+ const mtimeMs = fileStat.mtimeMs;
486
+ const active = now - mtimeMs < ACTIVE_THRESHOLD_MS;
487
+
488
+ return {
489
+ id: sessionId,
490
+ name: '',
491
+ sessionKey: sessionId,
492
+ agentType: '',
493
+ active,
494
+ live: active,
495
+ historyCount: totalMessages,
496
+ createdAt: fileStat.birthtime?.toISOString() ?? new Date(mtimeMs).toISOString(),
497
+ updatedAt: new Date(mtimeMs).toISOString(),
498
+ platform: 'local',
499
+ history: messages,
500
+ model,
501
+ inputTokens,
502
+ outputTokens,
503
+ };
504
+ }
505
+
506
+ /**
507
+ * Clear the summary cache. Useful for testing or forced refresh.
508
+ */
509
+ clearCache(): void {
510
+ this.summaryCache.clear();
511
+ }
512
+ }
@@ -10,7 +10,7 @@ import { createReadStream } from 'node:fs';
10
10
  import { readdir, stat } from 'node:fs/promises';
11
11
  import { createInterface } from 'node:readline';
12
12
  import * as path from 'node:path';
13
- import * as os from 'node:os';
13
+ import { getProjectsBasePath } from '@main/utils/pathDecoder';
14
14
 
15
15
  export interface SessionEntry {
16
16
  relPath: string;
@@ -79,7 +79,6 @@ export interface ParseResult {
79
79
  aggregate: UsageAggregate;
80
80
  }
81
81
 
82
- const PROJECTS_ROOT = path.join(os.homedir(), '.claude', 'projects');
83
82
  const SEG_GAP_MS = 10 * 60 * 1000; // 10 minutes gap threshold
84
83
  const RECENT_DAYS = 7;
85
84
 
@@ -349,8 +348,9 @@ export async function scanSessions(): Promise<ParseResult> {
349
348
  const activeDaySet = new Set<string>();
350
349
  const allEvents: EventEntry[] = [];
351
350
  const projectMap: Record<string, ProjectMetricsEntry> = {};
351
+ const projectsRoot = getProjectsBasePath();
352
352
 
353
- for await (const filePath of walkJsonl(PROJECTS_ROOT)) {
353
+ for await (const filePath of walkJsonl(projectsRoot)) {
354
354
  let fileStat;
355
355
  try {
356
356
  fileStat = await stat(filePath);
@@ -361,7 +361,7 @@ export async function scanSessions(): Promise<ParseResult> {
361
361
  const parsed = await parseJsonl(filePath);
362
362
  if (!parsed) continue;
363
363
 
364
- const relPath = path.relative(PROJECTS_ROOT, filePath);
364
+ const relPath = path.relative(projectsRoot, filePath);
365
365
  sessions.push({
366
366
  relPath,
367
367
  projectPath: parsed.projectPath,
@@ -0,0 +1,122 @@
1
+ import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import type {
6
+ SystemManagerConfig,
7
+ SystemManagerConfigPatch,
8
+ SystemManagerStatus,
9
+ } from '@shared/types/systemManager';
10
+
11
+ const CONFIG_FILE = 'system-manager.json';
12
+
13
+ function hermitHome(): string {
14
+ return process.env.HERMIT_HOME || path.join(os.homedir(), '.hermit');
15
+ }
16
+
17
+ function expandHome(input: string): string {
18
+ const normalized = input.trim().replace(/^~/, '~');
19
+ if (normalized === '~') return os.homedir();
20
+ if (normalized.startsWith('~/')) return path.join(os.homedir(), normalized.slice(2));
21
+ return normalized;
22
+ }
23
+
24
+ async function isDirectory(dirPath: string): Promise<boolean> {
25
+ try {
26
+ return (await stat(dirPath)).isDirectory();
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ async function commandExists(command: string): Promise<boolean> {
33
+ const paths = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
34
+ for (const dir of paths) {
35
+ try {
36
+ await access(path.join(dir, command));
37
+ return true;
38
+ } catch {
39
+ // keep looking
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+
45
+ export class SystemManagerConfigService {
46
+ private readonly configPath = path.join(hermitHome(), CONFIG_FILE);
47
+
48
+ constructor(private readonly defaultWorkDir: string) {}
49
+
50
+ async getConfig(): Promise<SystemManagerConfig> {
51
+ try {
52
+ const raw = await readFile(this.configPath, 'utf-8');
53
+ const parsed = JSON.parse(raw) as Partial<SystemManagerConfig>;
54
+ const selectedWorkDir = await this.normalizeDirectory(
55
+ parsed.selectedWorkDir || this.defaultWorkDir,
56
+ 'selectedWorkDir'
57
+ );
58
+ const workflowFolder = parsed.workflowFolder
59
+ ? await this.normalizeDirectory(parsed.workflowFolder, 'workflowFolder')
60
+ : undefined;
61
+ return {
62
+ schemaVersion: 1,
63
+ selectedWorkDir,
64
+ ...(workflowFolder ? { workflowFolder } : {}),
65
+ updatedAt:
66
+ typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
67
+ };
68
+ } catch {
69
+ return {
70
+ schemaVersion: 1,
71
+ selectedWorkDir: this.defaultWorkDir,
72
+ updatedAt: new Date().toISOString(),
73
+ };
74
+ }
75
+ }
76
+
77
+ async updateConfig(patch: SystemManagerConfigPatch): Promise<SystemManagerConfig> {
78
+ const current = await this.getConfig();
79
+ const next: SystemManagerConfig = {
80
+ ...current,
81
+ updatedAt: new Date().toISOString(),
82
+ };
83
+
84
+ if (typeof patch.selectedWorkDir === 'string') {
85
+ next.selectedWorkDir = await this.normalizeDirectory(
86
+ patch.selectedWorkDir,
87
+ 'selectedWorkDir'
88
+ );
89
+ }
90
+ if (patch.workflowFolder === null) {
91
+ delete next.workflowFolder;
92
+ } else if (typeof patch.workflowFolder === 'string') {
93
+ next.workflowFolder = await this.normalizeDirectory(patch.workflowFolder, 'workflowFolder');
94
+ }
95
+
96
+ await mkdir(path.dirname(this.configPath), { recursive: true });
97
+ await writeFile(this.configPath, JSON.stringify(next, null, 2), 'utf-8');
98
+ return next;
99
+ }
100
+
101
+ async getStatus(): Promise<SystemManagerStatus> {
102
+ const config = await this.getConfig();
103
+ const hasClaude = await commandExists('claude');
104
+ return {
105
+ displayName: '控制台',
106
+ defaultWorkDir: this.defaultWorkDir,
107
+ selectedWorkDir: config.selectedWorkDir,
108
+ ...(config.workflowFolder ? { workflowFolder: config.workflowFolder } : {}),
109
+ claudeCommand: 'claude',
110
+ localStatus: hasClaude ? 'ready' : 'missing-claude',
111
+ ...(hasClaude ? {} : { error: '未在 PATH 中找到 claude 命令' }),
112
+ };
113
+ }
114
+
115
+ private async normalizeDirectory(input: string, fieldName: string): Promise<string> {
116
+ const resolved = path.resolve(expandHome(input));
117
+ if (!(await isDirectory(resolved))) {
118
+ throw new Error(`${fieldName} 不是有效目录: ${resolved}`);
119
+ }
120
+ return resolved;
121
+ }
122
+ }