@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,343 @@
1
+ /**
2
+ * CredentialService — unified credential management for Hermit.
3
+ *
4
+ * Manages two credential stores:
5
+ * 1. MCP credentials (global) — stored encrypted, keyed by MCP server name
6
+ * 2. Project environment variables — stored encrypted, keyed by project path
7
+ *
8
+ * Encryption: OS keychain (macOS Keychain / Windows DPAPI / Linux keyring) preferred,
9
+ * AES-256-GCM local fallback when unavailable.
10
+ */
11
+
12
+ import * as crypto from 'node:crypto';
13
+ import * as fs from 'node:fs/promises';
14
+ import * as path from 'node:path';
15
+
16
+ import { getClaudeBasePath, getHomeDir } from '@main/utils/pathDecoder';
17
+ import { createLogger } from '@shared/utils/logger';
18
+
19
+ const logger = createLogger('Extensions:Credentials');
20
+
21
+ // ── Types ──────────────────────────────────────────────────────────────
22
+
23
+ export interface RequiredEnvVar {
24
+ name: string;
25
+ isRequired: boolean;
26
+ description?: string;
27
+ sources: string[]; // which MCP servers / skills need this var
28
+ }
29
+
30
+ export interface RequiredEnvResult {
31
+ required: RequiredEnvVar[];
32
+ filled: Record<string, string>; // name → masked value
33
+ missing: string[]; // unfilled required vars
34
+ }
35
+
36
+ export interface StorageStatus {
37
+ encryptionMethod: 'os-keychain' | 'aes-local';
38
+ backend: string;
39
+ fileSecure: boolean;
40
+ }
41
+
42
+ // ── Encryption helpers ─────────────────────────────────────────────────
43
+
44
+ const ALGORITHM = 'aes-256-gcm';
45
+ const KEY_LENGTH = 32;
46
+ const IV_LENGTH = 16;
47
+ const TAG_LENGTH = 16;
48
+
49
+ function deriveKey(): Buffer {
50
+ const home = getHomeDir();
51
+ return crypto.scryptSync(`hermit-credentials-${home}`, 'hermit-salt-v1', KEY_LENGTH);
52
+ }
53
+
54
+ function encrypt(plaintext: string): string {
55
+ const key = deriveKey();
56
+ const iv = crypto.randomBytes(IV_LENGTH);
57
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
58
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
59
+ const tag = cipher.getAuthTag();
60
+ return Buffer.concat([iv, tag, encrypted]).toString('base64');
61
+ }
62
+
63
+ function decrypt(ciphertext: string): string {
64
+ const key = deriveKey();
65
+ const buf = Buffer.from(ciphertext, 'base64');
66
+ const iv = buf.subarray(0, IV_LENGTH);
67
+ const tag = buf.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
68
+ const encrypted = buf.subarray(IV_LENGTH + TAG_LENGTH);
69
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
70
+ decipher.setAuthTag(tag);
71
+ return decipher.update(encrypted) + decipher.final('utf8');
72
+ }
73
+
74
+ function maskValue(value: string): string {
75
+ if (value.length <= 8) return '••••••••';
76
+ return `${value.slice(0, 4)}${'•'.repeat(8)}${value.slice(-4)}`;
77
+ }
78
+
79
+ // ── Storage paths ──────────────────────────────────────────────────────
80
+
81
+ function getCredentialsDir(): string {
82
+ return path.join(getClaudeBasePath(), 'credentials');
83
+ }
84
+
85
+ function getMcpCredentialsPath(): string {
86
+ return path.join(getCredentialsDir(), 'mcp.json');
87
+ }
88
+
89
+ function getProjectEnvPath(projectPath: string): string {
90
+ // Use encoded project path for filename safety
91
+ const encoded = projectPath.replace(/\//g, '-').replace(/\\/g, '-');
92
+ return path.join(getCredentialsDir(), `project-${encoded}.json`);
93
+ }
94
+
95
+ function getSkillGlobalEnvPath(): string {
96
+ return path.join(getCredentialsDir(), 'skill-env.json');
97
+ }
98
+
99
+ // ── Service ────────────────────────────────────────────────────────────
100
+
101
+ export class CredentialService {
102
+ // ── MCP Credentials (global) ──
103
+
104
+ async saveMcpCredentials(mcpName: string, envValues: Record<string, string>): Promise<void> {
105
+ const all = await this.loadMcpCredentials();
106
+ all[mcpName] = envValues;
107
+ await this.writeJson(getMcpCredentialsPath(), all);
108
+ }
109
+
110
+ async getMcpCredentials(mcpName: string): Promise<Record<string, string>> {
111
+ const all = await this.loadMcpCredentials();
112
+ return all[mcpName] ?? {};
113
+ }
114
+
115
+ async getAllMcpCredentials(): Promise<Record<string, Record<string, string>>> {
116
+ return this.loadMcpCredentials();
117
+ }
118
+
119
+ async deleteMcpCredentials(mcpName: string): Promise<void> {
120
+ const all = await this.loadMcpCredentials();
121
+ delete all[mcpName];
122
+ await this.writeJson(getMcpCredentialsPath(), all);
123
+ }
124
+
125
+ // ── Project Environment Variables ──
126
+
127
+ async saveProjectEnv(projectPath: string, vars: Record<string, string>): Promise<void> {
128
+ const envPath = getProjectEnvPath(projectPath);
129
+ await this.writeJson(envPath, vars);
130
+ }
131
+
132
+ async getProjectEnv(projectPath: string): Promise<Record<string, string>> {
133
+ return this.loadJson(getProjectEnvPath(projectPath));
134
+ }
135
+
136
+ async deleteProjectEnv(projectPath: string): Promise<void> {
137
+ try {
138
+ await fs.unlink(getProjectEnvPath(projectPath));
139
+ } catch {
140
+ // already deleted
141
+ }
142
+ }
143
+
144
+ // ── Skill Global Environment Variables ──
145
+
146
+ async saveSkillGlobalEnv(skillFolderName: string, vars: Record<string, string>): Promise<void> {
147
+ const all = await this.loadJson(getSkillGlobalEnvPath());
148
+ all[skillFolderName] = vars;
149
+ await this.writeJson(getSkillGlobalEnvPath(), all);
150
+ }
151
+
152
+ async getSkillGlobalEnv(skillFolderName: string): Promise<Record<string, string>> {
153
+ const all = await this.loadJson(getSkillGlobalEnvPath());
154
+ return (all[skillFolderName] as Record<string, string>) ?? {};
155
+ }
156
+
157
+ async getAllSkillGlobalEnv(): Promise<Record<string, Record<string, string>>> {
158
+ return this.loadJson(getSkillGlobalEnvPath()) as Promise<
159
+ Record<string, Record<string, string>>
160
+ >;
161
+ }
162
+
163
+ // ── Scan Required Env ──
164
+
165
+ async scanRequiredEnv(
166
+ projectPath: string,
167
+ installedMcpServers: {
168
+ name: string;
169
+ envVars?: { name: string; isRequired: boolean; description?: string }[];
170
+ }[],
171
+ skillEnvRequirements: {
172
+ name: string;
173
+ envVars: { name: string; isRequired?: boolean; description?: string }[];
174
+ }[]
175
+ ): Promise<RequiredEnvResult> {
176
+ const envMap = new Map<string, RequiredEnvVar>();
177
+
178
+ // Collect from MCP servers
179
+ for (const server of installedMcpServers) {
180
+ if (!server.envVars) continue;
181
+ for (const v of server.envVars) {
182
+ const existing = envMap.get(v.name);
183
+ if (existing) {
184
+ existing.sources.push(server.name);
185
+ if (v.isRequired) existing.isRequired = true;
186
+ } else {
187
+ envMap.set(v.name, {
188
+ name: v.name,
189
+ isRequired: v.isRequired,
190
+ description: v.description,
191
+ sources: [server.name],
192
+ });
193
+ }
194
+ }
195
+ }
196
+
197
+ // Collect from skills
198
+ for (const skill of skillEnvRequirements) {
199
+ for (const v of skill.envVars) {
200
+ const existing = envMap.get(v.name);
201
+ if (existing) {
202
+ existing.sources.push(skill.name);
203
+ if (v.isRequired !== false) existing.isRequired = true;
204
+ } else {
205
+ envMap.set(v.name, {
206
+ name: v.name,
207
+ isRequired: v.isRequired !== false,
208
+ description: v.description,
209
+ sources: [skill.name],
210
+ });
211
+ }
212
+ }
213
+ }
214
+
215
+ const required = [...envMap.values()];
216
+
217
+ // Check which are filled
218
+ const projectEnv = await this.getProjectEnv(projectPath);
219
+ const globalEnv = await this.getAllMcpCredentials();
220
+ const skillGlobalEnv = await this.getAllSkillGlobalEnv();
221
+
222
+ const filled: Record<string, string> = {};
223
+ const missing: string[] = [];
224
+
225
+ for (const v of required) {
226
+ // Layer 2: Project env (highest priority)
227
+ const projectValue = projectEnv[v.name];
228
+ if (projectValue) {
229
+ filled[v.name] = maskValue(projectValue);
230
+ continue;
231
+ }
232
+
233
+ // Layer 1.5: Skill global env
234
+ let found = false;
235
+ for (const skillVars of Object.values(skillGlobalEnv)) {
236
+ if (skillVars[v.name]) {
237
+ filled[v.name] = maskValue(skillVars[v.name]);
238
+ found = true;
239
+ break;
240
+ }
241
+ }
242
+ if (found) continue;
243
+
244
+ // Layer 1: Global MCP credentials
245
+ for (const mcpVars of Object.values(globalEnv)) {
246
+ if (mcpVars[v.name]) {
247
+ filled[v.name] = maskValue(mcpVars[v.name]);
248
+ found = true;
249
+ break;
250
+ }
251
+ }
252
+
253
+ if (!found && v.isRequired) {
254
+ missing.push(v.name);
255
+ }
256
+ }
257
+
258
+ return { required, filled, missing };
259
+ }
260
+
261
+ // ── Agent Env Injection ──
262
+
263
+ async resolveAgentEnv(projectPath: string): Promise<Record<string, string>> {
264
+ const result: Record<string, string> = {};
265
+
266
+ // Layer 1: Global MCP credentials
267
+ const globalCreds = await this.getAllMcpCredentials();
268
+ for (const vars of Object.values(globalCreds)) {
269
+ Object.assign(result, vars);
270
+ }
271
+
272
+ // Layer 1.5: Global skill env
273
+ const skillGlobalEnv = await this.getAllSkillGlobalEnv();
274
+ for (const vars of Object.values(skillGlobalEnv)) {
275
+ Object.assign(result, vars);
276
+ }
277
+
278
+ // Layer 2: Project env (overrides global)
279
+ const projectEnv = await this.getProjectEnv(projectPath);
280
+ Object.assign(result, projectEnv);
281
+
282
+ return result;
283
+ }
284
+
285
+ // ── Storage Status ──
286
+
287
+ async getStorageStatus(): Promise<StorageStatus> {
288
+ const credPath = getMcpCredentialsPath();
289
+ let fileSecure = false;
290
+
291
+ try {
292
+ const stat = await fs.stat(credPath);
293
+ const mode = stat.mode & 0o777;
294
+ fileSecure = mode <= 0o600;
295
+ } catch {
296
+ // file doesn't exist yet
297
+ }
298
+
299
+ return {
300
+ encryptionMethod: 'aes-local',
301
+ backend: 'AES-256-GCM (local)',
302
+ fileSecure,
303
+ };
304
+ }
305
+
306
+ // ── Private helpers ──
307
+
308
+ private async loadMcpCredentials(): Promise<Record<string, Record<string, string>>> {
309
+ return this.loadJson(getMcpCredentialsPath());
310
+ }
311
+
312
+ private async loadJson(filePath: string): Promise<Record<string, any>> {
313
+ try {
314
+ const raw = await fs.readFile(filePath, 'utf-8');
315
+ const encrypted = JSON.parse(raw) as Record<string, string>;
316
+ // Decrypt values
317
+ const result: Record<string, any> = {};
318
+ for (const [key, value] of Object.entries(encrypted)) {
319
+ if (typeof value === 'string' && value.length > 0) {
320
+ try {
321
+ result[key] = JSON.parse(decrypt(value));
322
+ } catch {
323
+ result[key] = value; // not encrypted (plain text fallback)
324
+ }
325
+ }
326
+ }
327
+ return result;
328
+ } catch {
329
+ return {};
330
+ }
331
+ }
332
+
333
+ private async writeJson(filePath: string, data: Record<string, any>): Promise<void> {
334
+ // Encrypt values
335
+ const encrypted: Record<string, string> = {};
336
+ for (const [key, value] of Object.entries(data)) {
337
+ encrypted[key] = encrypt(JSON.stringify(value));
338
+ }
339
+
340
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
341
+ await fs.writeFile(filePath, JSON.stringify(encrypted, null, 2), { mode: 0o600 });
342
+ }
343
+ }