@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.
Files changed (177) hide show
  1. package/dist-renderer/assets/ProjectEditorOverlay-DsQt4FHy.js +52 -0
  2. package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
  5. package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
  9. package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.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-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
  30. package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
  31. package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
  32. package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
  33. package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
  34. package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
  35. package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
  36. package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
  37. package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
  41. package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
  42. package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
  52. package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.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 +907 -184
  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/session-intelligence/UsageTelemetryService.ts +33 -18
  93. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  94. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  95. package/src/main/services/team/cliFlavor.ts +54 -0
  96. package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
  97. package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
  98. package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
  99. package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
  100. package/src/main/services/teams-mvp/index.ts +3 -0
  101. package/src/main/utils/atomicWrite.ts +72 -0
  102. package/src/main/utils/childProcess.ts +554 -0
  103. package/src/main/utils/cliEnv.ts +54 -0
  104. package/src/main/utils/cliPathMerge.ts +97 -0
  105. package/src/main/utils/pathDecoder.ts +664 -0
  106. package/src/main/utils/pathValidation.ts +432 -0
  107. package/src/main/utils/shellEnv.ts +331 -0
  108. package/src/renderer/App.tsx +5 -0
  109. package/src/renderer/api/httpClient.ts +128 -0
  110. package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
  111. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  112. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  113. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  114. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  115. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  116. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  117. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
  118. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  119. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  120. package/src/renderer/components/layout/PaneContent.tsx +2 -0
  121. package/src/renderer/components/layout/SortableTab.tsx +1 -0
  122. package/src/renderer/components/layout/TabBarActions.tsx +12 -12
  123. package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
  124. package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
  125. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  126. package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
  127. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  128. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  129. package/src/renderer/components/tasks/TasksView.tsx +343 -0
  130. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  131. package/src/renderer/components/team/TeamDetailView.tsx +55 -98
  132. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  133. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
  134. package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
  135. package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
  136. package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
  137. package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
  138. package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
  139. package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
  140. package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
  141. package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
  142. package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
  143. package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
  144. package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
  145. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  146. package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
  147. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  148. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  149. package/src/renderer/store/slices/scheduleSlice.ts +21 -0
  150. package/src/renderer/store/slices/teamSlice.ts +67 -25
  151. package/src/renderer/types/tabs.ts +1 -0
  152. package/src/shared/types/api.ts +58 -0
  153. package/src/shared/types/extensions/index.ts +1 -0
  154. package/src/shared/types/extensions/mcp.ts +2 -0
  155. package/src/shared/types/extensions/plugin.ts +2 -1
  156. package/src/shared/types/extensions/skill.ts +7 -0
  157. package/src/shared/types/team.ts +104 -1
  158. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  159. package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
  160. package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
  161. package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
  162. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
  163. package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
  164. package/dist-renderer/assets/index-BIOJremZ.css +0 -1
  165. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  166. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  167. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  168. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  169. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  170. package/src/features/recent-projects/main/index.ts +0 -3
  171. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  172. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  173. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  174. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  175. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  176. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  177. 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
+ }
@@ -17,6 +17,7 @@ const KEY_WORK_SECONDS = (slug: string) => `hermit:usage:${slug}:workSeconds`;
17
17
  const KEY_PROJECTS = (slug: string) => `hermit:usage:${slug}:projects`;
18
18
 
19
19
  let scanInterval: ReturnType<typeof setInterval> | null = null;
20
+ let lastLocalScan: TelemetryStatusResult | null = null;
20
21
 
21
22
  function redisConfig(cfg: TaskBusConfig) {
22
23
  return {
@@ -103,12 +104,19 @@ async function uploadMetrics(client: Redis, slug: string, result: ParseResult):
103
104
  async function doScan(cfg: TaskBusConfig): Promise<ParseResult | null> {
104
105
  if (!cfg.telemetry?.enabled) return null;
105
106
 
107
+ const result = await scanSessions();
108
+ lastLocalScan = statusFromParseResult(result, false);
109
+
110
+ if (!cfg.telemetry.uploadEnabled) {
111
+ return result;
112
+ }
113
+
106
114
  const client = await getRedis(cfg);
107
- if (!client) return null;
115
+ if (!client) return result;
108
116
 
109
117
  try {
110
- const result = await scanSessions();
111
118
  await uploadMetrics(client, 'global', result);
119
+ lastLocalScan = statusFromParseResult(result, true);
112
120
  return result;
113
121
  } finally {
114
122
  try {
@@ -171,15 +179,35 @@ interface TelemetryStatusResult {
171
179
  workSecondsByDay: Record<string, number>;
172
180
  }
173
181
 
182
+ function statusFromParseResult(result: ParseResult, connected: boolean): TelemetryStatusResult {
183
+ const { aggregate } = result;
184
+ return {
185
+ connected,
186
+ lastScan: new Date().toISOString(),
187
+ sessions: aggregate.sessions,
188
+ messages: aggregate.messages,
189
+ tokensIn: aggregate.tokens.input,
190
+ tokensOut: aggregate.tokens.output,
191
+ cacheRead: aggregate.tokens.cacheRead,
192
+ cacheCreation: aggregate.tokens.cacheCreation,
193
+ activeDays: aggregate.activeDays,
194
+ hourly: aggregate.hourly,
195
+ projects: aggregate.projects,
196
+ workSecondsByDay: aggregate.workSecondsByDay,
197
+ };
198
+ }
199
+
174
200
  export async function getTelemetryStatus(
175
- redisCfg: TaskBusConfig['redis']
201
+ redisCfg?: TaskBusConfig['redis']
176
202
  ): Promise<TelemetryStatusResult | null> {
203
+ if (!redisCfg) return lastLocalScan;
204
+
177
205
  let Redis: typeof import('ioredis').default;
178
206
  try {
179
207
  const mod = await import('ioredis');
180
208
  Redis = mod.default;
181
209
  } catch {
182
- return null;
210
+ return lastLocalScan;
183
211
  }
184
212
 
185
213
  const cfg = { redis: redisCfg };
@@ -188,20 +216,7 @@ export async function getTelemetryStatus(
188
216
  await client.connect();
189
217
  await client.ping();
190
218
  } catch {
191
- return {
192
- connected: false,
193
- lastScan: null,
194
- sessions: 0,
195
- messages: 0,
196
- tokensIn: 0,
197
- tokensOut: 0,
198
- cacheRead: 0,
199
- cacheCreation: 0,
200
- activeDays: 0,
201
- hourly: [],
202
- projects: [],
203
- workSecondsByDay: {},
204
- };
219
+ return lastLocalScan;
205
220
  }
206
221
 
207
222
  try {