@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0

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 (200) hide show
  1. package/.oxlintrc.json +8 -0
  2. package/dist/index.mjs +12603 -0
  3. package/package.json +7 -39
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +9 -0
  6. package/src/__e2e__/README.md +196 -0
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
  10. package/src/__e2e__/conversation.e2e.test.tsx +56 -0
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
  12. package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
  13. package/src/__e2e__/helpers/test-helpers.ts +450 -0
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
  15. package/src/__e2e__/llm-models.e2e.test.ts +402 -0
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
  19. package/src/__e2e__/repl.e2e.test.tsx +78 -0
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
  23. package/src/args.ts +22 -0
  24. package/src/components/__tests__/react-compiler.test.tsx +78 -0
  25. package/src/components/__tests__/status-indicator.test.tsx +403 -0
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
  27. package/src/components/composer/agent-mode-indicator.tsx +63 -0
  28. package/src/components/composer/bash-runner.tsx +54 -0
  29. package/src/components/composer/commands/default-commands.tsx +615 -0
  30. package/src/components/composer/commands/handler.tsx +59 -0
  31. package/src/components/composer/commands/picker.tsx +273 -0
  32. package/src/components/composer/commands/registry.ts +233 -0
  33. package/src/components/composer/commands/types.ts +33 -0
  34. package/src/components/composer/context.tsx +88 -0
  35. package/src/components/composer/file-mention-picker.tsx +83 -0
  36. package/src/components/composer/help.tsx +44 -0
  37. package/src/components/composer/index.tsx +1006 -0
  38. package/src/components/composer/mentions.ts +57 -0
  39. package/src/components/composer/message-queue.tsx +70 -0
  40. package/src/components/composer/mode-panel.tsx +35 -0
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
  43. package/src/components/composer/modes/bash-handler.tsx +132 -0
  44. package/src/components/composer/modes/bash-renderer.tsx +175 -0
  45. package/src/components/composer/modes/default-handlers.tsx +33 -0
  46. package/src/components/composer/modes/index.ts +41 -0
  47. package/src/components/composer/modes/types.ts +21 -0
  48. package/src/components/composer/persistent-shell.ts +283 -0
  49. package/src/components/composer/process.ts +65 -0
  50. package/src/components/composer/types.ts +9 -0
  51. package/src/components/composer/use-mention-search.ts +68 -0
  52. package/src/components/error-boundry.tsx +60 -0
  53. package/src/components/exit-message.tsx +29 -0
  54. package/src/components/expanded-view.tsx +74 -0
  55. package/src/components/file-completion.tsx +127 -0
  56. package/src/components/header.tsx +47 -0
  57. package/src/components/logo.tsx +37 -0
  58. package/src/components/segments.tsx +356 -0
  59. package/src/components/status-indicator.tsx +306 -0
  60. package/src/components/tool-group-summary.tsx +263 -0
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
  62. package/src/components/tool-permissions/diff-preview.tsx +355 -0
  63. package/src/components/tool-permissions/index.ts +5 -0
  64. package/src/components/tool-permissions/permission-options.tsx +375 -0
  65. package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
  67. package/src/components/tools/agent/ask-user-question.tsx +101 -0
  68. package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
  69. package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
  70. package/src/components/tools/agent/handoff-to-main.tsx +27 -0
  71. package/src/components/tools/agent/subagent.tsx +37 -0
  72. package/src/components/tools/agent/todo-write.tsx +104 -0
  73. package/src/components/tools/browser/close-tab.tsx +58 -0
  74. package/src/components/tools/browser/computer.tsx +70 -0
  75. package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
  76. package/src/components/tools/browser/get-tab-content.tsx +51 -0
  77. package/src/components/tools/browser/navigate-to.tsx +59 -0
  78. package/src/components/tools/browser/new-tab.tsx +60 -0
  79. package/src/components/tools/browser/perform-action.tsx +63 -0
  80. package/src/components/tools/browser/refresh-tab.tsx +43 -0
  81. package/src/components/tools/browser/switch-tab.tsx +58 -0
  82. package/src/components/tools/filesystem/delete-file.tsx +104 -0
  83. package/src/components/tools/filesystem/edit.tsx +220 -0
  84. package/src/components/tools/filesystem/list-dir.tsx +78 -0
  85. package/src/components/tools/filesystem/read-file.tsx +180 -0
  86. package/src/components/tools/filesystem/upload-image.tsx +76 -0
  87. package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
  88. package/src/components/tools/index.ts +91 -0
  89. package/src/components/tools/mcp/mcp-tool.tsx +158 -0
  90. package/src/components/tools/search/fetch-url.tsx +73 -0
  91. package/src/components/tools/search/file-search.tsx +78 -0
  92. package/src/components/tools/search/grep.tsx +90 -0
  93. package/src/components/tools/search/semantic-search.tsx +66 -0
  94. package/src/components/tools/search/web-search.tsx +71 -0
  95. package/src/components/tools/shared/index.tsx +48 -0
  96. package/src/components/tools/shared/zod-coercion.ts +35 -0
  97. package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
  98. package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
  99. package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
  100. package/src/components/tools/types.ts +16 -0
  101. package/src/components/tools.tsx +66 -0
  102. package/src/components/ui/__tests__/divider.test.tsx +61 -0
  103. package/src/components/ui/__tests__/gradient.test.tsx +125 -0
  104. package/src/components/ui/__tests__/input.test.tsx +166 -0
  105. package/src/components/ui/__tests__/select.test.tsx +273 -0
  106. package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
  107. package/src/components/ui/blinking-indicator.tsx +25 -0
  108. package/src/components/ui/divider.tsx +162 -0
  109. package/src/components/ui/gradient.tsx +56 -0
  110. package/src/components/ui/input.tsx +228 -0
  111. package/src/components/ui/select.tsx +151 -0
  112. package/src/components/ui/shimmer.tsx +84 -0
  113. package/src/context/agent-mode.tsx +95 -0
  114. package/src/context/extension-file.tsx +136 -0
  115. package/src/context/network-activity.tsx +45 -0
  116. package/src/context/notification.tsx +62 -0
  117. package/src/context/shell-size.tsx +49 -0
  118. package/src/context/shell-title.tsx +38 -0
  119. package/src/entrypoints/print-mode.ts +312 -0
  120. package/src/entrypoints/repl.tsx +401 -0
  121. package/src/hooks/use-agent.ts +15 -0
  122. package/src/hooks/use-api-client.ts +1 -0
  123. package/src/hooks/use-available-height.ts +8 -0
  124. package/src/hooks/use-cleanup.ts +29 -0
  125. package/src/hooks/use-interrupt-manager.ts +242 -0
  126. package/src/hooks/use-models.ts +22 -0
  127. package/src/index.ts +217 -0
  128. package/src/lib/__tests__/ansi.test.ts +255 -0
  129. package/src/lib/__tests__/cli.test.ts +122 -0
  130. package/src/lib/__tests__/commands.test.ts +325 -0
  131. package/src/lib/__tests__/constants.test.ts +15 -0
  132. package/src/lib/__tests__/focusables.test.ts +25 -0
  133. package/src/lib/__tests__/fs.test.ts +231 -0
  134. package/src/lib/__tests__/markdown.test.tsx +348 -0
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
  136. package/src/lib/__tests__/mcpManagement.test.ts +38 -0
  137. package/src/lib/__tests__/path-paste.test.ts +144 -0
  138. package/src/lib/__tests__/path.test.ts +300 -0
  139. package/src/lib/__tests__/queries.test.ts +39 -0
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
  141. package/src/lib/__tests__/text-buffer.test.ts +328 -0
  142. package/src/lib/__tests__/text-utils.test.ts +32 -0
  143. package/src/lib/__tests__/timing.test.ts +78 -0
  144. package/src/lib/__tests__/utils.test.ts +238 -0
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
  146. package/src/lib/ansi.ts +150 -0
  147. package/src/lib/cli-push-server.ts +112 -0
  148. package/src/lib/cli.ts +44 -0
  149. package/src/lib/clipboard.ts +226 -0
  150. package/src/lib/command-utils.ts +93 -0
  151. package/src/lib/commands.ts +270 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/extension-connection.ts +181 -0
  154. package/src/lib/focusables.ts +7 -0
  155. package/src/lib/fs.ts +533 -0
  156. package/src/lib/markdown/code-block.tsx +63 -0
  157. package/src/lib/markdown/index.ts +4 -0
  158. package/src/lib/markdown/link.tsx +19 -0
  159. package/src/lib/markdown/markdown.tsx +372 -0
  160. package/src/lib/markdown/types.ts +15 -0
  161. package/src/lib/mcpCommandHandler.ts +121 -0
  162. package/src/lib/mcpManagement.ts +44 -0
  163. package/src/lib/path-paste.ts +185 -0
  164. package/src/lib/path.ts +179 -0
  165. package/src/lib/queries.ts +15 -0
  166. package/src/lib/standaloneMcpService.ts +688 -0
  167. package/src/lib/status-utils.ts +237 -0
  168. package/src/lib/test-utils.tsx +72 -0
  169. package/src/lib/text-buffer.ts +2415 -0
  170. package/src/lib/text-utils.ts +272 -0
  171. package/src/lib/timing.ts +63 -0
  172. package/src/lib/types.ts +295 -0
  173. package/src/lib/utils.ts +182 -0
  174. package/src/lib/vim-buffer-actions.ts +732 -0
  175. package/src/providers/agent.tsx +1075 -0
  176. package/src/providers/api-client.tsx +43 -0
  177. package/src/services/logger.ts +85 -0
  178. package/src/terminal/detection.ts +187 -0
  179. package/src/terminal/exit.ts +279 -0
  180. package/src/terminal/notification.ts +83 -0
  181. package/src/terminal/progress.ts +201 -0
  182. package/src/terminal/setup.ts +797 -0
  183. package/src/terminal/suspend.ts +58 -0
  184. package/src/terminal/types.ts +51 -0
  185. package/src/theme/context.tsx +57 -0
  186. package/src/theme/index.ts +4 -0
  187. package/src/theme/themed.tsx +35 -0
  188. package/src/theme/themes.json +546 -0
  189. package/src/theme/types.ts +110 -0
  190. package/src/tools/types.ts +59 -0
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
  192. package/src/tools/utils/tool-ui-components.tsx +631 -0
  193. package/src/tools/utils/zod-coercion.ts +35 -0
  194. package/tsconfig.json +11 -0
  195. package/tsconfig.node.json +29 -0
  196. package/tsconfig.test.json +27 -0
  197. package/tsdown.config.ts +17 -0
  198. package/vitest.config.ts +76 -0
  199. package/README.md +0 -28
  200. package/dist/index.js +0 -26
