@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,664 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * Utility functions for encoding/decoding Claude Code project directory names.
7
+ *
8
+ * Directory naming pattern:
9
+ * - Path: /Users/username/projectname
10
+ * - Encoded: -Users-username-projectname
11
+ *
12
+ * IMPORTANT: This encoding is LOSSY for paths containing dashes.
13
+ * For accurate path resolution, use extractCwd() from jsonl.ts to read
14
+ * the actual cwd from session files.
15
+ */
16
+
17
+ // =============================================================================
18
+ // Core Encoding/Decoding
19
+ // =============================================================================
20
+
21
+ /**
22
+ * Encodes an absolute path into Claude Code's directory naming format.
23
+ * Replaces all path separators (/ and \) with dashes.
24
+ *
25
+ * @param absolutePath - The absolute path to encode (e.g., "/Users/username/projectname")
26
+ * @returns The encoded directory name (e.g., "-Users-username-projectname")
27
+ */
28
+ export function encodePath(absolutePath: string): string {
29
+ if (!absolutePath) {
30
+ return '';
31
+ }
32
+
33
+ const encoded = absolutePath.replace(/[/\\]/g, '-');
34
+ const windowsDriveMatch = /^([a-zA-Z]):-(.*)$/.exec(encoded);
35
+ if (windowsDriveMatch) {
36
+ return `${windowsDriveMatch[1].toUpperCase()}--${windowsDriveMatch[2]}`;
37
+ }
38
+
39
+ // Ensure leading dash for absolute paths
40
+ return encoded.startsWith('-') ? encoded : `-${encoded}`;
41
+ }
42
+
43
+ function isWindowsAbsolutePathLike(name: string): boolean {
44
+ const slashPath = name.replace(/\\/g, '/');
45
+ return /^[a-zA-Z]:\//.test(slashPath) || slashPath.startsWith('//');
46
+ }
47
+
48
+ function normalizeWindowsPathForStorageKey(name: string): string {
49
+ if (!isWindowsAbsolutePathLike(name)) {
50
+ return name;
51
+ }
52
+ return name.replace(/\\/g, '/').toLowerCase();
53
+ }
54
+
55
+ /**
56
+ * Matches the orchestrator's cross-platform storage key codec.
57
+ * It lowercases Windows absolute paths, normalizes separators, and replaces
58
+ * every non-ASCII-alphanumeric character with a dash.
59
+ */
60
+ export function encodePathPortable(absolutePath: string): string {
61
+ if (!absolutePath) {
62
+ return '';
63
+ }
64
+ return normalizeWindowsPathForStorageKey(absolutePath).replace(/[^a-zA-Z0-9]/g, '-');
65
+ }
66
+
67
+ /**
68
+ * Decodes a project directory name to its original path.
69
+ * Note: This is a best-effort decode. Paths with dashes cannot be decoded accurately.
70
+ *
71
+ * @param encodedName - The encoded directory name (e.g., "-Users-username-projectname")
72
+ * @returns The decoded path (e.g., "/Users/username/projectname")
73
+ */
74
+ export function decodePath(encodedName: string): string {
75
+ if (!encodedName) {
76
+ return '';
77
+ }
78
+
79
+ // Legacy Windows format observed in some Claude installs: "C--Users-name-project"
80
+ // (no leading dash, drive separator encoded as "--").
81
+ const legacyWindowsRegex = /^([a-zA-Z])--(.*)$/;
82
+ const legacyWindowsMatch = legacyWindowsRegex.exec(encodedName);
83
+ if (legacyWindowsMatch) {
84
+ const drive = legacyWindowsMatch[1].toUpperCase();
85
+ const rest = legacyWindowsMatch[2].replace(/-/g, '/');
86
+ return `${drive}:/${rest}`;
87
+ }
88
+
89
+ // Remove leading dash if present (indicates absolute path)
90
+ const withoutLeadingDash = encodedName.startsWith('-') ? encodedName.slice(1) : encodedName;
91
+
92
+ // Replace dashes with slashes
93
+ const decodedPath = withoutLeadingDash.replace(/-/g, '/');
94
+
95
+ // Windows paths may decode to "C:/..."
96
+ if (/^[a-zA-Z]:\//.test(decodedPath)) {
97
+ return decodedPath;
98
+ }
99
+
100
+ // Ensure leading slash for POSIX-style absolute paths
101
+ const absolutePath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
102
+
103
+ // Translate WSL mount paths to Windows drive-letter paths on Windows
104
+ return translateWslMountPath(absolutePath);
105
+ }
106
+
107
+ /**
108
+ * Extract the project name (last path segment) from an encoded directory name.
109
+ *
110
+ * @param encodedName - The encoded directory name
111
+ * @returns The project name
112
+ */
113
+ export function extractProjectName(encodedName: string, cwdHint?: string): string {
114
+ // Prefer cwdHint (actual filesystem path) since decodePath is lossy for
115
+ // paths containing dashes (e.g., "claude-devtools" -> "claude/code/context").
116
+ if (cwdHint) {
117
+ const segments = cwdHint.split(/[/\\]/).filter(Boolean);
118
+ const last = segments[segments.length - 1];
119
+ if (last) return last;
120
+ }
121
+ const decoded = decodePath(encodedName);
122
+ const segments = decoded.split('/').filter(Boolean);
123
+ return segments[segments.length - 1] || encodedName;
124
+ }
125
+
126
+ /**
127
+ * Translate WSL mount paths (/mnt/X/...) to Windows drive-letter paths (X:/...)
128
+ * when running on Windows. No-op on other platforms.
129
+ */
130
+ export function translateWslMountPath(posixPath: string): string {
131
+ if (process.platform !== 'win32') {
132
+ return posixPath;
133
+ }
134
+ const match = /^\/mnt\/([a-zA-Z])(\/.*)?$/.exec(posixPath);
135
+ if (match) {
136
+ const drive = match[1].toUpperCase();
137
+ const rest = match[2] ?? '';
138
+ return `${drive}:${rest}`;
139
+ }
140
+ return posixPath;
141
+ }
142
+
143
+ // =============================================================================
144
+ // Validation
145
+ // =============================================================================
146
+
147
+ /**
148
+ * Validates if a directory name follows the Claude Code encoding pattern.
149
+ *
150
+ * @param encodedName - The directory name to validate
151
+ * @returns true if valid, false otherwise
152
+ */
153
+ export function isValidEncodedPath(encodedName: string): boolean {
154
+ if (!encodedName) {
155
+ return false;
156
+ }
157
+
158
+ // Support legacy Windows format: "C--Users-name-project"
159
+ // (no leading dash, drive separator encoded as "--").
160
+ if (/^[a-zA-Z]--[^\x00-\x1f/\\:*?"<>|]+$/u.test(encodedName)) {
161
+ return true;
162
+ }
163
+
164
+ // Must start with a dash (indicates absolute path)
165
+ if (!encodedName.startsWith('-')) {
166
+ return false;
167
+ }
168
+
169
+ // Encoded path is a single directory name. It may contain Unicode project
170
+ // names, but must not contain separators, control chars, or Windows-invalid chars.
171
+ // A single drive colon is allowed only in the old "-C:-Users-name" form.
172
+ if (/[\x00-\x1f/\\*?"<>|]/u.test(encodedName)) {
173
+ return false;
174
+ }
175
+
176
+ // Windows-style drive syntax is allowed only at the beginning after "-"
177
+ // e.g. "-C:-Users-name-project". Reject stray ":" elsewhere.
178
+ const firstColon = encodedName.indexOf(':');
179
+ if (firstColon === -1) {
180
+ return true;
181
+ }
182
+
183
+ if (!/^-[a-zA-Z]:/.test(encodedName)) {
184
+ return false;
185
+ }
186
+
187
+ return !encodedName.includes(':', firstColon + 1);
188
+ }
189
+
190
+ /**
191
+ * Validates a project ID that may be either a plain encoded path or
192
+ * a composite subproject ID (`{encodedPath}::{8-char-hex}`).
193
+ *
194
+ * @param projectId - The project ID to validate
195
+ * @returns true if valid
196
+ */
197
+ export function isValidProjectId(projectId: string): boolean {
198
+ if (!projectId) {
199
+ return false;
200
+ }
201
+
202
+ const sep = projectId.indexOf('::');
203
+ if (sep === -1) {
204
+ // Plain encoded path
205
+ return isValidEncodedPath(projectId);
206
+ }
207
+
208
+ // Composite ID: validate base part and hash suffix
209
+ const basePart = projectId.slice(0, sep);
210
+ const hashPart = projectId.slice(sep + 2);
211
+
212
+ return isValidEncodedPath(basePart) && /^[a-f0-9]{8}$/.test(hashPart);
213
+ }
214
+
215
+ /**
216
+ * Extract the base directory (encoded path) from a project ID.
217
+ * For composite IDs (`{encoded}::{hash}`), returns the encoded part.
218
+ * For plain IDs, returns the ID as-is.
219
+ */
220
+ export function extractBaseDir(projectId: string): string {
221
+ const sep = projectId.indexOf('::');
222
+ if (sep !== -1) {
223
+ return projectId.slice(0, sep);
224
+ }
225
+ return projectId;
226
+ }
227
+
228
+ function addUniqueCandidate(candidates: string[], candidate: string): void {
229
+ if (candidate && !candidates.includes(candidate)) {
230
+ candidates.push(candidate);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Returns possible ~/.claude/projects directory names for a project id.
236
+ * The first candidate is always the id's own base dir. Additional entries cover
237
+ * the orchestrator's portable codec, which lowercases Windows paths and folds
238
+ * underscores/non-ASCII characters to dashes.
239
+ */
240
+ export function getProjectDirNameCandidates(projectId: string): string[] {
241
+ const baseDir = extractBaseDir(projectId);
242
+ const candidates: string[] = [];
243
+ addUniqueCandidate(candidates, baseDir);
244
+
245
+ const decoded = decodePath(baseDir);
246
+ addUniqueCandidate(candidates, encodePath(decoded));
247
+ addUniqueCandidate(candidates, encodePathPortable(decoded));
248
+
249
+ if (path.isAbsolute(projectId)) {
250
+ addUniqueCandidate(candidates, encodePath(projectId));
251
+ addUniqueCandidate(candidates, encodePathPortable(projectId));
252
+ }
253
+
254
+ return candidates;
255
+ }
256
+
257
+ // =============================================================================
258
+ // Session ID Extraction
259
+ // =============================================================================
260
+
261
+ /**
262
+ * Extract session ID from a JSONL filename.
263
+ *
264
+ * @param filename - The filename (e.g., "abc123.jsonl")
265
+ * @returns The session ID (e.g., "abc123")
266
+ */
267
+ export function extractSessionId(filename: string): string {
268
+ return filename.replace(/\.jsonl$/, '');
269
+ }
270
+
271
+ // =============================================================================
272
+ // Path Construction
273
+ // =============================================================================
274
+
275
+ /**
276
+ * Construct the path to a session JSONL file.
277
+ * Handles composite project IDs by extracting the base directory.
278
+ */
279
+ export function buildSessionPath(basePath: string, projectId: string, sessionId: string): string {
280
+ return path.join(basePath, extractBaseDir(projectId), `${sessionId}.jsonl`);
281
+ }
282
+
283
+ /**
284
+ * Construct the path to a session's subagents directory.
285
+ * Handles composite project IDs by extracting the base directory.
286
+ */
287
+ export function buildSubagentsPath(basePath: string, projectId: string, sessionId: string): string {
288
+ return path.join(basePath, extractBaseDir(projectId), sessionId, 'subagents');
289
+ }
290
+
291
+ /**
292
+ * Construct the path to a task list file (stored in todos directory).
293
+ */
294
+ export function buildTodoPath(claudeBasePath: string, sessionId: string): string {
295
+ return path.join(claudeBasePath, 'todos', `${sessionId}.json`);
296
+ }
297
+
298
+ // =============================================================================
299
+ // Home Directory
300
+ // =============================================================================
301
+
302
+ /**
303
+ * Try Electron's app.getPath('home') which correctly handles Unicode paths
304
+ * on Windows (Cyrillic, CJK, etc.) unlike Node's os.homedir() / env vars
305
+ * that can suffer from UTF-8 vs system codepage mismatches.
306
+ *
307
+ * Returns null when Electron app is unavailable (e.g. in tests).
308
+ */
309
+ function getElectronHome(): string | null {
310
+ try {
311
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- Lazy require to avoid hard dependency on electron in test environments
312
+ const electron = require('electron') as {
313
+ app?: { getPath: (name: string) => string };
314
+ };
315
+ const app = electron.app;
316
+ if (app && typeof app.getPath === 'function') {
317
+ const home = app.getPath('home');
318
+ if (home) return home;
319
+ }
320
+ } catch {
321
+ // Not in Electron context (tests, standalone builds, etc.)
322
+ }
323
+ return null;
324
+ }
325
+
326
+ /**
327
+ * Get the user's home directory.
328
+ *
329
+ * Priority:
330
+ * 1. Electron app.getPath('home') -- correct Unicode handling on all platforms
331
+ * 2. HOME env var (Unix) / USERPROFILE (Windows)
332
+ * 3. HOMEDRIVE + HOMEPATH (Windows fallback)
333
+ * 4. os.homedir() (Node.js built-in)
334
+ */
335
+ export function getHomeDir(): string {
336
+ const electronHome = getElectronHome();
337
+ if (electronHome) return electronHome;
338
+
339
+ const windowsHome =
340
+ process.env.HOMEDRIVE && process.env.HOMEPATH
341
+ ? `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
342
+ : null;
343
+ return process.env.HOME || process.env.USERPROFILE || windowsHome || os.homedir() || '/';
344
+ }
345
+
346
+ let claudeBasePathOverride: string | null = null;
347
+
348
+ function getDefaultClaudeBasePath(): string {
349
+ return path.join(getHomeDir(), '.claude');
350
+ }
351
+
352
+ /**
353
+ * Get the auto-detected Claude config base path (~/.claude) without considering overrides.
354
+ */
355
+ export function getAutoDetectedClaudeBasePath(): string {
356
+ return getDefaultClaudeBasePath();
357
+ }
358
+
359
+ function normalizeOverridePath(claudeBasePath: string): string | null {
360
+ const trimmed = claudeBasePath.trim();
361
+ if (!trimmed) {
362
+ return null;
363
+ }
364
+
365
+ const normalized = path.normalize(trimmed);
366
+ if (!path.isAbsolute(normalized)) {
367
+ return null;
368
+ }
369
+
370
+ const resolved = path.resolve(normalized);
371
+ const root = path.parse(resolved).root;
372
+ if (resolved === root) {
373
+ return resolved;
374
+ }
375
+ let end = resolved.length;
376
+ while (end > root.length) {
377
+ const char = resolved[end - 1];
378
+ if (char !== '/' && char !== '\\') {
379
+ break;
380
+ }
381
+ end--;
382
+ }
383
+
384
+ return resolved.slice(0, end);
385
+ }
386
+
387
+ /**
388
+ * Override the Claude config base path (~/.claude).
389
+ * Pass null to return to auto-detection.
390
+ */
391
+ export function setClaudeBasePathOverride(claudeBasePath: string | null | undefined): void {
392
+ if (claudeBasePath == null) {
393
+ claudeBasePathOverride = null;
394
+ return;
395
+ }
396
+
397
+ claudeBasePathOverride = normalizeOverridePath(claudeBasePath);
398
+ }
399
+
400
+ /**
401
+ * Get the Claude config base path (~/.claude).
402
+ */
403
+ export function getClaudeBasePath(): string {
404
+ return claudeBasePathOverride ?? getDefaultClaudeBasePath();
405
+ }
406
+
407
+ /**
408
+ * Get the projects directory path (~/.claude/projects).
409
+ */
410
+ export function getProjectsBasePath(): string {
411
+ return path.join(getClaudeBasePath(), 'projects');
412
+ }
413
+
414
+ /**
415
+ * Get the todos directory path (~/.claude/todos).
416
+ */
417
+ export function getTodosBasePath(): string {
418
+ return path.join(getClaudeBasePath(), 'todos');
419
+ }
420
+
421
+ /**
422
+ * Get the teams directory path (~/.claude/teams).
423
+ */
424
+ export function getTeamsBasePath(): string {
425
+ return path.join(getClaudeBasePath(), 'teams');
426
+ }
427
+
428
+ /**
429
+ * Get the tasks directory path (~/.claude/tasks).
430
+ */
431
+ export function getTasksBasePath(): string {
432
+ return path.join(getClaudeBasePath(), 'tasks');
433
+ }
434
+
435
+ /**
436
+ * Get the tools directory path (~/.claude/tools).
437
+ */
438
+ export function getToolsBasePath(): string {
439
+ return path.join(getClaudeBasePath(), 'tools');
440
+ }
441
+
442
+ type CopyDirectoryForMigration = (sourcePath: string, targetPath: string) => void;
443
+
444
+ function copyDirectoryForMigrationSync(sourcePath: string, targetPath: string): void {
445
+ fs.cpSync(sourcePath, targetPath, {
446
+ recursive: true,
447
+ errorOnExist: false,
448
+ force: false,
449
+ });
450
+ }
451
+
452
+ let copyDirectoryForMigration: CopyDirectoryForMigration = copyDirectoryForMigrationSync;
453
+
454
+ export function __setPathDecoderCopyDirectoryForTests(
455
+ copyDirectory: CopyDirectoryForMigration | null
456
+ ): void {
457
+ copyDirectoryForMigration = copyDirectory ?? copyDirectoryForMigrationSync;
458
+ }
459
+
460
+ /**
461
+ * Get the schedules directory path (~/.claude/agent-teams-schedules).
462
+ */
463
+ export function getSchedulesBasePath(): string {
464
+ const basePath = getClaudeBasePath();
465
+ return migrateLegacyDirectoryPath(
466
+ path.join(basePath, 'agent-teams-schedules'),
467
+ path.join(basePath, 'claude-devtools-schedules')
468
+ );
469
+ }
470
+
471
+ function migrateLegacyDirectoryPath(currentPath: string, legacyPath: string): string {
472
+ if (!directoryExists(legacyPath)) {
473
+ return currentPath;
474
+ }
475
+
476
+ if (directoryExists(currentPath)) {
477
+ if (directoryHasEntries(currentPath)) {
478
+ return currentPath;
479
+ }
480
+ return copyLegacyDirectoryPath(currentPath, legacyPath);
481
+ }
482
+
483
+ if (pathExists(currentPath)) {
484
+ return currentPath;
485
+ }
486
+
487
+ return copyLegacyDirectoryPath(currentPath, legacyPath);
488
+ }
489
+
490
+ function copyLegacyDirectoryPath(currentPath: string, legacyPath: string): string {
491
+ const tempPath = `${currentPath}.migrating-${process.pid}`;
492
+
493
+ try {
494
+ fs.mkdirSync(path.dirname(currentPath), { recursive: true });
495
+ if (pathExists(tempPath)) {
496
+ fs.rmSync(tempPath, { recursive: true, force: true });
497
+ }
498
+
499
+ copyDirectoryForMigration(legacyPath, tempPath);
500
+
501
+ if (directoryExists(currentPath) && !directoryHasEntries(currentPath)) {
502
+ fs.rmdirSync(currentPath);
503
+ }
504
+
505
+ fs.renameSync(tempPath, currentPath);
506
+ return currentPath;
507
+ } catch {
508
+ try {
509
+ if (pathExists(tempPath)) {
510
+ fs.rmSync(tempPath, { recursive: true, force: true });
511
+ }
512
+ } catch {
513
+ // Best effort cleanup only.
514
+ }
515
+
516
+ return directoryExists(currentPath) && directoryHasEntries(currentPath)
517
+ ? currentPath
518
+ : legacyPath;
519
+ }
520
+ }
521
+
522
+ export function getTaskChangeSummariesBasePath(): string {
523
+ return path.join(getClaudeBasePath(), 'task-change-summaries');
524
+ }
525
+
526
+ export function getTaskChangePresenceBasePath(): string {
527
+ return path.join(getClaudeBasePath(), 'task-change-presence');
528
+ }
529
+
530
+ /**
531
+ * Get the backups directory path for the app's own storage.
532
+ */
533
+ export function getBackupsBasePath(): string {
534
+ return path.join(getAppDataBasePath(), 'backups');
535
+ }
536
+
537
+ /**
538
+ * Get the app's own data directory (attachments, task-attachments).
539
+ * Separate from ~/.claude/ so CLI cannot delete our data.
540
+ */
541
+ export function getAppDataPath(): string {
542
+ return path.join(getAppDataBasePath(), 'data');
543
+ }
544
+
545
+ // -- App data root (Electron userData) --
546
+
547
+ const APP_DATA_FALLBACK_DIR_NAME = '.agent-teams-ai';
548
+ const LEGACY_APP_DATA_FALLBACK_DIR_NAME = '.claude-agent-teams-ui';
549
+
550
+ let appDataBasePathOverride: string | null = null;
551
+
552
+ export function setAppDataBasePath(p: string | null | undefined): void {
553
+ appDataBasePathOverride = p ?? null;
554
+ }
555
+
556
+ function getAppDataBasePath(): string {
557
+ if (appDataBasePathOverride) return appDataBasePathOverride;
558
+ // Fallback: resolve lazily from Electron app (safe after app.whenReady)
559
+ try {
560
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
561
+ const { app } = require('electron') as { app: { getPath: (name: string) => string } };
562
+ return app.getPath('userData');
563
+ } catch {
564
+ // Outside Electron (tests, CLI): use the new fallback path and migrate legacy data once.
565
+ return getFallbackAppDataBasePath();
566
+ }
567
+ }
568
+
569
+ function getFallbackAppDataBasePath(): string {
570
+ const home = getHomeDir();
571
+ const currentPath = path.join(home, APP_DATA_FALLBACK_DIR_NAME);
572
+ const legacyPath = path.join(home, LEGACY_APP_DATA_FALLBACK_DIR_NAME);
573
+
574
+ if (!directoryExists(legacyPath)) {
575
+ return currentPath;
576
+ }
577
+
578
+ if (directoryExists(currentPath)) {
579
+ if (directoryHasEntries(currentPath)) {
580
+ return currentPath;
581
+ }
582
+ return migrateFallbackAppDataBasePath(currentPath, legacyPath);
583
+ }
584
+
585
+ if (pathExists(currentPath)) {
586
+ return currentPath;
587
+ }
588
+
589
+ return migrateFallbackAppDataBasePath(currentPath, legacyPath);
590
+ }
591
+
592
+ function migrateFallbackAppDataBasePath(currentPath: string, legacyPath: string): string {
593
+ const tempPath = `${currentPath}.migrating-${process.pid}`;
594
+
595
+ try {
596
+ if (pathExists(tempPath)) {
597
+ fs.rmSync(tempPath, { recursive: true, force: true });
598
+ }
599
+
600
+ copyDirectoryForMigration(legacyPath, tempPath);
601
+
602
+ if (directoryExists(currentPath) && !directoryHasEntries(currentPath)) {
603
+ fs.rmdirSync(currentPath);
604
+ }
605
+
606
+ fs.renameSync(tempPath, currentPath);
607
+ return currentPath;
608
+ } catch {
609
+ try {
610
+ if (pathExists(tempPath)) {
611
+ fs.rmSync(tempPath, { recursive: true, force: true });
612
+ }
613
+ } catch {
614
+ // Best effort cleanup only.
615
+ }
616
+
617
+ return directoryExists(currentPath) && directoryHasEntries(currentPath)
618
+ ? currentPath
619
+ : legacyPath;
620
+ }
621
+ }
622
+
623
+ function pathExists(targetPath: string): boolean {
624
+ try {
625
+ fs.accessSync(targetPath);
626
+ return true;
627
+ } catch {
628
+ return false;
629
+ }
630
+ }
631
+
632
+ function directoryExists(targetPath: string): boolean {
633
+ try {
634
+ return fs.statSync(targetPath).isDirectory();
635
+ } catch {
636
+ return false;
637
+ }
638
+ }
639
+
640
+ function directoryHasEntries(targetPath: string): boolean {
641
+ try {
642
+ return fs.readdirSync(targetPath).length > 0;
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+
648
+ /**
649
+ * Directory for per-team MCP config JSON files.
650
+ * Stored in app's userData so they persist across sessions and are
651
+ * accessible by Claude CLI subprocess on all platforms (including AppImage).
652
+ */
653
+ export function getMcpConfigsBasePath(): string {
654
+ return path.join(getAppDataBasePath(), 'mcp-configs');
655
+ }
656
+
657
+ /**
658
+ * Directory for the stable MCP server bundle copy (packaged builds).
659
+ * Versioned subdirectories contain the copied index.js + package.json
660
+ * so the server runs from a writable, non-FUSE location.
661
+ */
662
+ export function getMcpServerBasePath(): string {
663
+ return path.join(getAppDataBasePath(), 'mcp-server');
664
+ }