@yancyyu/openhermit 1.6.29 → 1.6.31

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 (157) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DkXfi2pg.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-CHNNVraw.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-Do-Ff83V.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-nDLhSuJI.js} +1 -1
  5. package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-Bp7fA6sx.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-CPC1HdGy.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DTVKyNTO.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-XVu-AN00.js} +1 -1
  9. package/dist-renderer/assets/channel-CIwbNcUO.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-BcWmVyA-.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-Co4Z2jsE.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-C8q9gfDT.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-qDgb1gxO.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-Cm8Gu2gu.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DYji1BRS.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-DWAS568H.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-CLFzXLA8.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-04A-pvql.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-04A-pvql.js +1 -0
  20. package/dist-renderer/assets/clone-DQnvTIEM.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-CZdGhX_3.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-BVY-G6nO.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CUACvAwG.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-3SfnesSG.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-E3ksXClJ.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-aYjGXss7.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-JMHrrTQs.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-CVQ-R5rN.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-OLn9jq61.js} +1 -1
  30. package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-BAb2J0l8.js} +1 -1
  31. package/dist-renderer/assets/{index-qNBNjW4K.js → index-BSoCjBWn.js} +1 -1
  32. package/dist-renderer/assets/{index-6m1ZAymG.js → index-BtG3HbqP.js} +1 -1
  33. package/dist-renderer/assets/{index-BowUl0Jb.js → index-CH8e7g1f.js} +583 -573
  34. package/dist-renderer/assets/index-CSt8DTcn.css +1 -0
  35. package/dist-renderer/assets/{index-Dp3kJTEe.js → index-Ca4iNkRA.js} +1 -1
  36. package/dist-renderer/assets/{index-vAykq1H1.js → index-DU9PGgZJ.js} +1 -1
  37. package/dist-renderer/assets/{index-TOpt_T7A.js → index-DtMzIS9o.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-CY_ptQNL.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-C2vuHEo_.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-mbdNfu8h.js} +1 -1
  41. package/dist-renderer/assets/{layout-DNANbrI4.js → layout-Do_ArEB1.js} +1 -1
  42. package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-BMlMKyiq.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-Dfntn-o2.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-LiWHsGMV.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-D87St8AF.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-DAa6lHBx.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-VOUngars.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-BzwzmFr2.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-BjAQEJ52.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-BDwy4GJm.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-Y5XBZt3W.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-DzkdUEow.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-D-zbvJOv.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 +63 -35
  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 +111 -15
  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 +74 -123
  120. package/src/renderer/components/team/TeamListFilterPopover.tsx +0 -16
  121. package/src/renderer/components/team/TeamListView.tsx +7 -32
  122. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  123. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +287 -418
  124. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +283 -0
  125. package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
  126. package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
  127. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  128. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  129. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  130. package/src/renderer/store/slices/teamSlice.ts +8 -2
  131. package/src/renderer/utils/multimodelProviderVisibility.ts +17 -0
  132. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +29 -9
  133. package/src/shared/types/api.ts +29 -0
  134. package/src/shared/types/extensions/index.ts +1 -0
  135. package/src/shared/types/extensions/mcp.ts +2 -0
  136. package/src/shared/types/extensions/plugin.ts +2 -1
  137. package/src/shared/types/extensions/skill.ts +7 -0
  138. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  139. package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
  140. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
  141. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
  142. package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
  143. package/dist-renderer/assets/index-BhellmRb.css +0 -1
  144. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
  145. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  146. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  147. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  148. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  149. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  150. package/src/features/recent-projects/main/index.ts +0 -3
  151. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  152. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  153. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  154. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  155. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  156. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  157. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -48,6 +48,7 @@ import {
48
48
  CROSS_TEAM_SOURCE,
49
49
  formatCrossTeamText,
50
50
  } from '@shared/constants/crossTeam';
51
+ import type { CcAgentType } from '../shared/types/ccConnect';
51
52
  import { CcConnectBridge } from './services/ccConnect/CcConnectBridge';
52
53
  import { CcConnectClient } from './services/ccConnect/CcConnectClient';
53
54
  import { TeamProvisioningService } from './services/teams-mvp';
@@ -231,7 +232,11 @@ async function initializeTaskBusFromSettings(): Promise<void> {
231
232
  }
232
233
 
233
234
  taskDispatch.dispose();
234
- await taskDispatch.start(config);
235
+ try {
236
+ await taskDispatch.start(config);
237
+ } catch (err) {
238
+ app.log.warn({ err }, 'Redis connection failed on startup — task bus disabled');
239
+ }
235
240
  }
236
241
 
