@yancyyu/openhermit 1.6.29 → 1.6.30

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 (152) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DsQt4FHy.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
  20. package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-BowUl0Jb.js → index-BlOrAXp3.js} +542 -532
  32. package/dist-renderer/assets/{index-6m1ZAymG.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-Dp3kJTEe.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-TOpt_T7A.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-qNBNjW4K.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-vAykq1H1.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-DNANbrI4.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-CGKpOUF2.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +4 -1
  56. package/src/main/ipc/extensions.ts +353 -0
  57. package/src/main/server.ts +209 -6
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
  59. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
  60. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
  61. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
  62. package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
  63. package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
  64. package/src/main/services/extensions/install/McpInstallService.ts +407 -0
  65. package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
  66. package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
  67. package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
  68. package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
  69. package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
  70. package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
  71. package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
  72. package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
  73. package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
  74. package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
  75. package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
  76. package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
  77. package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
  78. package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
  79. package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
  80. package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
  81. package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
  82. package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
  83. package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
  84. package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
  85. package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
  86. package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
  87. package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
  88. package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
  89. package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
  90. package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
  91. package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
  92. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  93. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  94. package/src/main/services/team/cliFlavor.ts +54 -0
  95. package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
  96. package/src/main/utils/atomicWrite.ts +72 -0
  97. package/src/main/utils/childProcess.ts +554 -0
  98. package/src/main/utils/cliEnv.ts +54 -0
  99. package/src/main/utils/cliPathMerge.ts +97 -0
  100. package/src/main/utils/pathDecoder.ts +664 -0
  101. package/src/main/utils/pathValidation.ts +432 -0
  102. package/src/main/utils/shellEnv.ts +331 -0
  103. package/src/renderer/api/httpClient.ts +61 -0
  104. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  105. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  106. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  107. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  108. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  109. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  110. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  111. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  112. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  113. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  114. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  115. package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
  116. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  117. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  118. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  119. package/src/renderer/components/team/TeamDetailView.tsx +35 -0
  120. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  121. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  122. package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
  123. package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
  124. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  125. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  126. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  127. package/src/renderer/store/slices/teamSlice.ts +8 -2
  128. package/src/shared/types/api.ts +29 -0
  129. package/src/shared/types/extensions/index.ts +1 -0
  130. package/src/shared/types/extensions/mcp.ts +2 -0
  131. package/src/shared/types/extensions/plugin.ts +2 -1
  132. package/src/shared/types/extensions/skill.ts +7 -0
  133. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  134. package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
  135. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
  136. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
  137. package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
  138. package/dist-renderer/assets/index-BhellmRb.css +0 -1
  139. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
  140. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  141. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  142. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  143. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  144. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  145. package/src/features/recent-projects/main/index.ts +0 -3
  146. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  147. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  148. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  149. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  150. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  151. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  152. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,469 @@
1
+ import { buildMergedCliPath } from '@main/utils/cliPathMerge';
2
+ import { getClaudeBasePath } from '@main/utils/pathDecoder';
3
+ import { getShellPreferredHome, resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+
7
+ import { getDoctorInvokedCandidates } from './ClaudeDoctorProbe';
8
+ import { getConfiguredCliFlavor } from './cliFlavor';
9
+
10
+ export interface ClaudeBinaryResolveProgress {
11
+ phase: string;
12
+ message: string;
13
+ }
14
+
15
+ export interface ClaudeBinaryResolveOptions {
16
+ onProgress?: (progress: ClaudeBinaryResolveProgress) => void;
17
+ }
18
+
19
+ function emitProgress(
20
+ options: ClaudeBinaryResolveOptions | undefined,
21
+ phase: string,
22
+ message: string
23
+ ): void {
24
+ options?.onProgress?.({ phase, message });
25
+ }
26
+
27
+ async function isExecutable(filePath: string): Promise<boolean> {
28
+ if (process.platform === 'win32') {
29
+ try {
30
+ const stat = await fs.promises.stat(filePath);
31
+ return stat.isFile();
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ try {
38
+ await fs.promises.access(filePath, fs.constants.X_OK);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function stripSurroundingQuotes(value: string): string {
46
+ const trimmed = value.trim();
47
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
48
+ return trimmed.slice(1, -1);
49
+ }
50
+ return trimmed;
51
+ }
52
+
53
+ function getWindowsExecutableExtensions(): string[] {
54
+ const raw = process.env.PATHEXT;
55
+ if (!raw) {
56
+ return ['.exe', '.cmd', '.bat', '.com'];
57
+ }
58
+
59
+ const exts = raw
60
+ .split(';')
61
+ .map((ext) => ext.trim())
62
+ .filter((ext) => ext.length > 0)
63
+ .map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
64
+ .map((ext) => ext.toLowerCase());
65
+
66
+ return Array.from(new Set(exts));
67
+ }
68
+
69
+ function expandWindowsBinaryNames(binaryName: string): string[] {
70
+ const trimmed = binaryName.trim();
71
+ if (!trimmed) {
72
+ return [];
73
+ }
74
+
75
+ const ext = path.extname(trimmed);
76
+ if (ext) {
77
+ return [trimmed];
78
+ }
79
+
80
+ const exts = getWindowsExecutableExtensions();
81
+ const withExt = exts.map((e) => `${trimmed}${e}`);
82
+ return [...withExt, trimmed];
83
+ }
84
+
85
+ async function collectNvmCandidates(): Promise<string[]> {
86
+ if (process.platform === 'win32') {
87
+ return collectNvmWindowsCandidates();
88
+ }
89
+
90
+ const nvmNodeRoot = path.join(getShellPreferredHome(), '.nvm', 'versions', 'node');
91
+ let versions: string[];
92
+ try {
93
+ versions = await fs.promises.readdir(nvmNodeRoot);
94
+ } catch {
95
+ return [];
96
+ }
97
+
98
+ return versions
99
+ .map((version) => path.join(nvmNodeRoot, version, 'bin', 'claude'))
100
+ .sort((a, b) => a.localeCompare(b))
101
+ .reverse();
102
+ }
103
+
104
+ /**
105
+ * Collect NVM for Windows (nvm-windows) candidates.
106
+ * nvm-windows stores Node versions under %APPDATA%\nvm\<version>\.
107
+ */
108
+ async function collectNvmWindowsCandidates(): Promise<string[]> {
109
+ const appdata = process.env.APPDATA;
110
+ if (!appdata) return [];
111
+
112
+ const nvmRoot = path.join(appdata, 'nvm');
113
+ let versions: string[];
114
+ try {
115
+ versions = await fs.promises.readdir(nvmRoot);
116
+ } catch {
117
+ return [];
118
+ }
119
+
120
+ const exts = getWindowsExecutableExtensions();
121
+ return versions
122
+ .toSorted((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }))
123
+ .flatMap((version) => exts.map((ext) => path.join(nvmRoot, version, `claude${ext}`)));
124
+ }
125
+
126
+ async function resolveFromPathEnv(binaryName: string, pathEnv?: string): Promise<string | null> {
127
+ // TODO: Consider migrating this PATH candidate collection to runtimePathBinaryResolver once
128
+ // Claude-specific executable checks, Windows PATHEXT handling, and parallel stat behavior
129
+ // can be preserved exactly.
130
+ const rawPath = pathEnv && pathEnv.length > 0 ? pathEnv : process.env.PATH;
131
+ if (!rawPath) {
132
+ return null;
133
+ }
134
+
135
+ const pathParts = rawPath.split(path.delimiter);
136
+ const binaryNames =
137
+ process.platform === 'win32' ? expandWindowsBinaryNames(binaryName) : [binaryName];
138
+
139
+ // Check all PATH directories in parallel. Each directory checks all extension
140
+ // variants concurrently. This turns N_dirs × N_exts sequential stat() calls
141
+ // into a single parallel batch, dramatically reducing startup time on Windows.
142
+ const dirResults = await Promise.all(
143
+ pathParts.map(async (part) => {
144
+ if (!part) return null;
145
+ const cleanedPart = stripSurroundingQuotes(part);
146
+ if (!cleanedPart) return null;
147
+
148
+ const candidates = binaryNames.map((name) => path.join(cleanedPart, name));
149
+ const results = await Promise.all(
150
+ candidates.map(async (candidate) => ({
151
+ path: candidate,
152
+ ok: await isExecutable(candidate),
153
+ }))
154
+ );
155
+ // Return the first matching extension variant within this directory
156
+ return results.find((r) => r.ok)?.path ?? null;
157
+ })
158
+ );
159
+
160
+ // Return first non-null result, preserving PATH priority order
161
+ return dirResults.find((r) => r !== null) ?? null;
162
+ }
163
+
164
+ async function resolveFromExplicitPath(inputPath: string): Promise<string | null> {
165
+ const trimmed = inputPath.trim();
166
+ if (!trimmed) {
167
+ return null;
168
+ }
169
+
170
+ if (process.platform === 'win32' && !path.extname(trimmed)) {
171
+ for (const ext of getWindowsExecutableExtensions()) {
172
+ const candidate = `${trimmed}${ext}`;
173
+ if (await isExecutable(candidate)) {
174
+ return candidate;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (await isExecutable(trimmed)) {
180
+ return trimmed;
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ async function resolveFromCandidateList(candidates: string[]): Promise<string | null> {
187
+ for (const candidate of candidates) {
188
+ if (await isExecutable(candidate)) {
189
+ return candidate;
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+
195
+ async function resolveFromDoctorFallback(commandName: string): Promise<string | null> {
196
+ const candidates = await getDoctorInvokedCandidates(commandName);
197
+ for (let index = candidates.length - 1; index >= 0; index -= 1) {
198
+ const candidate = candidates[index];
199
+ if (!candidate) {
200
+ continue;
201
+ }
202
+ const resolved = await resolveFromExplicitPath(candidate);
203
+ if (resolved) {
204
+ return resolved;
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+
210
+ async function resolveBundledOrchestratorBinary(): Promise<string | null> {
211
+ const resourcesPath = (process as unknown as { resourcesPath?: string }).resourcesPath?.trim();
212
+ if (!resourcesPath) {
213
+ return null;
214
+ }
215
+
216
+ const binaryName = process.platform === 'win32' ? 'claude-multimodel.exe' : 'claude-multimodel';
217
+ return resolveFromCandidateList([path.join(resourcesPath, 'runtime', binaryName)]);
218
+ }
219
+
220
+ function getConfiguredRuntimeOverrideRaw(flavor: 'claude' | 'agent_teams_orchestrator'): string {
221
+ return (
222
+ (flavor === 'agent_teams_orchestrator'
223
+ ? (process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() ??
224
+ process.env.CLAUDE_CLI_PATH?.trim())
225
+ : process.env.CLAUDE_CLI_PATH?.trim()) ?? ''
226
+ );
227
+ }
228
+
229
+ function looksLikeExplicitPath(value: string): boolean {
230
+ return path.isAbsolute(value) || value.includes('\\') || value.includes('/');
231
+ }
232
+
233
+ let cachedPath: string | null | undefined;
234
+
235
+ /** Timestamp of last successful cache verification (ms). */
236
+ let cacheVerifiedAt = 0;
237
+
238
+ /** Re-verify cached binary at most once per 30 seconds. */
239
+ const CACHE_VERIFY_TTL_MS = 30_000;
240
+
241
+ /** Coalesce concurrent first resolves so `cachedPath` is not torn by parallel scans. */
242
+ let resolveInFlight: Promise<string | null> | null = null;
243
+
244
+ export class ClaudeBinaryResolver {
245
+ /**
246
+ * Clear the cached binary path.
247
+ * Call after CLI install/update so the next resolve() picks up the new location.
248
+ */
249
+ static clearCache(): void {
250
+ cachedPath = undefined;
251
+ cacheVerifiedAt = 0;
252
+ }
253
+
254
+ static async resolve(options: ClaudeBinaryResolveOptions = {}): Promise<string | null> {
255
+ if (cachedPath !== undefined) {
256
+ const now = Date.now();
257
+ // Re-verify the cached binary still exists, but at most once per TTL
258
+ if (cachedPath !== null && now - cacheVerifiedAt > CACHE_VERIFY_TTL_MS) {
259
+ emitProgress(options, 'cache-verify', 'Verifying cached runtime...');
260
+ if (await isExecutable(cachedPath)) {
261
+ cacheVerifiedAt = now;
262
+ emitProgress(options, 'cache-hit', 'Using cached runtime...');
263
+ return cachedPath;
264
+ }
265
+ cachedPath = undefined;
266
+ cacheVerifiedAt = 0;
267
+ // Fall through to full resolution below
268
+ } else {
269
+ emitProgress(
270
+ options,
271
+ cachedPath ? 'cache-hit' : 'cache-miss',
272
+ 'Using cached runtime status...'
273
+ );
274
+ return cachedPath;
275
+ }
276
+ }
277
+ if (!resolveInFlight) {
278
+ resolveInFlight = ClaudeBinaryResolver.runResolve(options).finally(() => {
279
+ resolveInFlight = null;
280
+ });
281
+ } else {
282
+ emitProgress(options, 'in-flight', 'Waiting for runtime lookup...');
283
+ }
284
+ return resolveInFlight;
285
+ }
286
+
287
+ private static async runResolve(options: ClaudeBinaryResolveOptions): Promise<string | null> {
288
+ const flavor = getConfiguredCliFlavor();
289
+ emitProgress(options, 'flavor', `Using ${flavor} runtime mode...`);
290
+
291
+ const overrideRaw = getConfiguredRuntimeOverrideRaw(flavor);
292
+ const overrideIsExplicitPath = overrideRaw ? looksLikeExplicitPath(overrideRaw) : false;
293
+ if (overrideRaw && overrideIsExplicitPath) {
294
+ emitProgress(options, 'configured-path', 'Checking configured runtime path...');
295
+ const resolvedOverride = await resolveFromExplicitPath(overrideRaw);
296
+
297
+ if (resolvedOverride) {
298
+ cachedPath = resolvedOverride;
299
+ cacheVerifiedAt = Date.now();
300
+ emitProgress(options, 'configured-path-found', 'Using configured runtime path...');
301
+ return cachedPath;
302
+ }
303
+ }
304
+
305
+ const shouldTryBundledOrchestratorBeforeShell =
306
+ flavor === 'agent_teams_orchestrator' && (!overrideRaw || overrideIsExplicitPath);
307
+ if (shouldTryBundledOrchestratorBeforeShell) {
308
+ emitProgress(options, 'bundled-runtime', 'Checking bundled Agent Teams runtime...');
309
+ const bundledBinary = await resolveBundledOrchestratorBinary();
310
+ if (bundledBinary) {
311
+ cachedPath = bundledBinary;
312
+ cacheVerifiedAt = Date.now();
313
+ emitProgress(options, 'bundled-runtime-found', 'Using bundled Agent Teams runtime...');
314
+ return cachedPath;
315
+ }
316
+ }
317
+
318
+ await resolveInteractiveShellEnvBestEffort({
319
+ timeoutMs: 1_500,
320
+ fallbackEnv: process.env,
321
+ background: false,
322
+ onProgress: (progress) => emitProgress(options, progress.phase, progress.message),
323
+ });
324
+ const enrichedPath = buildMergedCliPath(null);
325
+
326
+ if (overrideRaw && !overrideIsExplicitPath) {
327
+ emitProgress(options, 'configured-path', 'Checking configured runtime path...');
328
+ const resolvedOverride = await resolveFromPathEnv(overrideRaw, enrichedPath);
329
+
330
+ if (resolvedOverride) {
331
+ cachedPath = resolvedOverride;
332
+ cacheVerifiedAt = Date.now();
333
+ emitProgress(options, 'configured-path-found', 'Using configured runtime path...');
334
+ return cachedPath;
335
+ }
336
+ }
337
+
338
+ if (flavor === 'agent_teams_orchestrator') {
339
+ if (!shouldTryBundledOrchestratorBeforeShell) {
340
+ emitProgress(options, 'bundled-runtime', 'Checking bundled Agent Teams runtime...');
341
+ const bundledBinary = await resolveBundledOrchestratorBinary();
342
+ if (bundledBinary) {
343
+ cachedPath = bundledBinary;
344
+ cacheVerifiedAt = Date.now();
345
+ emitProgress(options, 'bundled-runtime-found', 'Using bundled Agent Teams runtime...');
346
+ return cachedPath;
347
+ }
348
+ }
349
+
350
+ // Keep agent_teams_orchestrator resolution generic. Dev flows should
351
+ // inject an explicit CLI path, while non-dev setups can expose
352
+ // claude-multimodel on PATH without making this resolver guess a sibling
353
+ // repo name or folder.
354
+ const orchestratorBinaryName = 'claude-multimodel';
355
+ emitProgress(options, 'path-runtime', 'Searching PATH for Agent Teams runtime...');
356
+ const fromPath = await resolveFromPathEnv(orchestratorBinaryName, enrichedPath);
357
+ if (fromPath) {
358
+ cachedPath = fromPath;
359
+ cacheVerifiedAt = Date.now();
360
+ emitProgress(options, 'path-runtime-found', 'Using Agent Teams runtime from PATH...');
361
+ return cachedPath;
362
+ }
363
+
364
+ emitProgress(options, 'doctor-runtime', 'Checking runtime diagnostics fallback...');
365
+ const fromDoctor = await resolveFromDoctorFallback(orchestratorBinaryName);
366
+ if (fromDoctor) {
367
+ cachedPath = fromDoctor;
368
+ cacheVerifiedAt = Date.now();
369
+ emitProgress(options, 'doctor-runtime-found', 'Using runtime from diagnostics fallback...');
370
+ return cachedPath;
371
+ }
372
+
373
+ // agent_teams_orchestrator mode is explicit. If the configured local
374
+ // runtime is missing, fail closed instead of silently falling back to a
375
+ // different CLI.
376
+ return null;
377
+ }
378
+
379
+ const baseBinaryName = 'claude';
380
+ emitProgress(options, 'path-claude', 'Searching PATH for Claude CLI...');
381
+ const fromPath = await resolveFromPathEnv(baseBinaryName, enrichedPath);
382
+ if (fromPath) {
383
+ cachedPath = fromPath;
384
+ cacheVerifiedAt = Date.now();
385
+ emitProgress(options, 'path-claude-found', 'Using Claude CLI from PATH...');
386
+ return cachedPath;
387
+ }
388
+
389
+ const platformBinaryNames =
390
+ process.platform === 'win32' ? expandWindowsBinaryNames(baseBinaryName) : [baseBinaryName];
391
+
392
+ const home = getShellPreferredHome();
393
+ const vendorBinDir = path.join(getClaudeBasePath(), 'local', 'node_modules', '.bin');
394
+ const candidateDirs: string[] =
395
+ process.platform === 'win32'
396
+ ? [
397
+ // Windows: Claude npm-local vendor install
398
+ vendorBinDir,
399
+ // Windows: npm global install
400
+ path.join(home, 'AppData', 'Roaming', 'npm'),
401
+ // Windows: scoop, chocolatey, and other package managers
402
+ path.join(home, 'scoop', 'shims'),
403
+ // Windows: Local programs
404
+ ...(process.env.LOCALAPPDATA
405
+ ? [path.join(process.env.LOCALAPPDATA, 'Programs', 'claude')]
406
+ : []),
407
+ // Windows: Program Files
408
+ ...(process.env.ProgramFiles ? [path.join(process.env.ProgramFiles, 'claude')] : []),
409
+ ]
410
+ : [
411
+ // Unix: Claude npm-local vendor install
412
+ vendorBinDir,
413
+ // Unix: native binary installation path (claude install)
414
+ path.join(home, '.local', 'bin'),
415
+ path.join(home, '.npm-global', 'bin'),
416
+ path.join(home, '.npm', 'bin'),
417
+ '/usr/local/bin',
418
+ '/opt/homebrew/bin',
419
+ ];
420
+
421
+ const candidates = candidateDirs.flatMap((dir) =>
422
+ platformBinaryNames.map((name) => path.join(dir, name))
423
+ );
424
+
425
+ emitProgress(options, 'standard-locations', 'Checking standard Claude install locations...');
426
+ const nvmCandidates = await collectNvmCandidates();
427
+ if (nvmCandidates.length > 0) {
428
+ emitProgress(options, 'nvm-locations', 'Checking nvm-managed Claude installs...');
429
+ }
430
+ const allCandidates = [...candidates, ...nvmCandidates];
431
+
432
+ // Check all fallback candidates in parallel for speed
433
+ const results = await Promise.all(
434
+ allCandidates.map(async (candidate) => ({
435
+ path: candidate,
436
+ ok: await isExecutable(candidate),
437
+ }))
438
+ );
439
+ // Return first match, preserving candidate priority order
440
+ const found = results.find((r) => r.ok);
441
+ if (found) {
442
+ cachedPath = found.path;
443
+ cacheVerifiedAt = Date.now();
444
+ emitProgress(
445
+ options,
446
+ 'fallback-location-found',
447
+ 'Using Claude CLI from install locations...'
448
+ );
449
+ return cachedPath;
450
+ }
451
+
452
+ emitProgress(options, 'doctor-claude', 'Checking Claude diagnostics fallback...');
453
+ const fromDoctor = await resolveFromDoctorFallback(baseBinaryName);
454
+ if (fromDoctor) {
455
+ cachedPath = fromDoctor;
456
+ cacheVerifiedAt = Date.now();
457
+ emitProgress(options, 'doctor-claude-found', 'Using Claude CLI from diagnostics fallback...');
458
+ return cachedPath;
459
+ }
460
+
461
+ // Don't cache null — CLI may be installed later without app restart
462
+ emitProgress(
463
+ options,
464
+ 'not-found',
465
+ 'Runtime not found. Continuing with limited launch support...'
466
+ );
467
+ return null;
468
+ }
469
+ }
@@ -0,0 +1,54 @@
1
+ import type { CliFlavor, CliFlavorUiOptions } from '@shared/types';
2
+
3
+ export const DEFAULT_CLI_FLAVOR: CliFlavor = 'claude';
4
+
5
+ function parseFlavorOverride(raw: string | undefined): CliFlavor | null {
6
+ const trimmed = raw?.trim();
7
+ if (trimmed === 'claude' || trimmed === 'agent_teams_orchestrator') {
8
+ return trimmed;
9
+ }
10
+ return null;
11
+ }
12
+
13
+ export function getConfiguredCliFlavor(): CliFlavor {
14
+ const envOverride = parseFlavorOverride(process.env.CLAUDE_TEAM_CLI_FLAVOR);
15
+ if (envOverride) {
16
+ return envOverride;
17
+ }
18
+
19
+ return DEFAULT_CLI_FLAVOR;
20
+ }
21
+
22
+ export function getCliFlavorUiOptions(flavor: CliFlavor): CliFlavorUiOptions {
23
+ switch (flavor) {
24
+ case 'agent_teams_orchestrator':
25
+ return {
26
+ displayName: 'Multimodel runtime',
27
+ supportsSelfUpdate: false,
28
+ showVersionDetails: false,
29
+ showBinaryPath: false,
30
+ };
31
+ case 'claude':
32
+ default:
33
+ return {
34
+ displayName: 'Claude CLI',
35
+ supportsSelfUpdate: true,
36
+ showVersionDetails: true,
37
+ showBinaryPath: true,
38
+ };
39
+ }
40
+ }
41
+
42
+ export function getCliFlavorCommandLabel(flavor: CliFlavor): string {
43
+ switch (flavor) {
44
+ case 'agent_teams_orchestrator':
45
+ return 'orchestrator-cli';
46
+ case 'claude':
47
+ default:
48
+ return 'claude';
49
+ }
50
+ }
51
+
52
+ export function getConfiguredCliCommandLabel(): string {
53
+ return getCliFlavorCommandLabel(getConfiguredCliFlavor());
54
+ }
@@ -68,6 +68,9 @@ export class TaskDispatchService {
68
68
  this.config = config ?? null;
69
69
  if (config?.enabled && config.redis) {
70
70
  await this.connectRedis();
71
+ if (!this.redis) {
72
+ throw new Error('Redis connection failed: PING did not succeed');
73
+ }
71
74
  }
72
75
  }
73
76
 
@@ -0,0 +1,72 @@
1
+ import { randomUUID } from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ const RENAME_MAX_ATTEMPTS = 8;
6
+ const RENAME_RETRY_BASE_DELAY_MS = 40;
7
+ const RENAME_RETRY_MAX_DELAY_MS = 250;
8
+ const RENAME_RETRY_JITTER_MS = 25;
9
+ const RETRYABLE_RENAME_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
10
+
11
+ function sleep(ms: number): Promise<void> {
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+
15
+ function getRenameRetryDelayMs(attempt: number): number {
16
+ const backoff = Math.min(RENAME_RETRY_BASE_DELAY_MS * attempt, RENAME_RETRY_MAX_DELAY_MS);
17
+ return backoff + Math.floor(Math.random() * (RENAME_RETRY_JITTER_MS + 1));
18
+ }
19
+
20
+ async function renameWithRetry(src: string, dest: string): Promise<void> {
21
+ for (let attempt = 1; attempt <= RENAME_MAX_ATTEMPTS; attempt++) {
22
+ try {
23
+ await fs.promises.rename(src, dest);
24
+ return;
25
+ } catch (error) {
26
+ const code = (error as NodeJS.ErrnoException).code;
27
+ if (code === 'EXDEV') {
28
+ await fs.promises.copyFile(src, dest);
29
+ await fs.promises.unlink(src).catch(() => undefined);
30
+ return;
31
+ }
32
+ if (code && RETRYABLE_RENAME_CODES.has(code) && attempt < RENAME_MAX_ATTEMPTS) {
33
+ await sleep(getRenameRetryDelayMs(attempt));
34
+ continue;
35
+ }
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Async atomic write: write tmp file then rename over target.
43
+ * Uses best-effort fsync and bounded Windows transient rename retries for safety.
44
+ */
45
+ export async function atomicWriteAsync(targetPath: string, data: string | Buffer): Promise<void> {
46
+ const dir = path.dirname(targetPath);
47
+ const tmpPath = path.join(dir, `.tmp.${randomUUID()}`);
48
+
49
+ try {
50
+ await fs.promises.mkdir(dir, { recursive: true });
51
+ await fs.promises.writeFile(tmpPath, data, typeof data === 'string' ? 'utf8' : undefined);
52
+
53
+ let fd: fs.promises.FileHandle | null = null;
54
+ try {
55
+ fd = await fs.promises.open(tmpPath, 'r+');
56
+ await fd.sync();
57
+ } catch {
58
+ // fsync is best-effort.
59
+ } finally {
60
+ try {
61
+ await fd?.close();
62
+ } catch {
63
+ // close is best-effort after a best-effort fsync.
64
+ }
65
+ }
66
+
67
+ await renameWithRetry(tmpPath, targetPath);
68
+ } catch (error) {
69
+ await fs.promises.unlink(tmpPath).catch(() => undefined);
70
+ throw error;
71
+ }
72
+ }