@yancyyu/openhermit 1.6.28 → 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 (177) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-DsQt4FHy.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.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-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
  32. package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.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 +907 -184
  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/session-intelligence/UsageTelemetryService.ts +33 -18
  93. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  94. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  95. package/src/main/services/team/cliFlavor.ts +54 -0
  96. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  97. package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
  98. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  99. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  100. package/src/main/services/teams-mvp/index.ts +3 -0
  101. package/src/main/utils/atomicWrite.ts +72 -0
  102. package/src/main/utils/childProcess.ts +554 -0
  103. package/src/main/utils/cliEnv.ts +54 -0
  104. package/src/main/utils/cliPathMerge.ts +97 -0
  105. package/src/main/utils/pathDecoder.ts +664 -0
  106. package/src/main/utils/pathValidation.ts +432 -0
  107. package/src/main/utils/shellEnv.ts +331 -0
  108. package/src/renderer/App.tsx +5 -0
  109. package/src/renderer/api/httpClient.ts +128 -0
  110. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  111. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  112. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  113. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  114. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  115. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  116. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  117. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  118. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  119. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  120. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  121. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  122. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  123. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  124. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  125. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  126. package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
  127. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  128. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  129. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  130. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  131. package/src/renderer/components/team/TeamDetailView.tsx +55 -98
  132. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  133. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  134. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  135. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  136. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  137. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  138. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  139. package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
  140. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  141. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  142. package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
  143. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  144. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  145. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  146. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  147. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  148. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  149. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  150. package/src/renderer/store/slices/teamSlice.ts +67 -25
  151. package/src/renderer/types/tabs.ts +1 -0
  152. package/src/shared/types/api.ts +58 -0
  153. package/src/shared/types/extensions/index.ts +1 -0
  154. package/src/shared/types/extensions/mcp.ts +2 -0
  155. package/src/shared/types/extensions/plugin.ts +2 -1
  156. package/src/shared/types/extensions/skill.ts +7 -0
  157. package/src/shared/types/team.ts +104 -1
  158. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  159. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  160. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  161. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  162. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  163. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  164. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  165. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  166. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  167. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  168. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  169. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  170. package/src/features/recent-projects/main/index.ts +0 -3
  171. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  172. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  173. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  174. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  175. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  176. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  177. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,407 @@