237
242
  function normalizeStringArray(value: unknown): string[] {
@@ -4617,7 +4622,8 @@ app.get('/api/telemetry/status', async (request, reply) => {
4617
4622
  // no settings
4618
4623
  }
4619
4624
  const taskBus = (settings.taskBus ?? {}) as TaskBusConfig;
4620
- const status = await getTelemetryStatus(taskBus.redis);
4625
+ const redisCfg = taskBus.enabled ? taskBus.redis : undefined;
4626
+ const status = await getTelemetryStatus(redisCfg);
4621
4627
  return (
4622
4628
  status ?? {
4623
4629
  connected: false,
@@ -4727,10 +4733,207 @@ app.get('/api/events', (request, reply) => {
4727
4733
 
4728
4734
  const SSE_FALLBACK_RE = /^\/api\/(.*\/(events|stream|notifications\/stream))$/;
4729
4735
 
4730
- app.get('/api/extensions/mcp/browse', async () => ({
4731
- servers: [],
4732
- items: [],
4733
- }));
4736
+ // ── Extension Store routes (wired to extensionHandlers) ────────────────
4737
+
4738
+ import { extensionHandlers as ext, setSkillsWatcherEmitter } from './ipc/extensions';
4739
+
4740
+ // Broadcast skill file-watcher changes to connected frontends via SSE.
4741
+ setSkillsWatcherEmitter((event) => broadcastSse('skills:changed', event));
4742
+
4743
+ app.get('/api/extensions/plugins', async () => {
4744
+ const result = await ext.pluginGetAll();
4745
+ return result;
4746
+ });
4747
+
4748
+ app.get('/api/extensions/plugins/readme/:pluginId', async (request) => {
4749
+ const { pluginId } = request.params as { pluginId: string };
4750
+ const result = await ext.pluginGetReadme(pluginId);
4751
+ return result;
4752
+ });
4753
+
4754
+ app.post('/api/extensions/plugins/install', async (request) => {
4755
+ const body = request.body as Record<string, unknown>;
4756
+ const result = await ext.pluginInstall(body as any);
4757
+ return result;
4758
+ });
4759
+
4760
+ app.post('/api/extensions/plugins/uninstall', async (request) => {
4761
+ const body = request.body as Record<string, unknown>;
4762
+ const result = await ext.pluginUninstall(
4763
+ body.pluginId as string,
4764
+ body.scope as string,
4765
+ body.projectPath as string,
4766
+ body.harnessType as CcAgentType | undefined
4767
+ );
4768
+ return result;
4769
+ });
4770
+
4771
+ app.get('/api/extensions/mcp/search', async (request) => {
4772
+ const query = (request.query as Record<string, string>).q ?? '';
4773
+ const limit = Number((request.query as Record<string, string>).limit) || 20;
4774
+ const result = await ext.mcpSearch(query, limit);
4775
+ return result;
4776
+ });
4777
+
4778
+ app.get('/api/extensions/mcp/browse', async (request) => {
4779
+ const cursor = (request.query as Record<string, string>).cursor;
4780
+ const limit = Number((request.query as Record<string, string>).limit) || 20;
4781
+ const result = await ext.mcpBrowse(cursor || undefined, limit);
4782
+ return result;
4783
+ });
4784
+
4785
+ app.get('/api/extensions/mcp/installed', async (request) => {
4786
+ const projectPath = (request.query as Record<string, string>).projectPath;
4787
+ const result = await ext.mcpGetInstalled(projectPath);
4788
+ return result;
4789
+ });
4790
+
4791
+ app.get('/api/extensions/mcp/:registryId', async (request) => {
4792
+ const { registryId } = request.params as { registryId: string };
4793
+ const result = await ext.mcpGetById(registryId);
4794
+ return result;
4795
+ });
4796
+
4797
+ app.post('/api/extensions/mcp/install', async (request) => {
4798
+ const body = request.body as Record<string, unknown>;
4799
+ const result = await ext.mcpInstall(body as any);
4800
+ return result;
4801
+ });
4802
+
4803
+ app.post('/api/extensions/mcp/install-custom', async (request) => {
4804
+ const body = request.body as Record<string, unknown>;
4805
+ const result = await ext.mcpInstallCustom(body as any);
4806
+ return result;
4807
+ });
4808
+
4809
+ app.post('/api/extensions/mcp/uninstall', async (request) => {
4810
+ const body = request.body as Record<string, unknown>;
4811
+ const result = await ext.mcpUninstall(
4812
+ body.name as string,
4813
+ body.scope as string,
4814
+ body.projectPath as string,
4815
+ body.harnessType as CcAgentType | undefined
4816
+ );
4817
+ return result;
4818
+ });
4819
+
4820
+ app.get('/api/extensions/skills', async (request) => {
4821
+ const projectPath = (request.query as Record<string, string>).projectPath;
4822
+ const result = await ext.skillsList(projectPath);
4823
+ return result;
4824
+ });
4825
+
4826
+ app.get('/api/extensions/skills/:skillId', async (request) => {
4827
+ const { skillId } = request.params as { skillId: string };
4828
+ const projectPath = (request.query as Record<string, string>).projectPath;
4829
+ const result = await ext.skillsGetDetail(skillId, projectPath);
4830
+ return result;
4831
+ });
4832
+
4833
+ app.post('/api/extensions/skills/upsert', async (request) => {
4834
+ const result = await ext.skillsUpsert(request.body as any);
4835
+ return result;
4836
+ });
4837
+
4838
+ app.post('/api/extensions/skills/delete', async (request) => {
4839
+ const result = await ext.skillsDelete(request.body as any);
4840
+ return result;
4841
+ });
4842
+
4843
+ app.post('/api/extensions/skills/preview-upsert', async (request) => {
4844
+ return ext.skillsPreviewUpsert(request.body as any);
4845
+ });
4846
+
4847
+ app.post('/api/extensions/skills/apply-upsert', async (request) => {
4848
+ return ext.skillsApplyUpsert(request.body as any);
4849
+ });
4850
+
4851
+ app.post('/api/extensions/skills/preview-import', async (request) => {
4852
+ return ext.skillsPreviewImport(request.body as any);
4853
+ });
4854
+
4855
+ app.post('/api/extensions/skills/apply-import', async (request) => {
4856
+ return ext.skillsApplyImport(request.body as any);
4857
+ });
4858
+
4859
+ app.post('/api/extensions/skills/watching/start', async (request) => {
4860
+ const projectPath = (request.query as Record<string, string>).projectPath;
4861
+ return ext.skillsStartWatching(projectPath);
4862
+ });
4863
+
4864
+ app.post('/api/extensions/skills/watching/stop', async (request) => {
4865
+ const { watchId } = (request.body ?? {}) as { watchId?: string };
4866
+ return ext.skillsStopWatching(watchId as string);
4867
+ });
4868
+
4869
+ app.get('/api/extensions/credentials/status', async () => {
4870
+ const result = await ext.credentialsStatus();
4871
+ return result;
4872
+ });
4873
+
4874
+ app.get('/api/extensions/credentials/mcp/:mcpName', async (request) => {
4875
+ const { mcpName } = request.params as { mcpName: string };
4876
+ const result = await ext.credentialsGetMcp(mcpName);
4877
+ return result;
4878
+ });
4879
+
4880
+ app.post('/api/extensions/credentials/mcp', async (request) => {
4881
+ const body = request.body as Record<string, unknown>;
4882
+ const result = await ext.credentialsSaveMcp(
4883
+ body.mcpName as string,
4884
+ body.envValues as Record<string, string>
4885
+ );
4886
+ return result;
4887
+ });
4888
+
4889
+ app.get('/api/extensions/credentials/project-env', async (request) => {
4890
+ const projectPath = (request.query as Record<string, string>).projectPath;
4891
+ if (!projectPath) return { error: 'projectPath required' };
4892
+ const result = await ext.credentialsGetProjectEnv(projectPath);
4893
+ return result;
4894
+ });
4895
+
4896
+ app.post('/api/extensions/credentials/project-env', async (request) => {
4897
+ const body = request.body as Record<string, unknown>;
4898
+ const result = await ext.credentialsSaveProjectEnv(
4899
+ body.projectPath as string,
4900
+ body.vars as Record<string, string>
4901
+ );
4902
+ return result;
4903
+ });
4904
+
4905
+ app.post('/api/extensions/credentials/scan-required', async (request) => {
4906
+ const body = request.body as Record<string, unknown>;
4907
+ const result = await ext.credentialsScanRequired(
4908
+ body.projectPath as string,
4909
+ body.mcpServers as any,
4910
+ body.skillReqs as any
4911
+ );
4912
+ return result;
4913
+ });
4914
+
4915
+ app.get('/api/extensions/credentials/resolve-agent-env', async (request) => {
4916
+ const projectPath = (request.query as Record<string, string>).projectPath;
4917
+ if (!projectPath) return { error: 'projectPath required' };
4918
+ const result = await ext.credentialsResolveAgentEnv(projectPath);
4919
+ return result;
4920
+ });
4921
+
4922
+ app.get('/api/extensions/credentials/skill-env', async (request) => {
4923
+ const folderName = (request.query as Record<string, string>).folderName;
4924
+ if (!folderName) return { error: 'folderName required' };
4925
+ const result = await ext.credentialsGetSkillGlobalEnv(folderName);
4926
+ return result;
4927
+ });
4928
+
4929
+ app.post('/api/extensions/credentials/skill-env', async (request) => {
4930
+ const body = request.body as Record<string, unknown>;
4931
+ const result = await ext.credentialsSaveSkillGlobalEnv(
4932
+ body.folderName as string,
4933
+ body.vars as Record<string, string>
4934
+ );
4935
+ return result;
4936
+ });
4734
4937
 
4735
4938
  app.setNotFoundHandler((request, reply) => {
4736
4939
  const u = request.url;
@@ -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
+ }