@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,214 @@
1
+ import { isInstalledMcpScope } from '@shared/utils/mcpScopes';
2
+
3
+ import type { McpServerDiagnostic, McpServerHealthStatus } from '@shared/types/extensions';
4
+
5
+ interface McpDiagnoseJsonEntry {
6
+ name?: string;
7
+ target?: string;
8
+ scope?: 'local' | 'user' | 'project' | 'global' | 'dynamic' | 'managed';
9
+ transport?: string;
10
+ status?: 'connected' | 'needs-authentication' | 'failed' | 'timeout';
11
+ statusLabel?: string;
12
+ }
13
+
14
+ interface McpDiagnoseJsonPayload {
15
+ checkedAt?: string;
16
+ diagnostics?: McpDiagnoseJsonEntry[];
17
+ }
18
+
19
+ const EMBEDDED_HTTP_URL_PATTERN = /https?:\/\/[^\s"'`]+/gi;
20
+ const SENSITIVE_FLAG_VALUE_PATTERN = /(--[a-z0-9_-]+)(?:=([^\s]+)|\s+([^\s]+))/gi;
21
+ const URL_PASSWORD_KEY = `pass${'word'}` as keyof URL;
22
+ const SENSITIVE_FLAG_NAMES = new Set([
23
+ 'apikey',
24
+ 'accesstoken',
25
+ 'authtoken',
26
+ 'token',
27
+ 'secret',
28
+ 'password',
29
+ 'clientsecret',
30
+ ]);
31
+
32
+ function isPluginInjectedDiagnosticName(name: string): boolean {
33
+ return name.startsWith('plugin:');
34
+ }
35
+
36
+ function isExtensionsManagedDiagnosticEntry(entry: {
37
+ name: string;
38
+ scope?: 'local' | 'user' | 'project' | 'global' | 'dynamic' | 'managed';
39
+ }): boolean {
40
+ if (isPluginInjectedDiagnosticName(entry.name)) {
41
+ return false;
42
+ }
43
+
44
+ return entry.scope === undefined || isInstalledMcpScope(entry.scope);
45
+ }
46
+
47
+ function isSensitiveCliFlag(flag: string): boolean {
48
+ const normalizedFlag = flag.toLowerCase().replace(/^--/, '').replace(/[-_]/g, '');
49
+ return SENSITIVE_FLAG_NAMES.has(normalizedFlag);
50
+ }
51
+ function extractJsonObject<T>(raw: string): T {
52
+ const trimmed = raw.trim();
53
+ try {
54
+ return JSON.parse(trimmed) as T;
55
+ } catch {
56
+ const start = trimmed.indexOf('{');
57
+ const end = trimmed.lastIndexOf('}');
58
+ if (start >= 0 && end > start) {
59
+ return JSON.parse(trimmed.slice(start, end + 1)) as T;
60
+ }
61
+ throw new Error('No JSON object found in CLI output');
62
+ }
63
+ }
64
+
65
+ function parseStatusChunk(statusChunk: string): {
66
+ status: McpServerHealthStatus;
67
+ statusLabel: string;
68
+ } {
69
+ const symbol = statusChunk[0];
70
+ const label = statusChunk.slice(1).trim() || 'Unknown';
71
+
72
+ switch (symbol) {
73
+ case '✓':
74
+ return { status: 'connected', statusLabel: label };
75
+ case '!':
76
+ return { status: 'needs-authentication', statusLabel: label };
77
+ case '✗':
78
+ return { status: 'failed', statusLabel: label };
79
+ default:
80
+ return { status: 'unknown', statusLabel: statusChunk };
81
+ }
82
+ }
83
+
84
+ function redactHttpUrl(urlString: string): string {
85
+ try {
86
+ const parsed = new URL(urlString);
87
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
88
+ return urlString;
89
+ }
90
+
91
+ const passwordField = parsed[URL_PASSWORD_KEY];
92
+ const hasUsername = parsed.username.length > 0;
93
+ const hasPassword = Boolean(passwordField);
94
+
95
+ if (!hasUsername && !hasPassword && !parsed.search && !parsed.hash) {
96
+ return urlString;
97
+ }
98
+
99
+ const redactedSearchParams = new URLSearchParams(parsed.search);
100
+ for (const key of new Set(redactedSearchParams.keys())) {
101
+ redactedSearchParams.set(key, 'REDACTED');
102
+ }
103
+
104
+ const authPrefix =
105
+ hasUsername || hasPassword
106
+ ? `${hasUsername ? '***' : ''}${hasPassword ? `${hasUsername ? ':' : ''}***` : ''}@`
107
+ : '';
108
+ const searchSuffix = redactedSearchParams.size > 0 ? `?${redactedSearchParams.toString()}` : '';
109
+ const hashSuffix = parsed.hash ? '#REDACTED' : '';
110
+
111
+ return `${parsed.protocol}//${authPrefix}${parsed.host}${parsed.pathname}${searchSuffix}${hashSuffix}`;
112
+ } catch {
113
+ return urlString;
114
+ }
115
+ }
116
+
117
+ function redactDiagnosticTarget(target: string): string {
118
+ return target
119
+ .replace(EMBEDDED_HTTP_URL_PATTERN, (match) => redactHttpUrl(match))
120
+ .replace(
121
+ SENSITIVE_FLAG_VALUE_PATTERN,
122
+ (match, flag: string, inlineValue?: string, separatedValue?: string) => {
123
+ if (!isSensitiveCliFlag(flag)) {
124
+ return match;
125
+ }
126
+
127
+ return inlineValue || separatedValue ? `${flag}=REDACTED` : `${flag} REDACTED`;
128
+ }
129
+ );
130
+ }
131
+
132
+ function parseDiagnosticLine(line: string, checkedAt: number): McpServerDiagnostic | null {
133
+ const statusSeparatorIdx = line.lastIndexOf(' - ');
134
+ if (statusSeparatorIdx === -1) {
135
+ return null;
136
+ }
137
+
138
+ const descriptor = line.slice(0, statusSeparatorIdx).trim();
139
+ const statusChunk = line.slice(statusSeparatorIdx + 3).trim();
140
+
141
+ const nameSeparatorIdx = descriptor.indexOf(': ');
142
+ if (nameSeparatorIdx === -1) {
143
+ return null;
144
+ }
145
+
146
+ const name = descriptor.slice(0, nameSeparatorIdx).trim();
147
+ const target = redactDiagnosticTarget(descriptor.slice(nameSeparatorIdx + 2).trim());
148
+ if (!name || !target) {
149
+ return null;
150
+ }
151
+
152
+ const { status, statusLabel } = parseStatusChunk(statusChunk);
153
+
154
+ return {
155
+ name,
156
+ target,
157
+ status,
158
+ statusLabel,
159
+ rawLine: line,
160
+ checkedAt,
161
+ };
162
+ }
163
+
164
+ export function parseMcpDiagnosticsOutput(output: string): McpServerDiagnostic[] {
165
+ const checkedAt = Date.now();
166
+
167
+ return output
168
+ .split(/\r?\n/)
169
+ .map((line) => line.trim())
170
+ .filter((line) => line.length > 0 && !line.startsWith('Checking MCP server health'))
171
+ .map((line) => parseDiagnosticLine(line, checkedAt))
172
+ .filter((entry): entry is McpServerDiagnostic => entry !== null)
173
+ .filter((entry) => isExtensionsManagedDiagnosticEntry(entry));
174
+ }
175
+
176
+ export function parseMcpDiagnosticsJsonOutput(output: string): McpServerDiagnostic[] {
177
+ const parsed = extractJsonObject<McpDiagnoseJsonPayload>(output);
178
+ const checkedAtValue = parsed.checkedAt ? Date.parse(parsed.checkedAt) : Number.NaN;
179
+ const checkedAt = Number.isFinite(checkedAtValue) ? checkedAtValue : Date.now();
180
+
181
+ return (parsed.diagnostics ?? []).flatMap<McpServerDiagnostic>((entry) => {
182
+ if (
183
+ typeof entry.name !== 'string' ||
184
+ typeof entry.target !== 'string' ||
185
+ typeof entry.statusLabel !== 'string'
186
+ ) {
187
+ return [];
188
+ }
189
+
190
+ const redactedTarget = redactDiagnosticTarget(entry.target);
191
+ const normalizedStatus: McpServerHealthStatus =
192
+ entry.status === 'connected'
193
+ ? 'connected'
194
+ : entry.status === 'needs-authentication'
195
+ ? 'needs-authentication'
196
+ : entry.status === 'failed' || entry.status === 'timeout'
197
+ ? 'failed'
198
+ : 'unknown';
199
+
200
+ const rawLine = `${entry.name}: ${redactedTarget} - ${entry.statusLabel}`;
201
+ const diagnostic = {
202
+ name: entry.name,
203
+ target: redactedTarget,
204
+ scope: entry.scope,
205
+ transport: entry.transport,
206
+ status: normalizedStatus,
207
+ statusLabel: entry.statusLabel,
208
+ rawLine,
209
+ checkedAt,
210
+ } satisfies McpServerDiagnostic;
211
+
212
+ return isExtensionsManagedDiagnosticEntry(diagnostic) ? [diagnostic] : [];
213
+ });
214
+ }
@@ -0,0 +1,45 @@
1
+ import { isInstalledMcpScope } from '@shared/utils/mcpScopes';
2
+
3
+ import type { InstalledMcpEntry } from '@shared/types/extensions';
4
+
5
+ interface McpListJsonServer {
6
+ name?: string;
7
+ scope?: string;
8
+ transport?: string;
9
+ }
10
+
11
+ interface McpListJsonPayload {
12
+ servers?: McpListJsonServer[];
13
+ }
14
+
15
+ function extractJsonObject<T>(raw: string): T {
16
+ const trimmed = raw.trim();
17
+ try {
18
+ return JSON.parse(trimmed) as T;
19
+ } catch {
20
+ const start = trimmed.indexOf('{');
21
+ const end = trimmed.lastIndexOf('}');
22
+ if (start >= 0 && end > start) {
23
+ return JSON.parse(trimmed.slice(start, end + 1)) as T;
24
+ }
25
+ throw new Error('No JSON object found in CLI output');
26
+ }
27
+ }
28
+
29
+ export function parseInstalledMcpJsonOutput(output: string): InstalledMcpEntry[] {
30
+ const parsed = extractJsonObject<McpListJsonPayload>(output);
31
+
32
+ return (parsed.servers ?? []).flatMap<InstalledMcpEntry>((entry) => {
33
+ if (typeof entry.name !== 'string' || !isInstalledMcpScope(entry.scope)) {
34
+ return [];
35
+ }
36
+
37
+ return [
38
+ {
39
+ name: entry.name,
40
+ scope: entry.scope,
41
+ transport: typeof entry.transport === 'string' ? entry.transport : undefined,
42
+ },
43
+ ];
44
+ });
45
+ }
@@ -0,0 +1,155 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ import { validateOpenPathUserSelected } from '@main/utils/pathValidation';
5
+ import { isBinaryFile } from 'isbinaryfile';
6
+
7
+ import { SkillScanner } from './SkillScanner';
8
+
9
+ export interface ImportedSkillSourceFile {
10
+ relativePath: string;
11
+ absolutePath: string;
12
+ content: string | null;
13
+ isBinary: boolean;
14
+ }
15
+
16
+ export interface SkillImportInspection {
17
+ files: ImportedSkillSourceFile[];
18
+ warnings: string[];
19
+ hiddenEntriesSkipped: number;
20
+ }
21
+
22
+ const MAX_IMPORT_FILE_COUNT = 200;
23
+ const MAX_IMPORT_TOTAL_BYTES = 10 * 1024 * 1024;
24
+
25
+ export class SkillImportService {
26
+ constructor(private readonly scanner = new SkillScanner()) {}
27
+
28
+ async validateSourceDir(sourceDir: string): Promise<string> {
29
+ const validatedSource = validateOpenPathUserSelected(sourceDir);
30
+ if (!validatedSource.valid || !validatedSource.normalizedPath) {
31
+ throw new Error(validatedSource.error ?? 'Invalid import source');
32
+ }
33
+
34
+ const normalizedSourceDir = validatedSource.normalizedPath;
35
+ const sourceStat = await fs.stat(normalizedSourceDir);
36
+ if (!sourceStat.isDirectory()) {
37
+ throw new Error('Import source must be a directory');
38
+ }
39
+
40
+ const detectedSkillFile = await this.scanner.detectSkillFile(normalizedSourceDir);
41
+ if (!detectedSkillFile) {
42
+ throw new Error('Import source does not contain a valid skill file');
43
+ }
44
+
45
+ return normalizedSourceDir;
46
+ }
47
+
48
+ async inspectSourceDir(sourceDir: string): Promise<SkillImportInspection> {
49
+ const normalizedSourceDir = await this.validateSourceDir(sourceDir);
50
+ const walked = await this.walkDirectory(normalizedSourceDir);
51
+ const files = await Promise.all(
52
+ walked.files.map(async ({ absolutePath, relativePath }) => {
53
+ const binary = await isBinaryFile(absolutePath);
54
+ return {
55
+ relativePath,
56
+ absolutePath,
57
+ content: binary ? null : await fs.readFile(absolutePath, 'utf8'),
58
+ isBinary: binary,
59
+ };
60
+ })
61
+ );
62
+
63
+ const warnings: string[] = [];
64
+ if (walked.hiddenEntriesSkipped > 0) {
65
+ warnings.push('Hidden files and folders were skipped during import.');
66
+ }
67
+ if (files.some((file) => file.isBinary)) {
68
+ warnings.push('This import includes binary files. Binary files will be copied as-is.');
69
+ }
70
+ if (
71
+ files.some(
72
+ (file) => file.relativePath === 'scripts' || file.relativePath.startsWith('scripts/')
73
+ )
74
+ ) {
75
+ warnings.push('This import includes scripts. Review them carefully before importing.');
76
+ }
77
+
78
+ return {
79
+ files,
80
+ warnings,
81
+ hiddenEntriesSkipped: walked.hiddenEntriesSkipped,
82
+ };
83
+ }
84
+
85
+ async readSourceFiles(sourceDir: string): Promise<ImportedSkillSourceFile[]> {
86
+ return (await this.inspectSourceDir(sourceDir)).files;
87
+ }
88
+
89
+ async writeImportedFiles(
90
+ targetSkillDir: string,
91
+ files: ImportedSkillSourceFile[]
92
+ ): Promise<void> {
93
+ for (const file of files) {
94
+ const destPath = path.join(targetSkillDir, file.relativePath);
95
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
96
+ if (file.isBinary) {
97
+ await fs.copyFile(file.absolutePath, destPath);
98
+ } else {
99
+ await fs.writeFile(destPath, file.content ?? '', 'utf8');
100
+ }
101
+ }
102
+ }
103
+
104
+ private async walkDirectory(rootDir: string): Promise<{
105
+ files: { absolutePath: string; relativePath: string }[];
106
+ hiddenEntriesSkipped: number;
107
+ }> {
108
+ const allFiles: { absolutePath: string; relativePath: string }[] = [];
109
+ let hiddenEntriesSkipped = 0;
110
+ let totalBytes = 0;
111
+
112
+ const visit = async (currentDir: string): Promise<void> => {
113
+ const dirEntries = await fs.readdir(currentDir, { withFileTypes: true });
114
+ for (const entry of dirEntries) {
115
+ if (entry.name.startsWith('.')) {
116
+ hiddenEntriesSkipped += 1;
117
+ continue;
118
+ }
119
+
120
+ const fullPath = path.join(currentDir, entry.name);
121
+ if (entry.isSymbolicLink()) {
122
+ throw new Error('Import source cannot contain symbolic links');
123
+ }
124
+
125
+ if (entry.isDirectory()) {
126
+ await visit(fullPath);
127
+ continue;
128
+ }
129
+
130
+ const stat = await fs.stat(fullPath);
131
+ totalBytes += stat.size;
132
+ if (allFiles.length + 1 > MAX_IMPORT_FILE_COUNT) {
133
+ throw new Error(`Import source has too many files (max ${MAX_IMPORT_FILE_COUNT})`);
134
+ }
135
+ if (totalBytes > MAX_IMPORT_TOTAL_BYTES) {
136
+ throw new Error(
137
+ `Import source is too large (max ${Math.floor(MAX_IMPORT_TOTAL_BYTES / (1024 * 1024))} MB)`
138
+ );
139
+ }
140
+
141
+ allFiles.push({
142
+ absolutePath: fullPath,
143
+ relativePath: path.relative(rootDir, fullPath).replace(/\\/g, '/'),
144
+ });
145
+ }
146
+ };
147
+
148
+ await visit(rootDir);
149
+
150
+ return {
151
+ files: allFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath)),
152
+ hiddenEntriesSkipped,
153
+ };
154
+ }
155
+ }