@yancyyu/openhermit 1.6.29 → 1.6.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DsQt4FHy.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
  20. package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-BowUl0Jb.js → index-BlOrAXp3.js} +542 -532
  32. package/dist-renderer/assets/{index-6m1ZAymG.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-Dp3kJTEe.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-TOpt_T7A.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-qNBNjW4K.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-vAykq1H1.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-DNANbrI4.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-CGKpOUF2.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +4 -1
  56. package/src/main/ipc/extensions.ts +353 -0
  57. package/src/main/server.ts +209 -6
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
  59. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
  60. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
  61. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
  62. package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
  63. package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
  64. package/src/main/services/extensions/install/McpInstallService.ts +407 -0
  65. package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
  66. package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
  67. package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
  68. package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
  69. package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
  70. package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
  71. package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
  72. package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
  73. package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
  74. package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
  75. package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
  76. package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
  77. package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
  78. package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
  79. package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
  80. package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
  81. package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
  82. package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
  83. package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
  84. package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
  85. package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
  86. package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
  87. package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
  88. package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
  89. package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
  90. package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
  91. package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
  92. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  93. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  94. package/src/main/services/team/cliFlavor.ts +54 -0
  95. package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
  96. package/src/main/utils/atomicWrite.ts +72 -0
  97. package/src/main/utils/childProcess.ts +554 -0
  98. package/src/main/utils/cliEnv.ts +54 -0
  99. package/src/main/utils/cliPathMerge.ts +97 -0
  100. package/src/main/utils/pathDecoder.ts +664 -0
  101. package/src/main/utils/pathValidation.ts +432 -0
  102. package/src/main/utils/shellEnv.ts +331 -0
  103. package/src/renderer/api/httpClient.ts +61 -0
  104. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  105. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  106. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  107. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  108. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  109. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  110. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  111. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  112. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  113. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  114. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  115. package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
  116. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  117. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  118. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  119. package/src/renderer/components/team/TeamDetailView.tsx +35 -0
  120. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  121. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  122. package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
  123. package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
  124. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  125. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  126. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  127. package/src/renderer/store/slices/teamSlice.ts +8 -2
  128. package/src/shared/types/api.ts +29 -0
  129. package/src/shared/types/extensions/index.ts +1 -0
  130. package/src/shared/types/extensions/mcp.ts +2 -0
  131. package/src/shared/types/extensions/plugin.ts +2 -1
  132. package/src/shared/types/extensions/skill.ts +7 -0
  133. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  134. package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
  135. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
  136. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
  137. package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
  138. package/dist-renderer/assets/index-BhellmRb.css +0 -1
  139. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
  140. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  141. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  142. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  143. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  144. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  145. package/src/features/recent-projects/main/index.ts +0 -3
  146. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  147. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  148. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  149. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  150. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  151. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  152. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,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
+ }