@@ -0,0 +1,688 @@
1
+ import { product } from "@codellm/product";
2
+ import fs, { promises as fsPromises } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import type { IMcpServerInfo, IMcpManagementService, IMcpServerConfig } from "./types.js";
7
+
8
+ import { helpText } from "./mcpCommandHandler.js";
9
+
10
+ interface McpConfigFile {
11
+ inputs?: unknown[];
12
+ servers?: Record<string, IMcpServerConfig>;
13
+ }
14
+
15
+ interface CursorGlobalConfig {
16
+ mcpServers?: Record<
17
+ string,
18
+ {
19
+ command?: string;
20
+ args?: string[];
21
+ env?: Record<string, string>;
22
+ url?: string;
23
+ }
24
+ >;
25
+ }
26
+
27
+ interface VSCodeSettings {
28
+ mcp?: McpConfigFile;
29
+ [key: string]: any;
30
+ }
31
+
32
+ interface ConfigSource {
33
+ path: string;
34
+ type:
35
+ | "cursor-global"
36
+ | "cursor-workspace"
37
+ | "vscode-user"
38
+ | "vscode-workspace"
39
+ | "claude-desktop"
40
+ | "windsurf"
41
+ | "abacusai-user";
42
+ label: string;
43
+ exists: boolean;
44
+ }
45
+
46
+ /**
47
+ * Standalone MCP service that reads configurations directly from files
48
+ * without requiring any workbench dependencies.
49
+ */
50
+ export class StandaloneMcpService implements IMcpManagementService {
51
+ constructor() {}
52
+
53
+ async listServers(): Promise<IMcpServerInfo[]> {
54
+ const configSources = await this.findConfigSources();
55
+ const servers: IMcpServerInfo[] = [];
56
+
57
+ for (const source of configSources) {
58
+ if (!source.exists) {
59
+ continue;
60
+ }
61
+
62
+ try {
63
+ const sourceServers = await this.loadServersFromSource(source);
64
+ servers.push(...sourceServers);
65
+ } catch {
66
+ // Failed to load from this source, skip
67
+ }
68
+ }
69
+
70
+ return servers;
71
+ }
72
+
73
+ async addServer(name: string, config: IMcpServerConfig): Promise<void> {
74
+ // Validate the server configuration before saving
75
+ this.validateServerConfig(config);
76
+ // Prefer product-specific User settings over other formats
77
+ const configSources = await this.findConfigSources();
78
+
79
+ // Priority order: Product User > VSCode User > existing files > Cursor global
80
+ const abacusaiUser = configSources.find((s) => s.type === "abacusai-user" && s.exists);
81
+ const vscodeUser = configSources.find((s) => s.type === "vscode-user" && s.exists);
82
+ const existingFiles = configSources.filter((s) => s.exists);
83
+ const cursorGlobal = configSources.find((s) => s.type === "cursor-global");
84
+ const vscodeUserNonExisting = configSources.find((s) => s.type === "vscode-user");
85
+
86
+ // Try existing files first, then fall back to creating new ones
87
+ const targetSource =
88
+ abacusaiUser || vscodeUser || existingFiles[0] || vscodeUserNonExisting || cursorGlobal;
89
+ if (!targetSource) {
90
+ throw new Error("No suitable configuration location found.");
91
+ }
92
+
93
+ await this.addServerToSource(targetSource, name, config);
94
+ }
95
+
96
+ async removeServer(name: string): Promise<void> {
97
+ const configSources = await this.findConfigSources();
98
+ let removed = false;
99
+
100
+ for (const source of configSources) {
101
+ if (!source.exists) {
102
+ continue;
103
+ }
104
+
105
+ try {
106
+ const wasRemoved = await this.removeServerFromSource(source.path, name);
107
+ if (wasRemoved) {
108
+ removed = true;
109
+ }
110
+ } catch {
111
+ // Failed to remove from this source, continue
112
+ }
113
+ }
114
+
115
+ if (!removed) {
116
+ throw new Error(`Server '${name}' not found in any configuration file`);
117
+ }
118
+ }
119
+
120
+ async getServer(name: string): Promise<IMcpServerInfo | undefined> {
121
+ const servers = await this.listServers();
122
+ return servers.find((server) => server.name === name || server.id === name);
123
+ }
124
+
125
+ async addServerFromJson(name: string, jsonConfig: string): Promise<void> {
126
+ let config: IMcpServerConfig;
127
+
128
+ try {
129
+ config = this.parseJsonWithFallback(jsonConfig) as IMcpServerConfig;
130
+ } catch (error) {
131
+ throw new Error(
132
+ `Invalid JSON syntax: ${error instanceof Error ? error.message : String(error)}`,
133
+ );
134
+ }
135
+
136
+ try {
137
+ this.validateServerConfig(config);
138
+ } catch (error) {
139
+ throw new Error(
140
+ `Invalid server configuration: ${error instanceof Error ? error.message : String(error)}`,
141
+ );
142
+ }
143
+
144
+ await this.addServer(name, config);
145
+ }
146
+
147
+ async addServerFromUrl(name: string, url: string, args?: string[]): Promise<void> {
148
+ let config: IMcpServerConfig;
149
+
150
+ // Check if it's an HTTP URL
151
+ if (url.startsWith("http://") || url.startsWith("https://")) {
152
+ config = {
153
+ url,
154
+ };
155
+ } else {
156
+ // Treat as command
157
+ config = {
158
+ command: url,
159
+ args: args || [],
160
+ };
161
+ }
162
+
163
+ await this.addServer(name, config);
164
+ }
165
+
166
+ formatServerInfo(server: IMcpServerInfo): string {
167
+ const lines = [
168
+ `Name: ${server.name}`,
169
+ `ID: ${server.id}`,
170
+ `Source: ${server.description || "Unknown"}`,
171
+ `Status: ${server.status || "unknown"}`,
172
+ ];
173
+
174
+ if ("url" in server.config) {
175
+ lines.push(`Type: HTTP`);
176
+ lines.push(`URL: ${server.config.url}`);
177
+ if (server.config.headers && Object.keys(server.config.headers).length > 0) {
178
+ lines.push(`Headers: ${JSON.stringify(server.config.headers, null, 2)}`);
179
+ }
180
+ } else if ("command" in server.config) {
181
+ lines.push(`Type: Command`);
182
+ lines.push(`Command: ${server.config.command}`);
183
+ if (server.config.args && server.config.args.length > 0) {
184
+ lines.push(`Arguments: ${server.config.args.join(" ")}`);
185
+ }
186
+ if (server.config.env && Object.keys(server.config.env).length > 0) {
187
+ lines.push(`Environment: ${JSON.stringify(server.config.env, null, 2)}`);
188
+ }
189
+ if (server.config.cwd) {
190
+ lines.push(`Working Directory: ${server.config.cwd}`);
191
+ }
192
+ }
193
+
194
+ return lines.join("\n");
195
+ }
196
+
197
+ formatServerList(servers: IMcpServerInfo[]): string {
198
+ const lines: string[] = [];
199
+
200
+ if (servers.length === 0) {
201
+ lines.push("No MCP servers found.");
202
+ lines.push("");
203
+ lines.push("Use the following commands to manage MCP servers:");
204
+ lines.push("");
205
+ // Add the MCP help content
206
+ lines.push(helpText);
207
+ } else {
208
+ lines.push("📡 MCP Servers:");
209
+ lines.push("");
210
+
211
+ // Group servers by source
212
+ const serversBySource: Record<string, IMcpServerInfo[]> = {};
213
+ for (const server of servers) {
214
+ const source = this.getServerSourceLabel(server);
215
+ if (!serversBySource[source]) {
216
+ serversBySource[source] = [];
217
+ }
218
+ serversBySource[source].push(server);
219
+ }
220
+
221
+ for (const [source, sourceServers] of Object.entries(serversBySource)) {
222
+ lines.push(` ${source}:`);
223
+ for (const server of sourceServers) {
224
+ const typeLabel = "url" in server.config ? "🌐" : "💻";
225
+ lines.push(` ${typeLabel} ${server.name}`);
226
+ }
227
+ lines.push("");
228
+ }
229
+
230
+ lines.push("📝 Example - Add GitHub MCP Server:");
231
+ lines.push(
232
+ ' /mcp add-json github \'{"command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"your_token"}}\'',
233
+ );
234
+ lines.push("");
235
+ }
236
+
237
+ return lines.join("\n");
238
+ }
239
+
240
+ async findConfigSources(): Promise<ConfigSource[]> {
241
+ const sources: ConfigSource[] = [];
242
+
243
+ // Claude Desktop
244
+ const claudePath =
245
+ process.platform === "win32"
246
+ ? path.join(os.homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json")
247
+ : process.platform === "darwin"
248
+ ? path.join(
249
+ os.homedir(),
250
+ "Library",
251
+ "Application Support",
252
+ "Claude",
253
+ "claude_desktop_config.json",
254
+ )
255
+ : path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
256
+ sources.push({
257
+ path: claudePath,
258
+ type: "claude-desktop",
259
+ label: "Claude Desktop",
260
+ exists: fs.existsSync(claudePath),
261
+ });
262
+
263
+ // Windsurf
264
+ const windsurfPath = path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
265
+ sources.push({
266
+ path: windsurfPath,
267
+ type: "windsurf",
268
+ label: "Windsurf",
269
+ exists: fs.existsSync(windsurfPath),
270
+ });
271
+
272
+ // Cursor Global: ~/.cursor/mcp.json
273
+ const cursorGlobal = path.join(os.homedir(), ".cursor", "mcp.json");
274
+ sources.push({
275
+ path: cursorGlobal,
276
+ type: "cursor-global",
277
+ label: "Cursor (Global)",
278
+ exists: fs.existsSync(cursorGlobal),
279
+ });
280
+
281
+ // Cursor Workspace: .cursor/mcp.json
282
+ const cursorWorkspace = path.join(process.cwd(), ".cursor", "mcp.json");
283
+ sources.push({
284
+ path: cursorWorkspace,
285
+ type: "cursor-workspace",
286
+ label: "Cursor (Workspace)",
287
+ exists: fs.existsSync(cursorWorkspace),
288
+ });
289
+
290
+ const possibleProductNames = [product.fullName, "AbacusAI", "CodeLLM"];
291
+ for (const productName of possibleProductNames) {
292
+ const userDataDir = this.getUserDataDir(productName);
293
+ if (userDataDir) {
294
+ const mcpJsonPath = path.join(userDataDir, "User", "mcp.json");
295
+ if (fs.existsSync(mcpJsonPath)) {
296
+ sources.push({
297
+ path: mcpJsonPath,
298
+ type: "abacusai-user",
299
+ label: `${productName} (User)`,
300
+ exists: true,
301
+ });
302
+ }
303
+ }
304
+ }
305
+
306
+ // Standard VSCode User Settings (fallback)
307
+ const vscodeUserDir =
308
+ process.platform === "win32"
309
+ ? path.join(os.homedir(), "AppData", "Roaming", "Code", "User")
310
+ : process.platform === "darwin"
311
+ ? path.join(os.homedir(), "Library", "Application Support", "Code", "User")
312
+ : path.join(os.homedir(), ".config", "Code", "User");
313
+ const vscodeMcpJson = path.join(vscodeUserDir, "mcp.json");
314
+ sources.push({
315
+ path: vscodeMcpJson,
316
+ type: "vscode-user",
317
+ label: "VSCode (User)",
318
+ exists: fs.existsSync(vscodeMcpJson),
319
+ });
320
+
321
+ return sources;
322
+ }
323
+
324
+ private getUserDataDir(productName: string): string | null {
325
+ try {
326
+ switch (process.platform) {
327
+ case "win32": {
328
+ const appData = process.env["APPDATA"];
329
+ if (!appData) {
330
+ return null;
331
+ }
332
+ return path.join(appData, productName);
333
+ }
334
+
335
+ case "darwin": {
336
+ return path.join(os.homedir(), "Library", "Application Support", productName);
337
+ }
338
+
339
+ case "linux": {
340
+ const configHome = process.env["XDG_CONFIG_HOME"] || path.join(os.homedir(), ".config");
341
+ return path.join(configHome, productName);
342
+ }
343
+
344
+ default:
345
+ return null;
346
+ }
347
+ } catch {
348
+ return null;
349
+ }
350
+ }
351
+
352
+ private async loadServersFromSource(source: ConfigSource): Promise<IMcpServerInfo[]> {
353
+ const content = await fsPromises.readFile(source.path, "utf8");
354
+ const servers: IMcpServerInfo[] = [];
355
+
356
+ // Parse JSON with better error handling and trailing comma support
357
+ let parsedContent: any;
358
+ try {
359
+ parsedContent = this.parseJsonWithFallback(content);
360
+ } catch (error) {
361
+ throw new Error(
362
+ `JSON parsing failed: ${error instanceof Error ? error.message : String(error)}`,
363
+ );
364
+ }
365
+
366
+ if (source.type.startsWith("vscode") || source.type === "abacusai-user") {
367
+ let serversConfig: Record<string, IMcpServerConfig> | undefined;
368
+
369
+ if (parsedContent.servers && !parsedContent.mcp) {
370
+ serversConfig = parsedContent.servers;
371
+ } else if (parsedContent.mcp?.servers) {
372
+ serversConfig = parsedContent.mcp.servers;
373
+ }
374
+
375
+ if (serversConfig) {
376
+ for (const [name, serverConfig] of Object.entries(serversConfig)) {
377
+ servers.push({
378
+ id: `${source.type}.${name}`,
379
+ name,
380
+ config: serverConfig,
381
+ status: "stopped",
382
+ description: `${source.label} - ${this.getServerTypeDescription(serverConfig)}`,
383
+ });
384
+ }
385
+ }
386
+ } else if (
387
+ ["cursor-global", "cursor-workspace", "claude-desktop", "windsurf"].includes(source.type)
388
+ ) {
389
+ const cursorConfig: CursorGlobalConfig = parsedContent;
390
+
391
+ if (cursorConfig.mcpServers) {
392
+ for (const [name, serverConfig] of Object.entries(cursorConfig.mcpServers)) {
393
+ // Convert format to our internal format
394
+ const config: IMcpServerConfig = serverConfig.url
395
+ ? {
396
+ type: "http",
397
+ url: serverConfig.url,
398
+ headers: {},
399
+ }
400
+ : {
401
+ type: "stdio",
402
+ command: serverConfig.command || "",
403
+ args: serverConfig.args || [],
404
+ env: serverConfig.env || {},
405
+ };
406
+
407
+ servers.push({
408
+ id: `${source.type}.${name}`,
409
+ name,
410
+ config,
411
+ status: "stopped",
412
+ description: `${source.label} - ${this.getServerTypeDescription(config)}`,
413
+ });
414
+ }
415
+ }
416
+ }
417
+
418
+ return servers;
419
+ }
420
+
421
+ private parseJsonWithFallback(content: string): any {
422
+ // First try standard JSON parsing
423
+ try {
424
+ return JSON.parse(content);
425
+ } catch (error) {
426
+ // If that fails, try to fix common JSON issues like trailing commas
427
+ try {
428
+ // Remove trailing commas before closing braces/brackets
429
+ const fixedContent = content
430
+ .replace(/,(\s*[}\]])/g, "$1") // Remove trailing commas
431
+ .replace(/([}\]]),(\s*[}\]])/g, "$1$2"); // Remove commas before closing braces
432
+
433
+ return JSON.parse(fixedContent);
434
+ } catch (fallbackError) {
435
+ // If still failing, provide more detailed error info
436
+ const originalError = error instanceof Error ? error.message : String(error);
437
+ const fallbackErrorMsg =
438
+ fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
439
+ throw new Error(
440
+ `JSON parsing failed. Original error: ${originalError}. After fixing trailing commas: ${fallbackErrorMsg}`,
441
+ );
442
+ }
443
+ }
444
+ }
445
+
446
+ private async addServerToSource(
447
+ source: ConfigSource,
448
+ name: string,
449
+ config: IMcpServerConfig,
450
+ ): Promise<void> {
451
+ const configPath = source.path;
452
+ // Ensure directory exists
453
+ const dir = path.dirname(configPath);
454
+ await fsPromises.mkdir(dir, { recursive: true });
455
+
456
+ // Determine config type based on source type
457
+ const isVSCodeLikeSettings = ["vscode-user", "vscode-workspace", "abacusai-user"].includes(
458
+ source.type,
459
+ );
460
+
461
+ if (isVSCodeLikeSettings) {
462
+ if (configPath.endsWith("mcp.json")) {
463
+ let mcpConfig: McpConfigFile = { servers: {} };
464
+ if (fs.existsSync(configPath)) {
465
+ try {
466
+ const content = await fsPromises.readFile(configPath, "utf8");
467
+ mcpConfig = this.parseJsonWithFallback(content);
468
+ } catch {}
469
+ }
470
+
471
+ mcpConfig.servers = mcpConfig.servers || {};
472
+ mcpConfig.servers[name] = config;
473
+
474
+ const newContent = JSON.stringify(mcpConfig, null, 2);
475
+ await fsPromises.writeFile(configPath, newContent, "utf8");
476
+ } else {
477
+ let vscodeSettings: VSCodeSettings = {};
478
+ if (fs.existsSync(configPath)) {
479
+ try {
480
+ const content = await fsPromises.readFile(configPath, "utf8");
481
+ vscodeSettings = this.parseJsonWithFallback(content);
482
+ } catch {}
483
+ }
484
+
485
+ vscodeSettings.mcp = vscodeSettings.mcp || { servers: {} };
486
+ vscodeSettings.mcp.servers = vscodeSettings.mcp.servers || {};
487
+ vscodeSettings.mcp.servers[name] = config;
488
+
489
+ const newContent = JSON.stringify(vscodeSettings, null, 2);
490
+ await fsPromises.writeFile(configPath, newContent, "utf8");
491
+ }
492
+ } else if (source.type === "cursor-global") {
493
+ // Cursor Global format: { "mcpServers": { ... } }
494
+ let cursorConfig: CursorGlobalConfig = { mcpServers: {} };
495
+ if (fs.existsSync(configPath)) {
496
+ try {
497
+ const content = await fsPromises.readFile(configPath, "utf8");
498
+ cursorConfig = this.parseJsonWithFallback(content);
499
+ } catch {
500
+ // Invalid JSON, start fresh
501
+ }
502
+ }
503
+
504
+ cursorConfig.mcpServers = cursorConfig.mcpServers || {};
505
+
506
+ // Convert our internal format to Cursor format
507
+ const cursorServerConfig =
508
+ "url" in config
509
+ ? {
510
+ url: config.url,
511
+ }
512
+ : {
513
+ command: config.command,
514
+ args: config.args ? [...config.args] : undefined,
515
+ env: config.env
516
+ ? Object.fromEntries(Object.entries(config.env).map(([k, v]) => [k, String(v)]))
517
+ : undefined,
518
+ };
519
+
520
+ cursorConfig.mcpServers[name] = cursorServerConfig;
521
+
522
+ const newContent = JSON.stringify(cursorConfig, null, 2);
523
+ await fsPromises.writeFile(configPath, newContent, "utf8");
524
+ } else if (source.type === "cursor-workspace") {
525
+ let cursorWorkspaceConfig: CursorGlobalConfig = { mcpServers: {} };
526
+ if (fs.existsSync(configPath)) {
527
+ try {
528
+ const content = await fsPromises.readFile(configPath, "utf8");
529
+ cursorWorkspaceConfig = this.parseJsonWithFallback(content);
530
+ } catch {
531
+ // Invalid JSON, start fresh
532
+ }
533
+ }
534
+
535
+ cursorWorkspaceConfig.mcpServers = cursorWorkspaceConfig.mcpServers || {};
536
+
537
+ const cursorServerConfig =
538
+ "url" in config
539
+ ? {
540
+ url: config.url,
541
+ }
542
+ : {
543
+ command: config.command,
544
+ args: config.args ? [...config.args] : undefined,
545
+ env: config.env
546
+ ? Object.fromEntries(Object.entries(config.env).map(([k, v]) => [k, String(v)]))
547
+ : undefined,
548
+ };
549
+
550
+ cursorWorkspaceConfig.mcpServers[name] = cursorServerConfig;
551
+ const newContent = JSON.stringify(cursorWorkspaceConfig, null, 2);
552
+ await fsPromises.writeFile(configPath, newContent, "utf8");
553
+ } else {
554
+ throw new Error(`Unsupported config source type: ${source.type}`);
555
+ }
556
+ }
557
+
558
+ private async removeServerFromSource(configPath: string, name: string): Promise<boolean> {
559
+ if (!fs.existsSync(configPath)) {
560
+ return false;
561
+ }
562
+
563
+ const content = await fsPromises.readFile(configPath, "utf8");
564
+ const isCursorOrClaudeFormat =
565
+ configPath.includes(".cursor") ||
566
+ configPath.includes("claude_desktop_config.json") ||
567
+ configPath.includes("mcp_config.json"); // Windsurf
568
+
569
+ if (configPath.endsWith("mcp.json")) {
570
+ const mcpConfig: McpConfigFile = this.parseJsonWithFallback(content);
571
+
572
+ if (!mcpConfig.servers || !mcpConfig.servers[name]) {
573
+ return false;
574
+ }
575
+
576
+ delete mcpConfig.servers[name];
577
+
578
+ const newContent = JSON.stringify(mcpConfig, null, 2);
579
+ await fsPromises.writeFile(configPath, newContent, "utf8");
580
+ return true;
581
+ } else if (configPath.endsWith("settings.json")) {
582
+ const vscodeSettings: VSCodeSettings = this.parseJsonWithFallback(content);
583
+ const config = vscodeSettings?.mcp || { servers: {} };
584
+
585
+ if (!config.servers || !config.servers[name]) {
586
+ return false;
587
+ }
588
+
589
+ delete config.servers[name];
590
+ vscodeSettings.mcp = config;
591
+
592
+ const newContent = JSON.stringify(vscodeSettings, null, 2);
593
+ await fsPromises.writeFile(configPath, newContent, "utf8");
594
+ return true;
595
+ } else if (isCursorOrClaudeFormat) {
596
+ // Claude/Cursor/Windsurf format - matches GUI's claudeConfigToServerDefinition
597
+ const cursorConfig: CursorGlobalConfig = this.parseJsonWithFallback(content);
598
+
599
+ if (!cursorConfig.mcpServers || !cursorConfig.mcpServers[name]) {
600
+ return false;
601
+ }
602
+
603
+ delete cursorConfig.mcpServers[name];
604
+
605
+ const newContent = JSON.stringify(cursorConfig, null, 2);
606
+ await fsPromises.writeFile(configPath, newContent, "utf8");
607
+ return true;
608
+ } else {
609
+ throw new Error(`Unsupported config file format: ${configPath}`);
610
+ }
611
+ }
612
+
613
+ private validateServerConfig(config: IMcpServerConfig): void {
614
+ // Check if config is null, undefined, or not an object
615
+ if (!config || typeof config !== "object") {
616
+ throw new Error("Server configuration must be a valid object");
617
+ }
618
+
619
+ // Check for invalid top-level properties that suggest wrong format
620
+ if ("mcpServers" in config || "servers" in config) {
621
+ throw new Error(
622
+ 'Invalid configuration format. Server configuration should not contain "mcpServers" or "servers" properties. Each server should be configured individually.',
623
+ );
624
+ }
625
+
626
+ if ("url" in config) {
627
+ // HTTP server validation
628
+ if (!config.url) {
629
+ throw new Error("URL is required for HTTP servers");
630
+ }
631
+ if (typeof config.url !== "string") {
632
+ throw new Error("URL must be a string");
633
+ }
634
+ if (!config.url.startsWith("http://") && !config.url.startsWith("https://")) {
635
+ throw new Error("URL must start with http:// or https://");
636
+ }
637
+ } else if ("command" in config) {
638
+ // Stdio server validation
639
+ if (!config.command) {
640
+ throw new Error("Command is required for stdio servers");
641
+ }
642
+ if (typeof config.command !== "string") {
643
+ throw new Error("Command must be a string");
644
+ }
645
+ // Validate args if present
646
+ if (config.args && !Array.isArray(config.args)) {
647
+ throw new Error("Arguments must be an array");
648
+ }
649
+ if (config.args && config.args.some((arg) => typeof arg !== "string")) {
650
+ throw new Error("All arguments must be strings");
651
+ }
652
+ } else {
653
+ throw new Error('Server configuration must have either "url" or "command" property');
654
+ }
655
+ }
656
+
657
+ private getServerTypeDescription(config: IMcpServerConfig): string {
658
+ if ("url" in config) {
659
+ return `HTTP server at ${config.url}`;
660
+ } else if ("command" in config) {
661
+ const argsStr = config.args && config.args.length > 0 ? ` ${config.args.join(" ")}` : "";
662
+ return `Command: ${config.command}${argsStr}`;
663
+ }
664
+ return "Unknown server type";
665
+ }
666
+
667
+ private getServerSourceLabel(server: IMcpServerInfo): string {
668
+ if (server.id.startsWith("claude-desktop")) {
669
+ return "Claude Desktop";
670
+ } else if (server.id.startsWith("windsurf")) {
671
+ return "Windsurf";
672
+ } else if (server.id.startsWith("cursor-global")) {
673
+ return "Cursor (Global)";
674
+ } else if (server.id.startsWith("cursor-workspace")) {
675
+ return "Cursor (Workspace)";
676
+ } else if (server.id.startsWith("abacusai-user")) {
677
+ const isDev =
678
+ server.description?.includes(`${product.fullName}-dev`) ||
679
+ server.description?.includes("CodeLLM-dev");
680
+ return isDev ? `${product.fullName}-dev (User)` : `${product.fullName} (User)`;
681
+ } else if (server.id.startsWith("vscode-user")) {
682
+ return "VSCode (User)";
683
+ } else if (server.id.startsWith("vscode-workspace")) {
684
+ return "VSCode (Workspace)";
685
+ }
686
+ return "Other";
687
+ }
688
+ }