@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,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches and caches the Claude Code plugin marketplace catalog.
|
|
3
|
+
*
|
|
4
|
+
* - Fetches marketplace.json from raw.githubusercontent.com
|
|
5
|
+
* - ETag + If-None-Match for bandwidth efficiency
|
|
6
|
+
* - In-memory cache with TTL (15 min)
|
|
7
|
+
* - Stale cache fallback on network error
|
|
8
|
+
* - Deduplicates concurrent requests
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { promises as fsp } from 'node:fs';
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
import https from 'node:https';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { getClaudeBasePath } from '@main/utils/pathDecoder';
|
|
17
|
+
import { buildPluginId } from '@shared/utils/extensionNormalizers';
|
|
18
|
+
import { createLogger } from '@shared/utils/logger';
|
|
19
|
+
|
|
20
|
+
import type { PluginCatalogItem } from '@shared/types/extensions';
|
|
21
|
+
|
|
22
|
+
const logger = createLogger('Extensions:PluginCatalog');
|
|
23
|
+
|
|
24
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const MARKETPLACE_URL =
|
|
27
|
+
'https://raw.githubusercontent.com/anthropics/claude-plugins-official/main/.claude-plugin/marketplace.json';
|
|
28
|
+
|
|
29
|
+
const CACHE_TTL_MS = 15 * 60 * 1_000; // 15 minutes
|
|
30
|
+
const HTTP_TIMEOUT_MS = 15_000; // 15 seconds
|
|
31
|
+
const MAX_REDIRECTS = 5;
|
|
32
|
+
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB safety limit
|
|
33
|
+
const MAX_README_CACHE_SIZE = 50; // Max README entries to cache
|
|
34
|
+
|
|
35
|
+
// ── HTTP helpers (adapted from CliInstallerService) ────────────────────────
|
|
36
|
+
|
|
37
|
+
interface FetchOptions {
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FetchResponse {
|
|
42
|
+
statusCode: number;
|
|
43
|
+
headers: Record<string, string | string[] | undefined>;
|
|
44
|
+
body: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function httpsGetFollowRedirects(
|
|
48
|
+
url: string,
|
|
49
|
+
options: FetchOptions = {},
|
|
50
|
+
redirectsLeft = MAX_REDIRECTS,
|
|
51
|
+
timeoutMs = HTTP_TIMEOUT_MS
|
|
52
|
+
): Promise<FetchResponse> {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const parsedUrl = new URL(url);
|
|
55
|
+
const transport = parsedUrl.protocol === 'http:' ? http : https;
|
|
56
|
+
let settled = false;
|
|
57
|
+
|
|
58
|
+
const settleResolve = (value: FetchResponse): void => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
resolve(value);
|
|
62
|
+
};
|
|
63
|
+
const settleReject = (err: Error): void => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
settled = true;
|
|
66
|
+
reject(err);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const reqOptions = {
|
|
70
|
+
headers: options.headers ?? {},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const req = transport.get(url, reqOptions, (res) => {
|
|
74
|
+
const status = res.statusCode ?? 0;
|
|
75
|
+
|
|
76
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
77
|
+
if (redirectsLeft <= 0) {
|
|
78
|
+
res.destroy();
|
|
79
|
+
settleReject(new Error('Too many redirects'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const redirectUrl = new URL(res.headers.location, url).toString();
|
|
83
|
+
res.destroy();
|
|
84
|
+
httpsGetFollowRedirects(redirectUrl, options, redirectsLeft - 1, timeoutMs).then(
|
|
85
|
+
settleResolve,
|
|
86
|
+
settleReject
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const chunks: Buffer[] = [];
|
|
92
|
+
let totalSize = 0;
|
|
93
|
+
res.on('data', (chunk: Buffer) => {
|
|
94
|
+
totalSize += chunk.length;
|
|
95
|
+
if (totalSize > MAX_BODY_SIZE) {
|
|
96
|
+
res.destroy(new Error(`Response body exceeds ${MAX_BODY_SIZE} bytes`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
chunks.push(chunk);
|
|
100
|
+
});
|
|
101
|
+
res.on('end', () =>
|
|
102
|
+
settleResolve({
|
|
103
|
+
statusCode: status,
|
|
104
|
+
headers: res.headers as Record<string, string | string[] | undefined>,
|
|
105
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
res.on('error', settleReject);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
req.setTimeout(timeoutMs, () => {
|
|
112
|
+
req.destroy(new Error(`Connection timed out after ${timeoutMs}ms fetching ${url}`));
|
|
113
|
+
});
|
|
114
|
+
req.on('error', (err) => settleReject(err instanceof Error ? err : new Error(String(err))));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Marketplace JSON shape ─────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
interface MarketplaceJson {
|
|
121
|
+
name: string;
|
|
122
|
+
plugins: RawMarketplacePlugin[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface RawMarketplacePlugin {
|
|
126
|
+
name: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
version?: string;
|
|
129
|
+
category?: string;
|
|
130
|
+
author?: { name: string; email?: string };
|
|
131
|
+
source: string | { source: string; url: string; sha?: string };
|
|
132
|
+
homepage?: string;
|
|
133
|
+
tags?: string[];
|
|
134
|
+
strict?: boolean;
|
|
135
|
+
lspServers?: Record<string, unknown>;
|
|
136
|
+
mcpServers?: Record<string, unknown>;
|
|
137
|
+
agents?: Record<string, unknown>;
|
|
138
|
+
commands?: Record<string, unknown>;
|
|
139
|
+
hooks?: Record<string, unknown>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Cache ──────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
interface CatalogCache {
|
|
145
|
+
items: PluginCatalogItem[];
|
|
146
|
+
etag: string | null;
|
|
147
|
+
fetchedAt: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Service ────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export class PluginCatalogService {
|
|
153
|
+
private cache: CatalogCache | null = null;
|
|
154
|
+
private fetchInFlight: Promise<PluginCatalogItem[]> | null = null;
|
|
155
|
+
private readmeCache = new Map<string, { content: string | null; fetchedAt: number }>();
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all plugins shown in the store: the official Anthropic marketplace plus
|
|
159
|
+
* any plugins from marketplaces the user has registered locally (e.g. custom
|
|
160
|
+
* git marketplaces like `oh-my-claudecode@omc`). Each source is resilient —
|
|
161
|
+
* a failure in one does not hide the other.
|
|
162
|
+
*/
|
|
163
|
+
async getPlugins(forceRefresh = false): Promise<PluginCatalogItem[]> {
|
|
164
|
+
const [official, local] = await Promise.all([
|
|
165
|
+
this.getOfficialPlugins(forceRefresh).catch((err) => {
|
|
166
|
+
logger.warn('Official marketplace unavailable:', err);
|
|
167
|
+
return [] as PluginCatalogItem[];
|
|
168
|
+
}),
|
|
169
|
+
this.getLocalPlugins().catch((err) => {
|
|
170
|
+
logger.warn('Local marketplaces unavailable:', err);
|
|
171
|
+
return [] as PluginCatalogItem[];
|
|
172
|
+
}),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
// Official wins on pluginId collisions; append local plugins not already present.
|
|
176
|
+
const seen = new Set(official.map((p) => p.pluginId));
|
|
177
|
+
return [...official, ...local.filter((p) => !seen.has(p.pluginId))];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get plugins from the official Anthropic marketplace.
|
|
182
|
+
* Uses in-memory cache with ETag validation.
|
|
183
|
+
*/
|
|
184
|
+
private async getOfficialPlugins(forceRefresh = false): Promise<PluginCatalogItem[]> {
|
|
185
|
+
// Return cached if fresh and not forcing
|
|
186
|
+
if (!forceRefresh && this.cache && Date.now() - this.cache.fetchedAt < CACHE_TTL_MS) {
|
|
187
|
+
return this.cache.items;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Deduplicate concurrent requests
|
|
191
|
+
if (this.fetchInFlight) {
|
|
192
|
+
return this.fetchInFlight;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.fetchInFlight = this.fetchCatalog().finally(() => {
|
|
196
|
+
this.fetchInFlight = null;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return this.fetchInFlight;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Read plugins from marketplaces the user has registered locally.
|
|
204
|
+
*
|
|
205
|
+
* Source of truth: `~/.claude/plugins/known_marketplaces.json`, which maps a
|
|
206
|
+
* marketplace name to its on-disk `installLocation`. Each location holds a
|
|
207
|
+
* `.claude-plugin/marketplace.json` describing its plugins.
|
|
208
|
+
*/
|
|
209
|
+
private async getLocalPlugins(): Promise<PluginCatalogItem[]> {
|
|
210
|
+
const pluginsDir = path.join(getClaudeBasePath(), 'plugins');
|
|
211
|
+
const knownPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
212
|
+
|
|
213
|
+
let known: Record<string, { installLocation?: string }>;
|
|
214
|
+
try {
|
|
215
|
+
known = JSON.parse(await fsp.readFile(knownPath, 'utf-8')) as Record<
|
|
216
|
+
string,
|
|
217
|
+
{ installLocation?: string }
|
|
218
|
+
>;
|
|
219
|
+
} catch {
|
|
220
|
+
return []; // No known marketplaces (or unreadable) — nothing local to add.
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const items: PluginCatalogItem[] = [];
|
|
224
|
+
for (const [, entry] of Object.entries(known)) {
|
|
225
|
+
const installLocation = entry?.installLocation;
|
|
226
|
+
if (!installLocation) continue;
|
|
227
|
+
const marketplaceJsonPath = path.join(installLocation, '.claude-plugin', 'marketplace.json');
|
|
228
|
+
try {
|
|
229
|
+
const json = JSON.parse(
|
|
230
|
+
await fsp.readFile(marketplaceJsonPath, 'utf-8')
|
|
231
|
+
) as MarketplaceJson;
|
|
232
|
+
if (Array.isArray(json.plugins)) {
|
|
233
|
+
items.push(...this.parseMarketplace(json, 'local'));
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
logger.warn(`Failed to read local marketplace at ${marketplaceJsonPath}:`, err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return items;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get README content for a plugin by its pluginId.
|
|
244
|
+
* For external plugins (source is URL), fetches README from the GitHub repo.
|
|
245
|
+
* Returns null for local/bundled plugins or on error.
|
|
246
|
+
*/
|
|
247
|
+
async getPluginReadme(pluginId: string): Promise<string | null> {
|
|
248
|
+
const cached = this.readmeCache.get(pluginId);
|
|
249
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
250
|
+
return cached.content;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Need catalog to find the plugin's repo URL
|
|
254
|
+
const plugins = await this.getPlugins();
|
|
255
|
+
const plugin = plugins.find((p) => p.pluginId === pluginId);
|
|
256
|
+
if (!plugin?.homepage) {
|
|
257
|
+
this.setReadmeCache(pluginId, null);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const readmeUrl = this.buildReadmeUrl(plugin.homepage);
|
|
262
|
+
if (!readmeUrl) {
|
|
263
|
+
this.setReadmeCache(pluginId, null);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const response = await httpsGetFollowRedirects(readmeUrl);
|
|
269
|
+
if (response.statusCode === 200) {
|
|
270
|
+
this.setReadmeCache(pluginId, response.body);
|
|
271
|
+
return response.body;
|
|
272
|
+
}
|
|
273
|
+
this.setReadmeCache(pluginId, null);
|
|
274
|
+
return null;
|
|
275
|
+
} catch (err) {
|
|
276
|
+
logger.warn(`Failed to fetch README for ${pluginId}:`, err);
|
|
277
|
+
this.setReadmeCache(pluginId, null);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Look up a single plugin by pluginId from the cached catalog.
|
|
284
|
+
* Used for main-side re-resolution during install.
|
|
285
|
+
*/
|
|
286
|
+
async resolvePlugin(pluginId: string): Promise<PluginCatalogItem | null> {
|
|
287
|
+
const plugins = await this.getPlugins();
|
|
288
|
+
return plugins.find((p) => p.pluginId === pluginId) ?? null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
/** Set readme cache with LRU eviction */
|
|
294
|
+
private setReadmeCache(pluginId: string, content: string | null): void {
|
|
295
|
+
// Evict oldest entries if at capacity
|
|
296
|
+
if (this.readmeCache.size >= MAX_README_CACHE_SIZE && !this.readmeCache.has(pluginId)) {
|
|
297
|
+
let oldestKey: string | null = null;
|
|
298
|
+
let oldestTime = Infinity;
|
|
299
|
+
for (const [key, entry] of this.readmeCache) {
|
|
300
|
+
if (entry.fetchedAt < oldestTime) {
|
|
301
|
+
oldestTime = entry.fetchedAt;
|
|
302
|
+
oldestKey = key;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (oldestKey) this.readmeCache.delete(oldestKey);
|
|
306
|
+
}
|
|
307
|
+
this.readmeCache.set(pluginId, { content, fetchedAt: Date.now() });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private async fetchCatalog(): Promise<PluginCatalogItem[]> {
|
|
311
|
+
const headers: Record<string, string> = {};
|
|
312
|
+
if (this.cache?.etag) {
|
|
313
|
+
headers['If-None-Match'] = this.cache.etag;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const response = await httpsGetFollowRedirects(MARKETPLACE_URL, { headers });
|
|
318
|
+
|
|
319
|
+
// 304 Not Modified — cache is still valid
|
|
320
|
+
if (response.statusCode === 304 && this.cache) {
|
|
321
|
+
this.cache.fetchedAt = Date.now();
|
|
322
|
+
logger.info('Marketplace catalog not modified (304)');
|
|
323
|
+
return this.cache.items;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (response.statusCode !== 200) {
|
|
327
|
+
throw new Error(`HTTP ${response.statusCode} fetching marketplace`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const json = JSON.parse(response.body) as MarketplaceJson;
|
|
331
|
+
const items = this.parseMarketplace(json);
|
|
332
|
+
const etag = (response.headers.etag as string) ?? null;
|
|
333
|
+
|
|
334
|
+
this.cache = { items, etag, fetchedAt: Date.now() };
|
|
335
|
+
logger.info(`Fetched ${items.length} plugins from marketplace "${json.name}"`);
|
|
336
|
+
return items;
|
|
337
|
+
} catch (err) {
|
|
338
|
+
// Stale cache fallback
|
|
339
|
+
if (this.cache) {
|
|
340
|
+
logger.warn('Marketplace fetch failed, using stale cache:', err);
|
|
341
|
+
return this.cache.items;
|
|
342
|
+
}
|
|
343
|
+
logger.error('Marketplace fetch failed with no cache:', err);
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private parseMarketplace(
|
|
349
|
+
json: MarketplaceJson,
|
|
350
|
+
source: 'official' | 'local' = 'official'
|
|
351
|
+
): PluginCatalogItem[] {
|
|
352
|
+
const marketplaceName = json.name;
|
|
353
|
+
|
|
354
|
+
return json.plugins.map((raw): PluginCatalogItem => {
|
|
355
|
+
const qualifiedName = buildPluginId(raw.name, marketplaceName);
|
|
356
|
+
const isExternal = typeof raw.source === 'object';
|
|
357
|
+
const homepage =
|
|
358
|
+
raw.homepage ?? (isExternal ? (raw.source as { url: string }).url : undefined);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
pluginId: qualifiedName,
|
|
362
|
+
marketplaceId: qualifiedName,
|
|
363
|
+
qualifiedName,
|
|
364
|
+
name: raw.name,
|
|
365
|
+
source,
|
|
366
|
+
description: raw.description ?? '',
|
|
367
|
+
category: raw.category ?? 'other',
|
|
368
|
+
author: raw.author,
|
|
369
|
+
version: raw.version,
|
|
370
|
+
homepage: homepage?.replace(/\.git$/, ''),
|
|
371
|
+
tags: raw.tags,
|
|
372
|
+
hasLspServers: raw.lspServers != null && Object.keys(raw.lspServers).length > 0,
|
|
373
|
+
hasMcpServers: raw.mcpServers != null && Object.keys(raw.mcpServers).length > 0,
|
|
374
|
+
hasAgents: raw.agents != null && Object.keys(raw.agents).length > 0,
|
|
375
|
+
hasCommands: raw.commands != null && Object.keys(raw.commands).length > 0,
|
|
376
|
+
hasHooks: raw.hooks != null && Object.keys(raw.hooks).length > 0,
|
|
377
|
+
isExternal,
|
|
378
|
+
};
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Build raw GitHub README URL from a GitHub repo URL.
|
|
384
|
+
* e.g. https://github.com/org/repo → https://raw.githubusercontent.com/org/repo/main/README.md
|
|
385
|
+
*/
|
|
386
|
+
private buildReadmeUrl(repoUrl: string): string | null {
|
|
387
|
+
const match = /github\.com\/([^/]+)\/([^/]+)/.exec(repoUrl);
|
|
388
|
+
if (!match) return null;
|
|
389
|
+
const [, owner, repo] = match;
|
|
390
|
+
return `https://raw.githubusercontent.com/${owner}/${repo}/main/README.md`;
|
|
391
|
+
}
|
|
392
|
+
}
|