1
+ /**
2
+ * McpInstallService — installs/uninstalls MCP servers via Claude CLI.
3
+ *
4
+ * Security model: renderer sends ONLY registryId + user inputs (env values,
5
+ * headers, server name). Main re-fetches server spec from registry via getById()
6
+ * and builds CLI args from the fresh registry data (never trusts install spec
7
+ * from renderer).
8
+ */
9
+
10
+ import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
11
+ import { execCli } from '@main/utils/childProcess';
12
+ import { CLI_NOT_FOUND_MESSAGE } from '@shared/constants/cli';
13
+ import { createLogger } from '@shared/utils/logger';
14
+ import { isProjectScopedMcpScope } from '@shared/utils/mcpScopes';
15
+ import path from 'path';
16
+
17
+ import { createExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
18
+
19
+ import type { McpCatalogAggregator } from '../catalog/McpCatalogAggregator';
20
+ import type { ExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
21
+ import type {
22
+ McpCustomInstallRequest,
23
+ McpInstallRequest,
24
+ OperationResult,
25
+ } from '@shared/types/extensions';
26
+
27
+ const logger = createLogger('Extensions:McpInstall');
28
+
29
+ /** Validate server name: alphanumeric, dashes, underscores, dots */
30
+ const SERVER_NAME_RE = /^[\w.-]{1,100}$/;
31
+
32
+ /** Allowed scope values (prevent command injection) */
33
+ const VALID_SCOPES = new Set(['local', 'user', 'project', 'global']);
34
+
35
+ /** Env var key must be safe shell identifier */
36
+ const ENV_KEY_RE = /^[A-Z_][A-Z0-9_]{0,100}$/i;
37
+
38
+ /** HTTP header key must be safe (RFC 7230 token) */
39
+ const HEADER_KEY_RE = /^[A-Za-z][\w-]{0,100}$/;
40
+
41
+ const TIMEOUT_MS = 30_000;
42
+
43
+ function scopeRequiresProjectPath(scope?: string): boolean {
44
+ return isProjectScopedMcpScope(scope);
45
+ }
46
+
47
+ export class McpInstallService {
48
+ constructor(
49
+ private readonly aggregator: McpCatalogAggregator,
50
+ private readonly runtimeAdapter: ExtensionsRuntimeAdapter = createExtensionsRuntimeAdapter()
51
+ ) {}
52
+
53
+ async install(request: McpInstallRequest): Promise<OperationResult> {
54
+ const { registryId, serverName, scope, projectPath, envValues, headers } = request;
55
+
56
+ // 1. Validate server name
57
+ if (!SERVER_NAME_RE.test(serverName)) {
58
+ return {
59
+ state: 'error',
60
+ error: `Invalid server name: "${serverName}". Use alphanumeric, dashes, underscores, dots.`,
61
+ };
62
+ }
63
+
64
+ // 2. Validate scope
65
+ if (scope && !VALID_SCOPES.has(scope)) {
66
+ return {
67
+ state: 'error',
68
+ error: `Invalid scope: "${scope}". Must be one of: local, user, project, global.`,
69
+ };
70
+ }
71
+
72
+ if (scopeRequiresProjectPath(scope) && !projectPath) {
73
+ return {
74
+ state: 'error',
75
+ error: `projectPath is required for ${scope} scope`,
76
+ };
77
+ }
78
+
79
+ // 3. Validate env var keys (prevent command injection)
80
+ for (const key of Object.keys(envValues)) {
81
+ if (!ENV_KEY_RE.test(key)) {
82
+ return {
83
+ state: 'error',
84
+ error: `Invalid environment variable name: "${key}". Use uppercase alphanumeric and underscores.`,
85
+ };
86
+ }
87
+ }
88
+
89
+ // 4. Validate header keys (prevent header injection)
90
+ for (const header of headers) {
91
+ if (header.key && !HEADER_KEY_RE.test(header.key)) {
92
+ return {
93
+ state: 'error',
94
+ error: `Invalid header name: "${header.key}". Use alphanumeric, dashes, underscores.`,
95
+ };
96
+ }
97
+ }
98
+
99
+ // 5. Validate projectPath (if provided, must be absolute)
100
+ if (projectPath && !path.isAbsolute(projectPath)) {
101
+ return {
102
+ state: 'error',
103
+ error: 'projectPath must be an absolute path',
104
+ };
105
+ }
106
+
107
+ // 6. Re-fetch from registry (don't trust renderer-provided install spec)
108
+ const server = await this.aggregator.getById(registryId);
109
+ if (!server) {
110
+ return {
111
+ state: 'error',
112
+ error: `MCP server "${registryId}" not found in registry`,
113
+ };
114
+ }
115
+
116
+ if (!server.installSpec) {
117
+ return {
118
+ state: 'error',
119
+ error: `MCP server "${server.name}" does not have an automatic install spec. Manual setup required.`,
120
+ };
121
+ }
122
+
123
+ // 7. Build CLI args based on install spec type
124
+ const args: string[] = ['mcp', 'add'];
125
+
126
+ // Scope flag (-s)
127
+ if (scope && scope !== 'local') {
128
+ args.push('-s', scope);
129
+ }
130
+
131
+ if (server.installSpec.type === 'stdio') {
132
+ // Stdio: claude mcp add [-s scope] [-e KEY=val...] <name> -- npx -y <package>[@version]
133
+ // Add env flags
134
+ for (const [key, value] of Object.entries(envValues)) {
135
+ if (key && value) {
136
+ args.push('-e', `${key}=${value}`);
137
+ }
138
+ }
139
+
140
+ args.push(serverName);
141
+ args.push('--');
142
+ args.push('npx', '-y');
143
+
144
+ const pkg = server.installSpec.npmVersion
145
+ ? `${server.installSpec.npmPackage}@${server.installSpec.npmVersion}`
146
+ : server.installSpec.npmPackage;
147
+ args.push(pkg);
148
+ } else if (server.installSpec.type === 'http') {
149
+ // HTTP/SSE: claude mcp add [-s scope] -t <transport> [-H "Key: val"...] <name> <url>
150
+ const transport = server.installSpec.transportType === 'sse' ? 'sse' : 'http';
151
+ args.push('-t', transport);
152
+
153
+ // Add header flags
154
+ for (const header of headers) {
155
+ if (header.key && header.value) {
156
+ args.push('-H', `${header.key}:${header.value}`);
157
+ }
158
+ }
159
+
160
+ // Add env flags (some HTTP servers also need env vars)
161
+ for (const [key, value] of Object.entries(envValues)) {
162
+ if (key && value) {
163
+ args.push('-e', `${key}=${value}`);
164
+ }
165
+ }
166
+
167
+ args.push(serverName);
168
+ args.push(server.installSpec.url);
169
+ } else {
170
+ return {
171
+ state: 'error',
172
+ error: `Unsupported install spec type: ${(server.installSpec as { type: string }).type}`,
173
+ };
174
+ }
175
+
176
+ logger.info(
177
+ `Installing MCP server: ${serverName} (type: ${server.installSpec.type}, scope: ${scope ?? 'local'})`
178
+ );
179
+ // Don't log env values or header values (may contain secrets)
180
+
181
+ try {
182
+ const claudeBinary = await ClaudeBinaryResolver.resolve();
183
+ if (!claudeBinary) {
184
+ return {
185
+ state: 'error',
186
+ error: CLI_NOT_FOUND_MESSAGE,
187
+ };
188
+ }
189
+ const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
190
+
191
+ const { stderr } = await execCli(claudeBinary, args, {
192
+ timeout: TIMEOUT_MS,
193
+ cwd: projectPath,
194
+ env,
195
+ });
196
+
197
+ if (stderr) {
198
+ logger.warn(`MCP install stderr: ${stderr.slice(0, 200)}`);
199
+ }
200
+
201
+ return { state: 'success' };
202
+ } catch (err) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ // Mask potential secrets in error output
205
+ const safeMessage = maskSecrets(
206
+ message,
207
+ envValues,
208
+ headers.map((h) => h.value)
209
+ );
210
+ logger.error(`MCP install failed: ${safeMessage}`);
211
+ return { state: 'error', error: safeMessage };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Install a custom MCP server — user provides installSpec directly (bypasses registry).
217
+ */
218
+ async installCustom(request: McpCustomInstallRequest): Promise<OperationResult> {
219
+ const { serverName, scope, projectPath, installSpec, envValues, headers } = request;
220
+
221
+ // Validate inputs (same rules as registry install)
222
+ if (!SERVER_NAME_RE.test(serverName)) {
223
+ return {
224
+ state: 'error',
225
+ error: `Invalid server name: "${serverName}". Use alphanumeric, dashes, underscores, dots.`,
226
+ };
227
+ }
228
+
229
+ if (scope && !VALID_SCOPES.has(scope)) {
230
+ return { state: 'error', error: `Invalid scope: "${scope}".` };
231
+ }
232
+
233
+ if (scopeRequiresProjectPath(scope) && !projectPath) {
234
+ return { state: 'error', error: `projectPath is required for ${scope} scope` };
235
+ }
236
+
237
+ for (const key of Object.keys(envValues)) {
238
+ if (!ENV_KEY_RE.test(key)) {
239
+ return { state: 'error', error: `Invalid env var name: "${key}".` };
240
+ }
241
+ }
242
+
243
+ for (const header of headers) {
244
+ if (header.key && !HEADER_KEY_RE.test(header.key)) {
245
+ return { state: 'error', error: `Invalid header name: "${header.key}".` };
246
+ }
247
+ }
248
+
249
+ if (projectPath && !path.isAbsolute(projectPath)) {
250
+ return { state: 'error', error: 'projectPath must be an absolute path' };
251
+ }
252
+
253
+ // Build CLI args from provided installSpec
254
+ const args: string[] = ['mcp', 'add'];
255
+
256
+ if (scope && scope !== 'local') {
257
+ args.push('-s', scope);
258
+ }
259
+
260
+ if (installSpec.type === 'stdio') {
261
+ for (const [key, value] of Object.entries(envValues)) {
262
+ if (key && value) args.push('-e', `${key}=${value}`);
263
+ }
264
+
265
+ args.push(serverName);
266
+ args.push('--');
267
+ args.push('npx', '-y');
268
+
269
+ const pkg = installSpec.npmVersion
270
+ ? `${installSpec.npmPackage}@${installSpec.npmVersion}`
271
+ : installSpec.npmPackage;
272
+ args.push(pkg);
273
+ } else if (installSpec.type === 'http') {
274
+ const transport = installSpec.transportType === 'sse' ? 'sse' : 'http';
275
+ args.push('-t', transport);
276
+
277
+ // Positional args must come before variadic flags (-H, -e)
278
+ args.push(serverName);
279
+ args.push(installSpec.url);
280
+
281
+ for (const header of headers) {
282
+ if (header.key && header.value) {
283
+ args.push('-H', `${header.key}:${header.value}`);
284
+ }
285
+ }
286
+
287
+ for (const [key, value] of Object.entries(envValues)) {
288
+ if (key && value) args.push('-e', `${key}=${value}`);
289
+ }
290
+ } else {
291
+ return { state: 'error', error: 'Unsupported install spec type' };
292
+ }
293
+
294
+ logger.info(
295
+ `Installing custom MCP server: ${serverName} (type: ${installSpec.type}, scope: ${scope ?? 'local'})`
296
+ );
297
+
298
+ try {
299
+ const claudeBinary = await ClaudeBinaryResolver.resolve();
300
+ if (!claudeBinary) {
301
+ return {
302
+ state: 'error',
303
+ error: CLI_NOT_FOUND_MESSAGE,
304
+ };
305
+ }
306
+ const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
307
+
308
+ const { stderr } = await execCli(claudeBinary, args, {
309
+ timeout: TIMEOUT_MS,
310
+ cwd: projectPath,
311
+ env,
312
+ });
313
+
314
+ if (stderr) {
315
+ logger.warn(`Custom MCP install stderr: ${stderr.slice(0, 200)}`);
316
+ }
317
+
318
+ return { state: 'success' };
319
+ } catch (err) {
320
+ const message = err instanceof Error ? err.message : String(err);
321
+ const safeMessage = maskSecrets(
322
+ message,
323
+ envValues,
324
+ headers.map((h) => h.value)
325
+ );
326
+ logger.error(`Custom MCP install failed: ${safeMessage}`);
327
+ return { state: 'error', error: safeMessage };
328
+ }
329
+ }
330
+
331
+ async uninstall(name: string, scope?: string, projectPath?: string): Promise<OperationResult> {
332
+ if (!SERVER_NAME_RE.test(name)) {
333
+ return {
334
+ state: 'error',
335
+ error: `Invalid server name: "${name}"`,
336
+ };
337
+ }
338
+
339
+ if (scope && !VALID_SCOPES.has(scope)) {
340
+ return {
341
+ state: 'error',
342
+ error: `Invalid scope: "${scope}". Must be one of: local, user, project, global.`,
343
+ };
344
+ }
345
+
346
+ if (scopeRequiresProjectPath(scope) && !projectPath) {
347
+ return {
348
+ state: 'error',
349
+ error: `projectPath is required for ${scope} scope`,
350
+ };
351
+ }
352
+
353
+ if (projectPath && !path.isAbsolute(projectPath)) {
354
+ return {
355
+ state: 'error',
356
+ error: 'projectPath must be an absolute path',
357
+ };
358
+ }
359
+
360
+ const args = ['mcp', 'remove'];
361
+ if (scope && scope !== 'local') {
362
+ args.push('-s', scope);
363
+ }
364
+ args.push(name);
365
+
366
+ logger.info(`Removing MCP server: ${name} (scope: ${scope ?? 'local'})`);
367
+
368
+ try {
369
+ const claudeBinary = await ClaudeBinaryResolver.resolve();
370
+ if (!claudeBinary) {
371
+ return {
372
+ state: 'error',
373
+ error: CLI_NOT_FOUND_MESSAGE,
374
+ };
375
+ }
376
+ const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
377
+
378
+ await execCli(claudeBinary, args, {
379
+ timeout: TIMEOUT_MS,
380
+ cwd: projectPath,
381
+ env,
382
+ });
383
+ return { state: 'success' };
384
+ } catch (err) {
385
+ const message = err instanceof Error ? err.message : String(err);
386
+ logger.error(`MCP uninstall failed: ${message}`);
387
+ return { state: 'error', error: message };
388
+ }
389
+ }
390
+ }
391
+
392
+ /** Replace secret values in error messages with [REDACTED] */
393
+ function maskSecrets(
394
+ message: string,
395
+ envValues: Record<string, string>,
396
+ headerValues: string[]
397
+ ): string {
398
+ let result = message;
399
+ const secrets = [
400
+ ...Object.values(envValues).filter((v) => v.length > 3),
401
+ ...headerValues.filter((v) => v.length > 3),
402
+ ];
403
+ for (const secret of secrets) {
404
+ result = result.replaceAll(secret, '[REDACTED]');
405
+ }
406
+ return result;
407
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * PluginInstallService — installs/uninstalls plugins via Claude CLI.
3
+ *
4
+ * Security model: renderer sends ONLY pluginId, main resolves qualifiedName
5
+ * from the current catalog snapshot (never trusts renderer-provided paths).
6
+ */
7
+
8
+ import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver';
9
+ import { execCli } from '@main/utils/childProcess';
10
+ import { CLI_NOT_FOUND_MESSAGE } from '@shared/constants/cli';
11
+ import { createLogger } from '@shared/utils/logger';
12
+ import path from 'path';
13
+
14
+ import { createExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
15
+
16
+ import type { PluginCatalogService } from '../catalog/PluginCatalogService';
17
+ import type { ExtensionsRuntimeAdapter } from '../runtime/ExtensionsRuntimeAdapter';
18
+ import type { OperationResult, PluginInstallRequest } from '@shared/types/extensions';
19
+
20
+ const logger = createLogger('Extensions:PluginInstall');
21
+
22
+ /** Validate qualifiedName: must be <name>@<marketplace> with safe characters */
23
+ const QUALIFIED_NAME_RE = /^[\w.-]+@[\w.-]+$/;
24
+
25
+ /** Allowed scope values (prevent command injection) */
26
+ const VALID_SCOPES = new Set(['local', 'user', 'project']);
27
+
28
+ const INSTALL_TIMEOUT_MS = 120_000; // plugins may clone repos
29
+ const UNINSTALL_TIMEOUT_MS = 30_000;
30
+
31
+ function scopeRequiresProjectPath(scope?: string): boolean {
32
+ return scope === 'project' || scope === 'local';
33
+ }
34
+
35
+ export class PluginInstallService {
36
+ constructor(
37
+ private readonly catalogService: PluginCatalogService,
38
+ private readonly runtimeAdapter: ExtensionsRuntimeAdapter = createExtensionsRuntimeAdapter()
39
+ ) {}
40
+
41
+ async install(request: PluginInstallRequest): Promise<OperationResult> {
42
+ const { pluginId, scope, projectPath } = request;
43
+
44
+ // 1. Validate scope
45
+ if (scope && !VALID_SCOPES.has(scope)) {
46
+ return {
47
+ state: 'error',
48
+ error: `Invalid scope: "${scope}". Must be one of: local, user, project.`,
49
+ };
50
+ }
51
+
52
+ // 2. Validate projectPath
53
+ if (projectPath && !path.isAbsolute(projectPath)) {
54
+ return {
55
+ state: 'error',
56
+ error: 'projectPath must be an absolute path',
57
+ };
58
+ }
59
+
60
+ if (scopeRequiresProjectPath(scope) && !projectPath) {
61
+ return {
62
+ state: 'error',
63
+ error: `projectPath is required for ${scope}-scoped plugin installs`,
64
+ };
65
+ }
66
+
67
+ // 3. Resolve qualifiedName from catalog (NOT from renderer)
68
+ const resolved = await this.catalogService.resolvePlugin(pluginId);
69
+ if (!resolved) {
70
+ return {
71
+ state: 'error',
72
+ error: `Plugin "${pluginId}" not found in catalog`,
73
+ };
74
+ }
75
+
76
+ const { qualifiedName } = resolved;
77
+
78
+ // 2. Validate qualifiedName format (prevent injection)
79
+ if (!QUALIFIED_NAME_RE.test(qualifiedName)) {
80
+ return {
81
+ state: 'error',
82
+ error: `Invalid plugin identifier: ${qualifiedName}`,
83
+ };
84
+ }
85
+
86
+ // 5. Build CLI args: claude plugin install [-s scope] <qualifiedName>
87
+ const args = ['plugin', 'install'];
88
+ if (scope && scope !== 'user') {
89
+ args.push('-s', scope);
90
+ }
91
+ args.push(qualifiedName);
92
+
93
+ logger.info(`Installing plugin: ${qualifiedName} (scope: ${scope ?? 'user'})`);
94
+
95
+ try {
96
+ const claudeBinary = await ClaudeBinaryResolver.resolve();
97
+ if (!claudeBinary) {
98
+ return {
99
+ state: 'error',
100
+ error: CLI_NOT_FOUND_MESSAGE,
101
+ };
102
+ }
103
+ const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
104
+
105
+ const { stdout, stderr } = await execCli(claudeBinary, args, {
106
+ timeout: INSTALL_TIMEOUT_MS,
107
+ cwd: projectPath,
108
+ env,
109
+ });
110
+
111
+ if (stderr && !stdout) {
112
+ logger.warn(`Plugin install stderr: ${stderr}`);
113
+ }
114
+
115
+ return { state: 'success' };
116
+ } catch (err) {
117
+ const message = err instanceof Error ? err.message : String(err);
118
+ logger.error(`Plugin install failed: ${message}`);
119
+ return { state: 'error', error: message };
120
+ }
121
+ }
122
+
123
+ async uninstall(
124
+ pluginId: string,
125
+ scope?: string,
126
+ projectPath?: string
127
+ ): Promise<OperationResult> {
128
+ // Validate scope
129
+ if (scope && !VALID_SCOPES.has(scope)) {
130
+ return {
131
+ state: 'error',
132
+ error: `Invalid scope: "${scope}". Must be one of: local, user, project.`,
133
+ };
134
+ }
135
+
136
+ if (projectPath && !path.isAbsolute(projectPath)) {
137
+ return {
138
+ state: 'error',
139
+ error: 'projectPath must be an absolute path',
140
+ };
141
+ }
142
+
143
+ if (scopeRequiresProjectPath(scope) && !projectPath) {
144
+ return {
145
+ state: 'error',
146
+ error: `projectPath is required for ${scope}-scoped plugin uninstalls`,
147
+ };
148
+ }
149
+
150
+ // Resolve qualifiedName from catalog
151
+ const resolved = await this.catalogService.resolvePlugin(pluginId);
152
+ if (!resolved) {
153
+ return {
154
+ state: 'error',
155
+ error: `Plugin "${pluginId}" not found in catalog`,
156
+ };
157
+ }
158
+
159
+ const { qualifiedName } = resolved;
160
+
161
+ if (!QUALIFIED_NAME_RE.test(qualifiedName)) {
162
+ return {
163
+ state: 'error',
164
+ error: `Invalid plugin identifier: ${qualifiedName}`,
165
+ };
166
+ }
167
+
168
+ const args = ['plugin', 'uninstall'];
169
+ if (scope && scope !== 'user') {
170
+ args.push('-s', scope);
171
+ }
172
+ args.push(qualifiedName);
173
+
174
+ logger.info(`Uninstalling plugin: ${qualifiedName} (scope: ${scope ?? 'user'})`);
175
+
176
+ try {
177
+ const claudeBinary = await ClaudeBinaryResolver.resolve();
178
+ if (!claudeBinary) {
179
+ return {
180
+ state: 'error',
181
+ error: CLI_NOT_FOUND_MESSAGE,
182
+ };
183
+ }
184
+ const env = await this.runtimeAdapter.buildManagementCliEnv(claudeBinary);
185
+
186
+ await execCli(claudeBinary, args, {
187
+ timeout: UNINSTALL_TIMEOUT_MS,
188
+ cwd: projectPath,
189
+ env,
190
+ });
191
+ return { state: 'success' };
192
+ } catch (err) {
193
+ const message = err instanceof Error ? err.message : String(err);
194
+ logger.error(`Plugin uninstall failed: ${message}`);
195
+ return { state: 'error', error: message };
196
+ }
197
+ }
198
+ }