@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.
- package/dist-renderer/assets/ProjectEditorOverlay-DsQt4FHy.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
- package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
- package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
- package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
- package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
- package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
- package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
- package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
- package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
- package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
- package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
- package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
- package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +4 -1
- package/src/main/ipc/extensions.ts +353 -0
- package/src/main/server.ts +907 -184
- package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
- package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
- package/src/main/services/extensions/install/McpInstallService.ts +407 -0
- package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
- package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
- package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
- package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
- package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
- package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
- package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
- package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
- package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
- package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
- package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
- package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
- package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
- package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
- package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
- package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
- package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
- package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
- package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
- package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
- package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
- package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
- package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
- package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
- package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
- package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
- package/src/main/services/team/cliFlavor.ts +54 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
- package/src/main/services/teams-mvp/index.ts +3 -0
- package/src/main/utils/atomicWrite.ts +72 -0
- package/src/main/utils/childProcess.ts +554 -0
- package/src/main/utils/cliEnv.ts +54 -0
- package/src/main/utils/cliPathMerge.ts +97 -0
- package/src/main/utils/pathDecoder.ts +664 -0
- package/src/main/utils/pathValidation.ts +432 -0
- package/src/main/utils/shellEnv.ts +331 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +128 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
- package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
- package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
- package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
- package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/layout/PaneContent.tsx +2 -0
- package/src/renderer/components/layout/SortableTab.tsx +1 -0
- package/src/renderer/components/layout/TabBarActions.tsx +12 -12
- package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +55 -98
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
- package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
- package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
- package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
- package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
- package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +67 -25
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +58 -0
- package/src/shared/types/extensions/index.ts +1 -0
- package/src/shared/types/extensions/mcp.ts +2 -0
- package/src/shared/types/extensions/plugin.ts +2 -1
- package/src/shared/types/extensions/skill.ts +7 -0
- package/src/shared/types/team.ts +104 -1
- package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
- package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
- package/dist-renderer/assets/index-BIOJremZ.css +0 -1
- package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
- package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
- package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
- package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
- package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
- package/src/features/recent-projects/main/index.ts +0 -3
- package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
- package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
- package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
- package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
- package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
- package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
- package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facade service that combines plugin catalog + MCP catalog + installation state
|
|
3
|
+
* into enriched data ready for the renderer.
|
|
4
|
+
*
|
|
5
|
+
* Also provides install target resolution for the security model
|
|
6
|
+
* (main-side re-resolution: renderer sends pluginId/registryId, main resolves from catalog).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createLogger } from '@shared/utils/logger';
|
|
10
|
+
|
|
11
|
+
import { type McpCatalogAggregator } from './catalog/McpCatalogAggregator';
|
|
12
|
+
import { type PluginCatalogService } from './catalog/PluginCatalogService';
|
|
13
|
+
import { type McpInstallationStateService } from './state/McpInstallationStateService';
|
|
14
|
+
import { type PluginInstallationStateService } from './state/PluginInstallationStateService';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
EnrichedPlugin,
|
|
18
|
+
InstalledMcpEntry,
|
|
19
|
+
McpCatalogItem,
|
|
20
|
+
McpSearchResult,
|
|
21
|
+
PluginCatalogItem,
|
|
22
|
+
} from '@shared/types/extensions';
|
|
23
|
+
|
|
24
|
+
const logger = createLogger('Extensions:Facade');
|
|
25
|
+
|
|
26
|
+
export class ExtensionFacadeService {
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly pluginCatalog: PluginCatalogService,
|
|
29
|
+
private readonly pluginState: PluginInstallationStateService,
|
|
30
|
+
private readonly mcpAggregator: McpCatalogAggregator | null = null,
|
|
31
|
+
private readonly mcpState: McpInstallationStateService | null = null
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
// ── Plugin methods ───────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get all plugins enriched with install status and counts.
|
|
38
|
+
*/
|
|
39
|
+
async getEnrichedPlugins(projectPath?: string, forceRefresh = false): Promise<EnrichedPlugin[]> {
|
|
40
|
+
const [catalog, installed, counts] = await Promise.all([
|
|
41
|
+
this.pluginCatalog.getPlugins(forceRefresh),
|
|
42
|
+
this.pluginState.getInstalledPlugins(projectPath),
|
|
43
|
+
this.pluginState.getInstallCounts(),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// Build installed lookup: pluginId → entries[]
|
|
47
|
+
const installedMap = new Map<string, typeof installed>();
|
|
48
|
+
for (const entry of installed) {
|
|
49
|
+
const list = installedMap.get(entry.pluginId) ?? [];
|
|
50
|
+
list.push(entry);
|
|
51
|
+
installedMap.set(entry.pluginId, list);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return catalog.map((item): EnrichedPlugin => {
|
|
55
|
+
const installations = installedMap.get(item.pluginId) ?? [];
|
|
56
|
+
const installCount = counts.get(item.pluginId) ?? 0;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
...item,
|
|
60
|
+
installCount,
|
|
61
|
+
isInstalled: installations.length > 0,
|
|
62
|
+
installations,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get README content for a plugin.
|
|
69
|
+
*/
|
|
70
|
+
async getPluginReadme(pluginId: string): Promise<string | null> {
|
|
71
|
+
return this.pluginCatalog.getPluginReadme(pluginId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a pluginId to its install target.
|
|
76
|
+
*/
|
|
77
|
+
async resolvePluginInstallTarget(
|
|
78
|
+
pluginId: string
|
|
79
|
+
): Promise<{ qualifiedName: string; plugin: PluginCatalogItem } | null> {
|
|
80
|
+
const plugin = await this.pluginCatalog.resolvePlugin(pluginId);
|
|
81
|
+
if (!plugin) {
|
|
82
|
+
logger.warn(`Cannot resolve install target: pluginId "${pluginId}" not found in catalog`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return { qualifiedName: plugin.qualifiedName, plugin };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── MCP methods ──────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Search MCP servers across both registries.
|
|
92
|
+
*/
|
|
93
|
+
async searchMcp(query: string, limit?: number): Promise<McpSearchResult> {
|
|
94
|
+
if (!this.mcpAggregator) {
|
|
95
|
+
return { servers: [], warnings: ['MCP catalog not configured'] };
|
|
96
|
+
}
|
|
97
|
+
return this.mcpAggregator.search(query, limit);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Browse MCP catalog with pagination.
|
|
102
|
+
*/
|
|
103
|
+
async browseMcp(
|
|
104
|
+
cursor?: string,
|
|
105
|
+
limit?: number
|
|
106
|
+
): Promise<{ servers: McpCatalogItem[]; nextCursor?: string }> {
|
|
107
|
+
if (!this.mcpAggregator) {
|
|
108
|
+
return { servers: [] };
|
|
109
|
+
}
|
|
110
|
+
return this.mcpAggregator.browse(cursor, limit);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get a single MCP server by registry ID (for install flow).
|
|
115
|
+
*/
|
|
116
|
+
async getMcpById(registryId: string): Promise<McpCatalogItem | null> {
|
|
117
|
+
if (!this.mcpAggregator) return null;
|
|
118
|
+
return this.mcpAggregator.getById(registryId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get installed MCP servers.
|
|
123
|
+
*/
|
|
124
|
+
async getInstalledMcp(projectPath?: string): Promise<InstalledMcpEntry[]> {
|
|
125
|
+
if (!this.mcpState) return [];
|
|
126
|
+
return this.mcpState.getInstalled(projectPath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Cache invalidation ───────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
invalidateInstalledCache(): void {
|
|
132
|
+
this.pluginState.invalidateCache();
|
|
133
|
+
this.mcpState?.invalidateCache();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches MCP server data from the Glama.ai API.
|
|
3
|
+
*
|
|
4
|
+
* Optional enrichment layer — NOT a hard dependency.
|
|
5
|
+
* Provides: license, tools, Glama URL.
|
|
6
|
+
* Does NOT provide install info (no packages/remotes).
|
|
7
|
+
*
|
|
8
|
+
* Base URL: https://glama.ai/api/mcp/v1/servers
|
|
9
|
+
* Cursor-based pagination (after), no auth required.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
import https from 'node:https';
|
|
14
|
+
|
|
15
|
+
import { createLogger } from '@shared/utils/logger';
|
|
16
|
+
|
|
17
|
+
import type { McpCatalogItem, McpHostingType, McpToolDef } from '@shared/types/extensions';
|
|
18
|
+
|
|
19
|
+
const logger = createLogger('Extensions:GlamaMcp');
|
|
20
|
+
|
|
21
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const GLAMA_BASE_URL = 'https://glama.ai/api/mcp/v1/servers';
|
|
24
|
+
const HTTP_TIMEOUT_MS = 15_000;
|
|
25
|
+
const MAX_REDIRECTS = 5;
|
|
26
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB safety limit
|
|
27
|
+
|
|
28
|
+
// ── HTTP helper ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function httpGet(
|
|
31
|
+
url: string,
|
|
32
|
+
redirectsLeft = MAX_REDIRECTS
|
|
33
|
+
): Promise<{ statusCode: number; body: string }> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const parsedUrl = new URL(url);
|
|
36
|
+
const transport = parsedUrl.protocol === 'http:' ? http : https;
|
|
37
|
+
let settled = false;
|
|
38
|
+
|
|
39
|
+
const settleResolve = (v: { statusCode: number; body: string }): void => {
|
|
40
|
+
if (!settled) {
|
|
41
|
+
settled = true;
|
|
42
|
+
resolve(v);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const settleReject = (e: Error): void => {
|
|
46
|
+
if (!settled) {
|
|
47
|
+
settled = true;
|
|
48
|
+
reject(e);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const req = transport.get(url, (res) => {
|
|
53
|
+
const status = res.statusCode ?? 0;
|
|
54
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
55
|
+
if (redirectsLeft <= 0) {
|
|
56
|
+
res.destroy();
|
|
57
|
+
settleReject(new Error('Too many redirects'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
res.destroy();
|
|
61
|
+
httpGet(new URL(res.headers.location, url).toString(), redirectsLeft - 1).then(
|
|
62
|
+
settleResolve,
|
|
63
|
+
settleReject
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const chunks: Buffer[] = [];
|
|
68
|
+
let totalSize = 0;
|
|
69
|
+
res.on('data', (c: Buffer) => {
|
|
70
|
+
totalSize += c.length;
|
|
71
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
72
|
+
res.destroy(new Error(`Response body exceeds ${MAX_BODY_SIZE} bytes`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
chunks.push(c);
|
|
76
|
+
});
|
|
77
|
+
res.on('end', () =>
|
|
78
|
+
settleResolve({ statusCode: status, body: Buffer.concat(chunks).toString('utf-8') })
|
|
79
|
+
);
|
|
80
|
+
res.on('error', settleReject);
|
|
81
|
+
});
|
|
82
|
+
req.setTimeout(HTTP_TIMEOUT_MS, () => req.destroy(new Error(`Timeout fetching ${url}`)));
|
|
83
|
+
req.on('error', (e) => settleReject(e instanceof Error ? e : new Error(String(e))));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Raw Glama API shapes ───────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
interface GlamaResponse {
|
|
90
|
+
pageInfo: {
|
|
91
|
+
endCursor?: string;
|
|
92
|
+
hasNextPage?: boolean;
|
|
93
|
+
};
|
|
94
|
+
servers: GlamaServer[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface GlamaServer {
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
namespace?: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
slug?: string;
|
|
103
|
+
url?: string;
|
|
104
|
+
repository?: { url: string };
|
|
105
|
+
spdxLicense?: { name: string; url?: string } | null;
|
|
106
|
+
tools?: { name?: string; description?: string }[];
|
|
107
|
+
attributes?: string[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Service ────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export class GlamaMcpEnrichmentService {
|
|
113
|
+
/**
|
|
114
|
+
* Search Glama for MCP servers.
|
|
115
|
+
*/
|
|
116
|
+
async search(query: string, limit = 20): Promise<McpCatalogItem[]> {
|
|
117
|
+
const params = new URLSearchParams({ search: query, first: String(limit) });
|
|
118
|
+
const url = `${GLAMA_BASE_URL}?${params}`;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const resp = await httpGet(url);
|
|
122
|
+
if (resp.statusCode !== 200) throw new Error(`HTTP ${resp.statusCode}`);
|
|
123
|
+
const json = JSON.parse(resp.body) as GlamaResponse;
|
|
124
|
+
return json.servers.map((s) => this.normalize(s));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.warn('Glama MCP search failed:', err);
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Browse Glama catalog with cursor pagination.
|
|
133
|
+
*/
|
|
134
|
+
async browse(
|
|
135
|
+
cursor?: string,
|
|
136
|
+
limit = 20
|
|
137
|
+
): Promise<{ servers: McpCatalogItem[]; nextCursor?: string }> {
|
|
138
|
+
const params = new URLSearchParams({ first: String(limit) });
|
|
139
|
+
if (cursor) params.set('after', cursor);
|
|
140
|
+
const url = `${GLAMA_BASE_URL}?${params}`;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const resp = await httpGet(url);
|
|
144
|
+
if (resp.statusCode !== 200) throw new Error(`HTTP ${resp.statusCode}`);
|
|
145
|
+
const json = JSON.parse(resp.body) as GlamaResponse;
|
|
146
|
+
return {
|
|
147
|
+
servers: json.servers.map((s) => this.normalize(s)),
|
|
148
|
+
nextCursor: json.pageInfo.hasNextPage ? json.pageInfo.endCursor : undefined,
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
logger.warn('Glama MCP browse failed:', err);
|
|
152
|
+
return { servers: [] };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
private normalize(raw: GlamaServer): McpCatalogItem {
|
|
159
|
+
const tools: McpToolDef[] = (raw.tools ?? [])
|
|
160
|
+
.filter((t): t is { name: string; description: string } => Boolean(t.name))
|
|
161
|
+
.map((t) => ({ name: t.name, description: t.description ?? '' }));
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
id: `glama:${raw.id}`,
|
|
165
|
+
name: raw.name,
|
|
166
|
+
description: raw.description ?? '',
|
|
167
|
+
repositoryUrl: raw.repository?.url,
|
|
168
|
+
version: undefined, // Glama doesn't expose version
|
|
169
|
+
source: 'glama',
|
|
170
|
+
installSpec: null, // Glama has NO install info
|
|
171
|
+
envVars: [],
|
|
172
|
+
license: raw.spdxLicense?.name,
|
|
173
|
+
tools,
|
|
174
|
+
glamaUrl: raw.url,
|
|
175
|
+
requiresAuth: false,
|
|
176
|
+
author: raw.namespace,
|
|
177
|
+
hostingType: this.deriveHostingType(raw.attributes),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private deriveHostingType(attributes?: string[]): McpHostingType | undefined {
|
|
182
|
+
if (!attributes?.length) return undefined;
|
|
183
|
+
const hasLocal = attributes.includes('hosting:local-only');
|
|
184
|
+
const hasRemote = attributes.includes('hosting:remote-capable');
|
|
185
|
+
if (hasLocal && hasRemote) return 'both';
|
|
186
|
+
if (hasLocal) return 'local';
|
|
187
|
+
if (hasRemote) return 'remote';
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregates MCP catalog data from Official Registry + Glama.
|
|
3
|
+
*
|
|
4
|
+
* - Uses Promise.allSettled so partial API failures don't break the whole catalog
|
|
5
|
+
* - Dedup by repository URL; Official source takes priority
|
|
6
|
+
* - Enriches Official entries with Glama data (license, tools) when matched
|
|
7
|
+
* - Provides getById() for secure install flow
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { normalizeRepoUrl } from '@shared/utils/extensionNormalizers';
|
|
11
|
+
import { createLogger } from '@shared/utils/logger';
|
|
12
|
+
|
|
13
|
+
import { type GlamaMcpEnrichmentService } from './GlamaMcpEnrichmentService';
|
|
14
|
+
import { type OfficialMcpRegistryService } from './OfficialMcpRegistryService';
|
|
15
|
+
|
|
16
|
+
import type { McpCatalogItem, McpSearchResult } from '@shared/types/extensions';
|
|
17
|
+
|
|
18
|
+
const logger = createLogger('Extensions:McpAggregator');
|
|
19
|
+
|
|
20
|
+
export class McpCatalogAggregator {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly official: OfficialMcpRegistryService,
|
|
23
|
+
private readonly glama: GlamaMcpEnrichmentService
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Search both sources and return merged results.
|
|
28
|
+
*/
|
|
29
|
+
async search(query: string, limit = 20): Promise<McpSearchResult> {
|
|
30
|
+
const warnings: string[] = [];
|
|
31
|
+
|
|
32
|
+
const [officialResult, glamaResult] = await Promise.allSettled([
|
|
33
|
+
this.official.search(query, limit),
|
|
34
|
+
this.glama.search(query, limit),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const officialServers = officialResult.status === 'fulfilled' ? officialResult.value : [];
|
|
38
|
+
const glamaServers = glamaResult.status === 'fulfilled' ? glamaResult.value : [];
|
|
39
|
+
|
|
40
|
+
if (officialResult.status === 'rejected') {
|
|
41
|
+
warnings.push('Official MCP Registry unavailable');
|
|
42
|
+
logger.warn('Official registry search failed:', officialResult.reason);
|
|
43
|
+
}
|
|
44
|
+
if (glamaResult.status === 'rejected') {
|
|
45
|
+
warnings.push('Glama enrichment unavailable');
|
|
46
|
+
logger.warn('Glama search failed:', glamaResult.reason);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const merged = this.mergeAndDeduplicate(officialServers, glamaServers);
|
|
50
|
+
|
|
51
|
+
return { servers: merged, warnings };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Browse the official registry with optional Glama enrichment.
|
|
56
|
+
*/
|
|
57
|
+
async browse(
|
|
58
|
+
cursor?: string,
|
|
59
|
+
limit = 20
|
|
60
|
+
): Promise<{ servers: McpCatalogItem[]; nextCursor?: string }> {
|
|
61
|
+
// Browse primarily from official registry (has pagination)
|
|
62
|
+
const result = await this.official.browse(cursor, limit);
|
|
63
|
+
|
|
64
|
+
// Optionally enrich with Glama data (best effort, no pagination sync)
|
|
65
|
+
try {
|
|
66
|
+
const glamaBrowse = await this.glama.browse(undefined, limit);
|
|
67
|
+
const enriched = this.enrichOfficialWithGlama(result.servers, glamaBrowse.servers);
|
|
68
|
+
return { servers: enriched, nextCursor: result.nextCursor };
|
|
69
|
+
} catch {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a single server by ID for secure install flow.
|
|
76
|
+
* Delegates to appropriate source based on ID prefix.
|
|
77
|
+
*/
|
|
78
|
+
async getById(registryId: string): Promise<McpCatalogItem | null> {
|
|
79
|
+
// Glama IDs are prefixed with "glama:"
|
|
80
|
+
if (registryId.startsWith('glama:')) {
|
|
81
|
+
logger.warn(`Cannot install Glama-only server: ${registryId}`);
|
|
82
|
+
return null; // Glama servers can't be auto-installed
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Official registry lookup
|
|
86
|
+
return this.official.getById(registryId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Merge Official + Glama, dedup by repository URL.
|
|
93
|
+
* Official entries take priority.
|
|
94
|
+
*/
|
|
95
|
+
private mergeAndDeduplicate(
|
|
96
|
+
official: McpCatalogItem[],
|
|
97
|
+
glama: McpCatalogItem[]
|
|
98
|
+
): McpCatalogItem[] {
|
|
99
|
+
// Build repo URL index from official entries
|
|
100
|
+
const officialRepoUrls = new Set<string>();
|
|
101
|
+
for (const item of official) {
|
|
102
|
+
if (item.repositoryUrl) {
|
|
103
|
+
officialRepoUrls.add(normalizeRepoUrl(item.repositoryUrl));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Enrich official entries with Glama data
|
|
108
|
+
const enriched = this.enrichOfficialWithGlama(official, glama);
|
|
109
|
+
|
|
110
|
+
// Add Glama-only entries (no matching official entry)
|
|
111
|
+
const glamaOnly = glama.filter((g) => {
|
|
112
|
+
if (!g.repositoryUrl) return true; // no repo URL = can't match, show separately
|
|
113
|
+
return !officialRepoUrls.has(normalizeRepoUrl(g.repositoryUrl));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
return [...enriched, ...glamaOnly];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Enrich official entries with Glama metadata (license, tools, glamaUrl).
|
|
121
|
+
*/
|
|
122
|
+
private enrichOfficialWithGlama(
|
|
123
|
+
official: McpCatalogItem[],
|
|
124
|
+
glama: McpCatalogItem[]
|
|
125
|
+
): McpCatalogItem[] {
|
|
126
|
+
// Index Glama by normalized repo URL
|
|
127
|
+
const glamaByRepo = new Map<string, McpCatalogItem>();
|
|
128
|
+
for (const g of glama) {
|
|
129
|
+
if (g.repositoryUrl) {
|
|
130
|
+
glamaByRepo.set(normalizeRepoUrl(g.repositoryUrl), g);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return official.map((item) => {
|
|
135
|
+
if (!item.repositoryUrl) return item;
|
|
136
|
+
|
|
137
|
+
const glamaMatch = glamaByRepo.get(normalizeRepoUrl(item.repositoryUrl));
|
|
138
|
+
if (!glamaMatch) return item;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
...item,
|
|
142
|
+
license: item.license ?? glamaMatch.license,
|
|
143
|
+
tools: item.tools.length > 0 ? item.tools : glamaMatch.tools,
|
|
144
|
+
glamaUrl: glamaMatch.glamaUrl,
|
|
145
|
+
author: item.author ?? glamaMatch.author,
|
|
146
|
+
hostingType: item.hostingType ?? glamaMatch.hostingType,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|