@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.
- package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DkXfi2pg.js} +1 -1
- package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-CHNNVraw.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-Do-Ff83V.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-nDLhSuJI.js} +1 -1
- package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-Bp7fA6sx.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-CPC1HdGy.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DTVKyNTO.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-XVu-AN00.js} +1 -1
- package/dist-renderer/assets/channel-CIwbNcUO.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-BcWmVyA-.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-Co4Z2jsE.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-C8q9gfDT.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-qDgb1gxO.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-Cm8Gu2gu.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DYji1BRS.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-DWAS568H.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-CLFzXLA8.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-04A-pvql.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-04A-pvql.js +1 -0
- package/dist-renderer/assets/clone-DQnvTIEM.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-CZdGhX_3.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-BVY-G6nO.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CUACvAwG.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-3SfnesSG.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-E3ksXClJ.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-aYjGXss7.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-JMHrrTQs.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-CVQ-R5rN.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-OLn9jq61.js} +1 -1
- package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-BAb2J0l8.js} +1 -1
- package/dist-renderer/assets/{index-qNBNjW4K.js → index-BSoCjBWn.js} +1 -1
- package/dist-renderer/assets/{index-6m1ZAymG.js → index-BtG3HbqP.js} +1 -1
- package/dist-renderer/assets/{index-BowUl0Jb.js → index-CH8e7g1f.js} +583 -573
- package/dist-renderer/assets/index-CSt8DTcn.css +1 -0
- package/dist-renderer/assets/{index-Dp3kJTEe.js → index-Ca4iNkRA.js} +1 -1
- package/dist-renderer/assets/{index-vAykq1H1.js → index-DU9PGgZJ.js} +1 -1
- package/dist-renderer/assets/{index-TOpt_T7A.js → index-DtMzIS9o.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-CY_ptQNL.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-C2vuHEo_.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-mbdNfu8h.js} +1 -1
- package/dist-renderer/assets/{layout-DNANbrI4.js → layout-Do_ArEB1.js} +1 -1
- package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-BMlMKyiq.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-Dfntn-o2.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-LiWHsGMV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-D87St8AF.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-DAa6lHBx.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-VOUngars.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-BzwzmFr2.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-BjAQEJ52.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-BDwy4GJm.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-Y5XBZt3W.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DzkdUEow.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-D-zbvJOv.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 +209 -6
- 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/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/TaskDispatchService.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/api/httpClient.ts +61 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +63 -35
- 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 +111 -15
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +74 -123
- package/src/renderer/components/team/TeamListFilterPopover.tsx +0 -16
- package/src/renderer/components/team/TeamListView.tsx +7 -32
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +287 -418
- package/src/renderer/components/team/dialogs/useTeamEditForm.ts +283 -0
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
- package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/teamSlice.ts +8 -2
- package/src/renderer/utils/multimodelProviderVisibility.ts +17 -0
- package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +29 -9
- package/src/shared/types/api.ts +29 -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/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
- package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
- package/dist-renderer/assets/index-BhellmRb.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
- 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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads plugin installed state and install counts from the filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Sources:
|
|
5
|
+
* - Installed state: ~/.claude/plugins/installed_plugins.json
|
|
6
|
+
* - Install counts: ~/.claude/plugins/install-counts-cache.json
|
|
7
|
+
*
|
|
8
|
+
* Both files are managed by the Claude CLI. This service is read-only.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs/promises';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { getClaudeBasePath } from '@main/utils/pathDecoder';
|
|
15
|
+
import { createLogger } from '@shared/utils/logger';
|
|
16
|
+
|
|
17
|
+
import type { InstalledPluginEntry } from '@shared/types/extensions';
|
|
18
|
+
import type { InstallScope } from '@shared/types/extensions';
|
|
19
|
+
|
|
20
|
+
const logger = createLogger('Extensions:PluginState');
|
|
21
|
+
|
|
22
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const INSTALLED_STATE_TTL_MS = 10_000; // 10 seconds
|
|
25
|
+
const INSTALL_COUNTS_TTL_MS = 5 * 60_000; // 5 minutes
|
|
26
|
+
|
|
27
|
+
// ── Raw file shapes ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
interface InstalledPluginsJson {
|
|
30
|
+
version: number;
|
|
31
|
+
plugins: Record<
|
|
32
|
+
string, // qualifiedName
|
|
33
|
+
{
|
|
34
|
+
scope: string;
|
|
35
|
+
installPath?: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
installedAt?: string;
|
|
38
|
+
lastUpdated?: string;
|
|
39
|
+
gitCommitSha?: string;
|
|
40
|
+
}[]
|
|
41
|
+
>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface InstallCountsJson {
|
|
45
|
+
version: number;
|
|
46
|
+
fetchedAt: string;
|
|
47
|
+
counts: {
|
|
48
|
+
plugin: string; // qualifiedName format
|
|
49
|
+
unique_installs: number;
|
|
50
|
+
}[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Cache ──────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
interface TimedCache<T> {
|
|
56
|
+
data: T;
|
|
57
|
+
fetchedAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Service ────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export class PluginInstallationStateService {
|
|
63
|
+
private installedCache = new Map<string, TimedCache<InstalledPluginEntry[]>>();
|
|
64
|
+
private countsCache: TimedCache<Map<string, number>> | null = null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get installed plugins relevant to the active context.
|
|
68
|
+
* Always includes user scope. Project/local entries are only included when
|
|
69
|
+
* they are enabled for the active project.
|
|
70
|
+
*/
|
|
71
|
+
async getInstalledPlugins(projectPath?: string): Promise<InstalledPluginEntry[]> {
|
|
72
|
+
const normalizedProjectPath =
|
|
73
|
+
typeof projectPath === 'string' && path.isAbsolute(projectPath) ? projectPath : undefined;
|
|
74
|
+
const cacheKey = this.getInstalledCacheKey(normalizedProjectPath);
|
|
75
|
+
const cached = this.installedCache.get(cacheKey);
|
|
76
|
+
|
|
77
|
+
if (cached && Date.now() - cached.fetchedAt < INSTALLED_STATE_TTL_MS) {
|
|
78
|
+
return cached.data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const entries = await this.buildInstalledEntriesForContext(normalizedProjectPath);
|
|
82
|
+
this.installedCache.set(cacheKey, { data: entries, fetchedAt: Date.now() });
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get install counts keyed by pluginId (qualifiedName).
|
|
88
|
+
*/
|
|
89
|
+
async getInstallCounts(): Promise<Map<string, number>> {
|
|
90
|
+
if (this.countsCache && Date.now() - this.countsCache.fetchedAt < INSTALL_COUNTS_TTL_MS) {
|
|
91
|
+
return this.countsCache.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const counts = await this.readInstallCounts();
|
|
95
|
+
this.countsCache = { data: counts, fetchedAt: Date.now() };
|
|
96
|
+
return counts;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Invalidate all caches. Call after install/uninstall operations.
|
|
101
|
+
*/
|
|
102
|
+
invalidateCache(): void {
|
|
103
|
+
this.installedCache.clear();
|
|
104
|
+
this.countsCache = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Private ────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
private getPluginsDir(): string {
|
|
110
|
+
return path.join(getClaudeBasePath(), 'plugins');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getInstalledCacheKey(projectPath?: string): string {
|
|
114
|
+
return projectPath ?? '__user__';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async buildInstalledEntriesForContext(
|
|
118
|
+
projectPath?: string
|
|
119
|
+
): Promise<InstalledPluginEntry[]> {
|
|
120
|
+
const installedMetadata = await this.readInstalledPluginMetadata();
|
|
121
|
+
const metadataByKey = new Map<string, InstalledPluginEntry[]>();
|
|
122
|
+
|
|
123
|
+
for (const entry of installedMetadata) {
|
|
124
|
+
const key = this.getPluginScopeKey(entry.pluginId, entry.scope);
|
|
125
|
+
const matches = metadataByKey.get(key) ?? [];
|
|
126
|
+
matches.push(entry);
|
|
127
|
+
metadataByKey.set(key, matches);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const userEnabled = await this.readEnabledPlugins(
|
|
131
|
+
path.join(getClaudeBasePath(), 'settings.json')
|
|
132
|
+
);
|
|
133
|
+
const projectEnabled = projectPath
|
|
134
|
+
? await this.readEnabledPlugins(path.join(projectPath, '.claude', 'settings.json'))
|
|
135
|
+
: new Set<string>();
|
|
136
|
+
const localEnabled = projectPath
|
|
137
|
+
? await this.readEnabledPlugins(path.join(projectPath, '.claude', 'settings.local.json'))
|
|
138
|
+
: new Set<string>();
|
|
139
|
+
|
|
140
|
+
return [
|
|
141
|
+
...this.buildScopedEntries('user', userEnabled, metadataByKey),
|
|
142
|
+
...this.buildScopedEntries('project', projectEnabled, metadataByKey),
|
|
143
|
+
...this.buildScopedEntries('local', localEnabled, metadataByKey),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private buildScopedEntries(
|
|
148
|
+
scope: InstallScope,
|
|
149
|
+
enabledPlugins: Set<string>,
|
|
150
|
+
metadataByKey: Map<string, InstalledPluginEntry[]>
|
|
151
|
+
): InstalledPluginEntry[] {
|
|
152
|
+
return Array.from(enabledPlugins).map((pluginId) => {
|
|
153
|
+
const key = this.getPluginScopeKey(pluginId, scope);
|
|
154
|
+
const bestMatch = this.pickBestInstallationEntry(metadataByKey.get(key) ?? []);
|
|
155
|
+
|
|
156
|
+
return bestMatch
|
|
157
|
+
? {
|
|
158
|
+
...bestMatch,
|
|
159
|
+
pluginId,
|
|
160
|
+
scope,
|
|
161
|
+
}
|
|
162
|
+
: {
|
|
163
|
+
pluginId,
|
|
164
|
+
scope,
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private getPluginScopeKey(pluginId: string, scope: InstallScope): string {
|
|
170
|
+
return `${pluginId}::${scope}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private pickBestInstallationEntry(entries: InstalledPluginEntry[]): InstalledPluginEntry | null {
|
|
174
|
+
if (entries.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [...entries].sort((left, right) => {
|
|
179
|
+
const leftInstalledAt = Date.parse(left.installedAt ?? '');
|
|
180
|
+
const rightInstalledAt = Date.parse(right.installedAt ?? '');
|
|
181
|
+
const normalizedLeft = Number.isFinite(leftInstalledAt) ? leftInstalledAt : 0;
|
|
182
|
+
const normalizedRight = Number.isFinite(rightInstalledAt) ? rightInstalledAt : 0;
|
|
183
|
+
return normalizedRight - normalizedLeft;
|
|
184
|
+
})[0];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async readInstalledPluginMetadata(): Promise<InstalledPluginEntry[]> {
|
|
188
|
+
const filePath = path.join(this.getPluginsDir(), 'installed_plugins.json');
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
192
|
+
const json = JSON.parse(raw) as InstalledPluginsJson;
|
|
193
|
+
|
|
194
|
+
if (json.version !== 2 || !json.plugins) {
|
|
195
|
+
logger.warn(`Unexpected installed_plugins.json version: ${json.version}`);
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const entries: InstalledPluginEntry[] = [];
|
|
200
|
+
|
|
201
|
+
for (const [qualifiedName, installations] of Object.entries(json.plugins)) {
|
|
202
|
+
for (const inst of installations) {
|
|
203
|
+
entries.push({
|
|
204
|
+
pluginId: qualifiedName,
|
|
205
|
+
scope: this.normalizeScope(inst.scope),
|
|
206
|
+
version: inst.version,
|
|
207
|
+
installedAt: inst.installedAt,
|
|
208
|
+
installPath: inst.installPath,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return entries;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
216
|
+
return []; // No plugins installed yet
|
|
217
|
+
}
|
|
218
|
+
logger.error('Failed to read installed_plugins.json:', err);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private async readEnabledPlugins(filePath: string): Promise<Set<string>> {
|
|
224
|
+
try {
|
|
225
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
226
|
+
const json = JSON.parse(raw) as {
|
|
227
|
+
enabledPlugins?: Record<string, boolean> | null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (!json.enabledPlugins || typeof json.enabledPlugins !== 'object') {
|
|
231
|
+
return new Set<string>();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return new Set(
|
|
235
|
+
Object.entries(json.enabledPlugins)
|
|
236
|
+
.filter(([, enabled]) => enabled === true)
|
|
237
|
+
.map(([pluginId]) => pluginId)
|
|
238
|
+
);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
241
|
+
return new Set<string>();
|
|
242
|
+
}
|
|
243
|
+
logger.error(`Failed to read plugin settings from ${filePath}:`, err);
|
|
244
|
+
return new Set<string>();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async readInstallCounts(): Promise<Map<string, number>> {
|
|
249
|
+
const filePath = path.join(this.getPluginsDir(), 'install-counts-cache.json');
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
253
|
+
const json = JSON.parse(raw) as InstallCountsJson;
|
|
254
|
+
|
|
255
|
+
const map = new Map<string, number>();
|
|
256
|
+
|
|
257
|
+
if (json.counts && Array.isArray(json.counts)) {
|
|
258
|
+
for (const entry of json.counts) {
|
|
259
|
+
// Install counts use qualifiedName format (name@marketplace)
|
|
260
|
+
map.set(entry.plugin, entry.unique_installs);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return map;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
267
|
+
return new Map();
|
|
268
|
+
}
|
|
269
|
+
logger.error('Failed to read install-counts-cache.json:', err);
|
|
270
|
+
return new Map();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private normalizeScope(raw: string): InstallScope {
|
|
275
|
+
const lower = raw.toLowerCase();
|
|
276
|
+
if (lower === 'user' || lower === 'project' || lower === 'local') {
|
|
277
|
+
return lower;
|
|
278
|
+
}
|
|
279
|
+
return 'user'; // safe default
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { atomicWriteAsync } from '@main/utils/atomicWrite';
|
|
2
|
+
import {
|
|
3
|
+
getAppDataPath,
|
|
4
|
+
getAutoDetectedClaudeBasePath,
|
|
5
|
+
getClaudeBasePath,
|
|
6
|
+
getHomeDir,
|
|
7
|
+
} from '@main/utils/pathDecoder';
|
|
8
|
+
import { createHash, randomUUID } from 'crypto';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
export const AGENT_TEAMS_IDENTITY_STORE_PATH_ENV = 'AGENT_TEAMS_IDENTITY_STORE_PATH';
|
|
13
|
+
export const AGENT_TEAMS_IDENTITY_SCHEMA_VERSION = 1;
|
|
14
|
+
const SENTRY_ANONYMOUS_USER_PREFIX = 'agent-teams-sentry-v1:';
|
|
15
|
+
const IDENTITY_DIR_MODE = 0o700;
|
|
16
|
+
const IDENTITY_FILE_MODE = 0o600;
|
|
17
|
+
|
|
18
|
+
type ParsedJson = null | boolean | number | string | ParsedJson[] | { [key: string]: ParsedJson };
|
|
19
|
+
|
|
20
|
+
export type AgentTeamsIdentitySource = 'app-data' | 'legacy-global-config' | 'created';
|
|
21
|
+
|
|
22
|
+
export interface AgentTeamsIdentityStoreV1 {
|
|
23
|
+
schemaVersion: typeof AGENT_TEAMS_IDENTITY_SCHEMA_VERSION;
|
|
24
|
+
clientId: string;
|
|
25
|
+
session?: Record<string, unknown>;
|
|
26
|
+
capabilities?: Record<string, unknown>;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentTeamsClientIdentity {
|
|
32
|
+
clientId: string;
|
|
33
|
+
source: AgentTeamsIdentitySource;
|
|
34
|
+
storePath: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface LegacyAgentTeamsState {
|
|
38
|
+
clientId: string;
|
|
39
|
+
session?: Record<string, unknown>;
|
|
40
|
+
capabilities?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isValidAgentTeamsClientId(value: unknown): value is string {
|
|
48
|
+
return (
|
|
49
|
+
typeof value === 'string' &&
|
|
50
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
55
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pickObjectField(
|
|
59
|
+
record: Record<string, unknown>,
|
|
60
|
+
key: string
|
|
61
|
+
): Record<string, unknown> | undefined {
|
|
62
|
+
const value = record[key];
|
|
63
|
+
return isRecord(value) ? value : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getAgentTeamsIdentityStorePath(): string {
|
|
67
|
+
return path.join(getAppDataPath(), 'identity', 'agent-teams-client.json');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function applyAgentTeamsIdentityEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
71
|
+
const existing = env[AGENT_TEAMS_IDENTITY_STORE_PATH_ENV];
|
|
72
|
+
if (!isNonEmptyString(existing)) {
|
|
73
|
+
env[AGENT_TEAMS_IDENTITY_STORE_PATH_ENV] = getAgentTeamsIdentityStorePath();
|
|
74
|
+
}
|
|
75
|
+
return env;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getSentryAnonymousUserId(clientId: string): string {
|
|
79
|
+
if (!isValidAgentTeamsClientId(clientId)) {
|
|
80
|
+
throw new Error('Invalid Agent Teams clientId');
|
|
81
|
+
}
|
|
82
|
+
return createHash('sha256').update(`${SENTRY_ANONYMOUS_USER_PREFIX}${clientId}`).digest('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getLegacyGlobalConfigPath(): string {
|
|
86
|
+
const claudeBasePath = getClaudeBasePath();
|
|
87
|
+
return claudeBasePath !== getAutoDetectedClaudeBasePath()
|
|
88
|
+
? path.join(claudeBasePath, '.claude.json')
|
|
89
|
+
: path.join(getHomeDir(), '.claude.json');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readJsonFile(filePath: string): Promise<ParsedJson | undefined> {
|
|
93
|
+
try {
|
|
94
|
+
const raw = await fs.promises.readFile(filePath, 'utf8');
|
|
95
|
+
return JSON.parse(raw) as ParsedJson;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
98
|
+
if (code === 'ENOENT') {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
await fs.promises.stat(filePath);
|
|
108
|
+
return true;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return (error as NodeJS.ErrnoException).code !== 'ENOENT';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeStoreRecord(value: unknown): AgentTeamsIdentityStoreV1 | null {
|
|
115
|
+
if (!isRecord(value)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (value.schemaVersion !== AGENT_TEAMS_IDENTITY_SCHEMA_VERSION) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!isValidAgentTeamsClientId(value.clientId)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const createdAt = isNonEmptyString(value.createdAt) ? value.createdAt : new Date().toISOString();
|
|
128
|
+
const updatedAt = isNonEmptyString(value.updatedAt) ? value.updatedAt : createdAt;
|
|
129
|
+
return {
|
|
130
|
+
schemaVersion: AGENT_TEAMS_IDENTITY_SCHEMA_VERSION,
|
|
131
|
+
clientId: value.clientId,
|
|
132
|
+
session: pickObjectField(value, 'session'),
|
|
133
|
+
capabilities: pickObjectField(value, 'capabilities'),
|
|
134
|
+
createdAt,
|
|
135
|
+
updatedAt,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeLegacyAgentTeams(value: unknown): LegacyAgentTeamsState | null {
|
|
140
|
+
if (!isRecord(value) || !isValidAgentTeamsClientId(value.clientId)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
clientId: value.clientId,
|
|
146
|
+
session: pickObjectField(value, 'session'),
|
|
147
|
+
capabilities: pickObjectField(value, 'capabilities'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readLegacyAgentTeamsState(): Promise<LegacyAgentTeamsState | null> {
|
|
152
|
+
const legacyConfig = await readJsonFile(getLegacyGlobalConfigPath());
|
|
153
|
+
if (!isRecord(legacyConfig)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return normalizeLegacyAgentTeams(legacyConfig.agentTeams);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildStoreRecord(
|
|
161
|
+
source: LegacyAgentTeamsState | null,
|
|
162
|
+
options?: { existingCreatedAt?: string }
|
|
163
|
+
): AgentTeamsIdentityStoreV1 {
|
|
164
|
+
const now = new Date().toISOString();
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: AGENT_TEAMS_IDENTITY_SCHEMA_VERSION,
|
|
167
|
+
clientId: source?.clientId ?? randomUUID(),
|
|
168
|
+
session: source?.session,
|
|
169
|
+
capabilities: source?.capabilities,
|
|
170
|
+
createdAt: options?.existingCreatedAt ?? now,
|
|
171
|
+
updatedAt: now,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function writeStoreRecord(
|
|
176
|
+
storePath: string,
|
|
177
|
+
record: AgentTeamsIdentityStoreV1
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
const dir = path.dirname(storePath);
|
|
180
|
+
await fs.promises.mkdir(dir, { recursive: true, mode: IDENTITY_DIR_MODE });
|
|
181
|
+
await fs.promises.chmod(dir, IDENTITY_DIR_MODE).catch(() => undefined);
|
|
182
|
+
await atomicWriteAsync(storePath, `${JSON.stringify(record, null, 2)}\n`);
|
|
183
|
+
await fs.promises.chmod(storePath, IDENTITY_FILE_MODE).catch(() => undefined);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function loadAppDataIdentity(storePath: string): Promise<AgentTeamsIdentityStoreV1 | null> {
|
|
187
|
+
return normalizeStoreRecord(await readJsonFile(storePath));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function ensureAgentTeamsClientIdentity(options?: {
|
|
191
|
+
storePath?: string;
|
|
192
|
+
}): Promise<AgentTeamsClientIdentity> {
|
|
193
|
+
const storePath = options?.storePath ?? getAgentTeamsIdentityStorePath();
|
|
194
|
+
const existing = await loadAppDataIdentity(storePath);
|
|
195
|
+
if (existing) {
|
|
196
|
+
return {
|
|
197
|
+
clientId: existing.clientId,
|
|
198
|
+
source: 'app-data',
|
|
199
|
+
storePath,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const legacy = !(await pathExists(storePath)) ? await readLegacyAgentTeamsState() : null;
|
|
204
|
+
const record = buildStoreRecord(legacy);
|
|
205
|
+
await writeStoreRecord(storePath, record);
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
clientId: record.clientId,
|
|
209
|
+
source: legacy ? 'legacy-global-config' : 'created',
|
|
210
|
+
storePath,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function readAgentTeamsIdentityStore(options?: {
|
|
215
|
+
storePath?: string;
|
|
216
|
+
}): Promise<AgentTeamsIdentityStoreV1 | null> {
|
|
217
|
+
return loadAppDataIdentity(options?.storePath ?? getAgentTeamsIdentityStorePath());
|
|
218
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-aware CLI environment builder.
|
|
3
|
+
*
|
|
4
|
+
* Builds an enriched environment for CLI processes that accounts for
|
|
5
|
+
* provider-specific configuration (API keys, base URLs, etc.).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: The full source in claude_agent_teams_ui depends on several services
|
|
8
|
+
* not yet available in this project (ProviderConnectionService, OpenCodeRuntime,
|
|
9
|
+
* codex-runtime-installer). This module provides the core interface and
|
|
10
|
+
* environment building, falling back gracefully when those services are absent.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { buildEnrichedEnv } from '@main/utils/cliEnv';
|
|
14
|
+
|
|
15
|
+
export interface ProviderAwareCliEnvOptions {
|
|
16
|
+
binaryPath?: string | null;
|
|
17
|
+
providerId?: string;
|
|
18
|
+
providerBackendId?: string | null;
|
|
19
|
+
shellEnv?: NodeJS.ProcessEnv | null;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
connectionMode?: 'strict' | 'augment';
|
|
22
|
+
allowStoredApiKeyDecryption?: boolean;
|
|
23
|
+
allowedStoredApiKeyEnvVarNames?: readonly string[];
|
|
24
|
+
projectPath?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ProviderAwareCliEnvResult {
|
|
28
|
+
env: NodeJS.ProcessEnv;
|
|
29
|
+
connectionIssues: Record<string, string>;
|
|
30
|
+
providerArgs: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function buildProviderAwareCliEnv(
|
|
34
|
+
options: ProviderAwareCliEnvOptions = {}
|
|
35
|
+
): Promise<ProviderAwareCliEnvResult> {
|
|
36
|
+
const env = buildEnrichedEnv(options.binaryPath);
|
|
37
|
+
|
|
38
|
+
// Remove ELECTRON_RUN_AS_NODE to prevent child processes from thinking
|
|
39
|
+
// they are running in Node.js mode instead of Electron mode.
|
|
40
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
41
|
+
|
|
42
|
+
// Inject project-level env vars (from CredentialService) when a projectPath is provided
|
|
43
|
+
if (options.projectPath) {
|
|
44
|
+
try {
|
|
45
|
+
const { CredentialService } =
|
|
46
|
+
await import('@main/services/extensions/credentials/CredentialService');
|
|
47
|
+
const credentials = new CredentialService();
|
|
48
|
+
const projectEnv = await credentials.resolveAgentEnv(options.projectPath);
|
|
49
|
+
Object.assign(env, projectEnv);
|
|
50
|
+
} catch {
|
|
51
|
+
// Non-critical — CLI will use system env as fallback
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
env,
|
|
57
|
+
connectionIssues: {},
|
|
58
|
+
providerArgs: [],
|
|
59
|
+
};
|
|
60
|
+
}
|