add-mcp 1.9.0 → 1.10.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.
@@ -0,0 +1,1221 @@
1
+ // src/config.ts
2
+ import { readFile, writeFile, mkdir, rm } from "fs/promises";
3
+ import { dirname, join } from "path";
4
+ import { homedir } from "os";
5
+ var CONFIG_DIR = "add-mcp";
6
+ var CONFIG_FILE = "config.json";
7
+ var CURRENT_VERSION = 1;
8
+ var LEGACY_AGENTS_DIR = ".agents";
9
+ var LEGACY_LOCK_FILE = ".mcp-lock.json";
10
+ function getXdgConfigHome() {
11
+ return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
12
+ }
13
+ function getConfigPath() {
14
+ return join(getXdgConfigHome(), CONFIG_DIR, CONFIG_FILE);
15
+ }
16
+ function getLegacyConfigPath() {
17
+ return join(homedir(), LEGACY_AGENTS_DIR, LEGACY_LOCK_FILE);
18
+ }
19
+ async function readConfig() {
20
+ const configPath = getConfigPath();
21
+ try {
22
+ const content = await readFile(configPath, "utf-8");
23
+ const parsed = JSON.parse(content);
24
+ if (typeof parsed.version !== "number") {
25
+ return createEmptyConfig();
26
+ }
27
+ if (parsed.version < CURRENT_VERSION) {
28
+ return createEmptyConfig();
29
+ }
30
+ return parsed;
31
+ } catch {
32
+ }
33
+ const legacyPath = getLegacyConfigPath();
34
+ try {
35
+ const content = await readFile(legacyPath, "utf-8");
36
+ const parsed = JSON.parse(content);
37
+ if (typeof parsed.version !== "number" || parsed.version < CURRENT_VERSION) {
38
+ return createEmptyConfig();
39
+ }
40
+ await writeConfig(parsed);
41
+ await cleanupLegacyConfig();
42
+ return parsed;
43
+ } catch {
44
+ return createEmptyConfig();
45
+ }
46
+ }
47
+ async function writeConfig(config) {
48
+ const configPath = getConfigPath();
49
+ await mkdir(dirname(configPath), { recursive: true });
50
+ const content = JSON.stringify(config, null, 2);
51
+ await writeFile(configPath, content, "utf-8");
52
+ }
53
+ async function cleanupLegacyConfig() {
54
+ const legacyPath = getLegacyConfigPath();
55
+ try {
56
+ await rm(legacyPath, { force: true });
57
+ } catch {
58
+ }
59
+ }
60
+ async function getLastSelectedAgents() {
61
+ const config = await readConfig();
62
+ return config.lastSelectedAgents;
63
+ }
64
+ async function saveSelectedAgents(agents2) {
65
+ const config = await readConfig();
66
+ config.lastSelectedAgents = agents2;
67
+ await writeConfig(config);
68
+ }
69
+ async function getFindRegistries() {
70
+ const config = await readConfig();
71
+ if (!config.findRegistries) return [];
72
+ return config.findRegistries.map(normalizeFindRegistryEntry);
73
+ }
74
+ function normalizeFindRegistryEntry(raw) {
75
+ const legacy = raw;
76
+ const url = legacy.url ?? legacy.serversUrl;
77
+ if (!url || typeof url !== "string") {
78
+ throw new Error("Registry entry missing url");
79
+ }
80
+ return {
81
+ url,
82
+ ...legacy.label ? { label: legacy.label } : {}
83
+ };
84
+ }
85
+ async function saveFindRegistries(registries) {
86
+ const config = await readConfig();
87
+ config.findRegistries = registries;
88
+ await writeConfig(config);
89
+ }
90
+ function createEmptyConfig() {
91
+ return {
92
+ version: CURRENT_VERSION
93
+ };
94
+ }
95
+
96
+ // src/agents.ts
97
+ import * as p from "@clack/prompts";
98
+ import { homedir as homedir2 } from "os";
99
+ import { dirname as dirname2, join as join2 } from "path";
100
+ import { existsSync } from "fs";
101
+ var home = homedir2();
102
+ function shortenPath(fullPath) {
103
+ if (fullPath.startsWith(home)) {
104
+ return fullPath.replace(home, "~");
105
+ }
106
+ return fullPath;
107
+ }
108
+ function getPlatformPaths() {
109
+ const platform = process.platform;
110
+ if (platform === "win32") {
111
+ const appData = process.env.APPDATA || join2(home, "AppData", "Roaming");
112
+ return {
113
+ appSupport: appData,
114
+ vscodePath: join2(appData, "Code", "User"),
115
+ gooseConfigPath: join2(appData, "Block", "goose", "config", "config.yaml")
116
+ };
117
+ } else if (platform === "darwin") {
118
+ return {
119
+ appSupport: join2(home, "Library", "Application Support"),
120
+ vscodePath: join2(home, "Library", "Application Support", "Code", "User"),
121
+ gooseConfigPath: join2(home, ".config", "goose", "config.yaml")
122
+ };
123
+ } else {
124
+ const configDir = process.env.XDG_CONFIG_HOME || join2(home, ".config");
125
+ return {
126
+ appSupport: configDir,
127
+ vscodePath: join2(configDir, "Code", "User"),
128
+ gooseConfigPath: join2(configDir, "goose", "config.yaml")
129
+ };
130
+ }
131
+ }
132
+ var { appSupport, vscodePath, gooseConfigPath } = getPlatformPaths();
133
+ var antigravityConfigPath = join2(
134
+ home,
135
+ ".gemini",
136
+ "antigravity",
137
+ "mcp_config.json"
138
+ );
139
+ var clineCliConfigPath = join2(
140
+ process.env.CLINE_DIR || join2(home, ".cline"),
141
+ "data",
142
+ "settings",
143
+ "cline_mcp_settings.json"
144
+ );
145
+ var clineExtensionConfigPath = join2(
146
+ vscodePath,
147
+ "globalStorage",
148
+ "saoudrizwan.claude-dev",
149
+ "settings",
150
+ "cline_mcp_settings.json"
151
+ );
152
+ var copilotConfigPath = join2(
153
+ process.env.XDG_CONFIG_HOME || join2(home, ".copilot"),
154
+ "mcp-config.json"
155
+ );
156
+ function transformGooseConfig(serverName, config) {
157
+ if (config.url) {
158
+ const gooseType = config.type === "sse" ? "sse" : "streamable_http";
159
+ return {
160
+ name: serverName,
161
+ description: "",
162
+ type: gooseType,
163
+ uri: config.url,
164
+ headers: config.headers || {},
165
+ enabled: true,
166
+ timeout: 300
167
+ };
168
+ }
169
+ return {
170
+ name: serverName,
171
+ description: "",
172
+ cmd: config.command,
173
+ args: config.args || [],
174
+ enabled: true,
175
+ envs: config.env || {},
176
+ type: "stdio",
177
+ timeout: 300
178
+ };
179
+ }
180
+ function transformZedConfig(_serverName, config) {
181
+ if (config.url) {
182
+ return {
183
+ source: "custom",
184
+ type: config.type || "http",
185
+ url: config.url,
186
+ headers: config.headers || {}
187
+ };
188
+ }
189
+ return {
190
+ source: "custom",
191
+ command: config.command,
192
+ args: config.args || [],
193
+ env: config.env || {}
194
+ };
195
+ }
196
+ function transformOpenCodeConfig(_serverName, config) {
197
+ if (config.url) {
198
+ return {
199
+ type: "remote",
200
+ url: config.url,
201
+ enabled: true,
202
+ headers: config.headers
203
+ };
204
+ }
205
+ return {
206
+ type: "local",
207
+ command: [config.command, ...config.args || []],
208
+ enabled: true,
209
+ environment: config.env || {}
210
+ };
211
+ }
212
+ function transformCodexConfig(_serverName, config) {
213
+ if (config.url) {
214
+ const remoteConfig = {
215
+ type: config.type || "http",
216
+ url: config.url
217
+ };
218
+ if (config.headers && Object.keys(config.headers).length > 0) {
219
+ remoteConfig.http_headers = config.headers;
220
+ }
221
+ return remoteConfig;
222
+ }
223
+ return {
224
+ command: config.command,
225
+ args: config.args || [],
226
+ env: config.env
227
+ };
228
+ }
229
+ function transformCursorConfig(_serverName, config) {
230
+ if (config.url) {
231
+ const remoteConfig = {
232
+ url: config.url
233
+ };
234
+ if (config.headers && Object.keys(config.headers).length > 0) {
235
+ remoteConfig.headers = config.headers;
236
+ }
237
+ return remoteConfig;
238
+ }
239
+ return config;
240
+ }
241
+ function transformClineConfig(_serverName, config) {
242
+ if (config.url) {
243
+ const remoteConfig = {
244
+ url: config.url,
245
+ type: config.type === "sse" ? "sse" : "streamableHttp",
246
+ disabled: false
247
+ };
248
+ if (config.headers && Object.keys(config.headers).length > 0) {
249
+ remoteConfig.headers = config.headers;
250
+ }
251
+ return remoteConfig;
252
+ }
253
+ const localConfig = {
254
+ command: config.command,
255
+ args: config.args || [],
256
+ disabled: false
257
+ };
258
+ if (config.env && Object.keys(config.env).length > 0) {
259
+ localConfig.env = config.env;
260
+ }
261
+ return localConfig;
262
+ }
263
+ function transformAntigravityConfig(_serverName, config) {
264
+ if (config.url) {
265
+ const remoteConfig = {
266
+ serverUrl: config.url
267
+ };
268
+ if (config.headers && Object.keys(config.headers).length > 0) {
269
+ remoteConfig.headers = config.headers;
270
+ }
271
+ return remoteConfig;
272
+ }
273
+ const localConfig = {
274
+ command: config.command,
275
+ args: config.args || []
276
+ };
277
+ if (config.env && Object.keys(config.env).length > 0) {
278
+ localConfig.env = config.env;
279
+ }
280
+ return localConfig;
281
+ }
282
+ function transformGitHubCopilotCliConfig(_serverName, config, context) {
283
+ if (context?.local) {
284
+ return config;
285
+ }
286
+ if (config.url) {
287
+ const remoteConfig = {
288
+ type: config.type || "http",
289
+ url: config.url,
290
+ tools: ["*"]
291
+ };
292
+ if (config.headers && Object.keys(config.headers).length > 0) {
293
+ remoteConfig.headers = config.headers;
294
+ }
295
+ return remoteConfig;
296
+ }
297
+ const localConfig = {
298
+ type: "stdio",
299
+ command: config.command,
300
+ args: config.args || [],
301
+ tools: ["*"]
302
+ };
303
+ if (config.env && Object.keys(config.env).length > 0) {
304
+ localConfig.env = config.env;
305
+ }
306
+ return localConfig;
307
+ }
308
+ function resolveMcporterConfigPath(agent, options) {
309
+ if (options.local && agent.localConfigPath) {
310
+ return join2(options.cwd, agent.localConfigPath);
311
+ }
312
+ const globalJsonPath = agent.configPath;
313
+ const globalJsoncPath = join2(dirname2(globalJsonPath), "mcporter.jsonc");
314
+ if (existsSync(globalJsonPath)) {
315
+ return globalJsonPath;
316
+ }
317
+ if (existsSync(globalJsoncPath)) {
318
+ return globalJsoncPath;
319
+ }
320
+ return globalJsonPath;
321
+ }
322
+ var agents = {
323
+ antigravity: {
324
+ name: "antigravity",
325
+ displayName: "Antigravity",
326
+ configPath: antigravityConfigPath,
327
+ projectDetectPaths: [],
328
+ // Global only - no project support
329
+ configKey: "mcpServers",
330
+ format: "json",
331
+ supportedTransports: ["stdio", "http", "sse"],
332
+ detectGlobalInstall: async () => {
333
+ return existsSync(join2(home, ".gemini"));
334
+ },
335
+ transformConfig: transformAntigravityConfig
336
+ },
337
+ cline: {
338
+ name: "cline",
339
+ displayName: "Cline VSCode Extension",
340
+ configPath: clineExtensionConfigPath,
341
+ projectDetectPaths: [],
342
+ // Global only - no project support
343
+ configKey: "mcpServers",
344
+ format: "json",
345
+ supportedTransports: ["stdio", "http", "sse"],
346
+ detectGlobalInstall: async () => {
347
+ return existsSync(dirname2(clineExtensionConfigPath));
348
+ },
349
+ transformConfig: transformClineConfig
350
+ },
351
+ "cline-cli": {
352
+ name: "cline-cli",
353
+ displayName: "Cline CLI",
354
+ configPath: clineCliConfigPath,
355
+ projectDetectPaths: [],
356
+ // Global only - no project support
357
+ configKey: "mcpServers",
358
+ format: "json",
359
+ supportedTransports: ["stdio", "http", "sse"],
360
+ detectGlobalInstall: async () => {
361
+ return existsSync(dirname2(clineCliConfigPath));
362
+ },
363
+ transformConfig: transformClineConfig
364
+ },
365
+ "claude-code": {
366
+ name: "claude-code",
367
+ displayName: "Claude Code",
368
+ configPath: join2(home, ".claude.json"),
369
+ localConfigPath: ".mcp.json",
370
+ projectDetectPaths: [".mcp.json", ".claude"],
371
+ configKey: "mcpServers",
372
+ format: "json",
373
+ supportedTransports: ["stdio", "http", "sse"],
374
+ detectGlobalInstall: async () => {
375
+ return existsSync(join2(home, ".claude"));
376
+ }
377
+ },
378
+ "claude-desktop": {
379
+ name: "claude-desktop",
380
+ displayName: "Claude Desktop",
381
+ configPath: join2(appSupport, "Claude", "claude_desktop_config.json"),
382
+ projectDetectPaths: [],
383
+ // Global only - no project support
384
+ configKey: "mcpServers",
385
+ format: "json",
386
+ supportedTransports: ["stdio"],
387
+ unsupportedTransportMessage: "Claude Desktop only supports local (stdio) servers via its config file. Add remote servers through Settings \u2192 Connectors in the app instead.",
388
+ detectGlobalInstall: async () => {
389
+ return existsSync(join2(appSupport, "Claude"));
390
+ }
391
+ },
392
+ codex: {
393
+ name: "codex",
394
+ displayName: "Codex",
395
+ configPath: join2(
396
+ process.env.CODEX_HOME || join2(home, ".codex"),
397
+ "config.toml"
398
+ ),
399
+ localConfigPath: ".codex/config.toml",
400
+ projectDetectPaths: [".codex"],
401
+ configKey: "mcp_servers",
402
+ format: "toml",
403
+ supportedTransports: ["stdio", "http", "sse"],
404
+ detectGlobalInstall: async () => {
405
+ return existsSync(join2(home, ".codex"));
406
+ },
407
+ transformConfig: transformCodexConfig
408
+ },
409
+ cursor: {
410
+ name: "cursor",
411
+ displayName: "Cursor",
412
+ configPath: join2(home, ".cursor", "mcp.json"),
413
+ localConfigPath: ".cursor/mcp.json",
414
+ projectDetectPaths: [".cursor"],
415
+ configKey: "mcpServers",
416
+ format: "json",
417
+ supportedTransports: ["stdio", "http", "sse"],
418
+ detectGlobalInstall: async () => {
419
+ return existsSync(join2(home, ".cursor"));
420
+ },
421
+ transformConfig: transformCursorConfig
422
+ },
423
+ "gemini-cli": {
424
+ name: "gemini-cli",
425
+ displayName: "Gemini CLI",
426
+ configPath: join2(home, ".gemini", "settings.json"),
427
+ localConfigPath: ".gemini/settings.json",
428
+ projectDetectPaths: [".gemini"],
429
+ configKey: "mcpServers",
430
+ format: "json",
431
+ supportedTransports: ["stdio", "http", "sse"],
432
+ detectGlobalInstall: async () => {
433
+ return existsSync(join2(home, ".gemini"));
434
+ }
435
+ },
436
+ goose: {
437
+ name: "goose",
438
+ displayName: "Goose",
439
+ configPath: gooseConfigPath,
440
+ projectDetectPaths: [],
441
+ // Global only - no project support
442
+ configKey: "extensions",
443
+ format: "yaml",
444
+ supportedTransports: ["stdio", "http", "sse"],
445
+ detectGlobalInstall: async () => {
446
+ return existsSync(gooseConfigPath);
447
+ },
448
+ transformConfig: transformGooseConfig
449
+ },
450
+ "github-copilot-cli": {
451
+ name: "github-copilot-cli",
452
+ displayName: "GitHub Copilot CLI",
453
+ configPath: copilotConfigPath,
454
+ localConfigPath: ".vscode/mcp.json",
455
+ projectDetectPaths: [".vscode"],
456
+ configKey: "mcpServers",
457
+ localConfigKey: "servers",
458
+ format: "json",
459
+ supportedTransports: ["stdio", "http", "sse"],
460
+ detectGlobalInstall: async () => {
461
+ return existsSync(dirname2(copilotConfigPath));
462
+ },
463
+ transformConfig: transformGitHubCopilotCliConfig
464
+ },
465
+ mcporter: {
466
+ name: "mcporter",
467
+ displayName: "MCPorter",
468
+ configPath: join2(home, ".mcporter", "mcporter.json"),
469
+ localConfigPath: "config/mcporter.json",
470
+ projectDetectPaths: ["config/mcporter.json"],
471
+ configKey: "mcpServers",
472
+ format: "json",
473
+ supportedTransports: ["stdio", "http", "sse"],
474
+ detectGlobalInstall: async () => {
475
+ return existsSync(join2(home, ".mcporter"));
476
+ },
477
+ resolveConfigPath: resolveMcporterConfigPath
478
+ },
479
+ opencode: {
480
+ name: "opencode",
481
+ displayName: "OpenCode",
482
+ configPath: join2(home, ".config", "opencode", "opencode.json"),
483
+ localConfigPath: "opencode.json",
484
+ projectDetectPaths: ["opencode.json", ".opencode"],
485
+ configKey: "mcp",
486
+ format: "json",
487
+ supportedTransports: ["stdio", "http", "sse"],
488
+ detectGlobalInstall: async () => {
489
+ return existsSync(join2(home, ".config", "opencode"));
490
+ },
491
+ transformConfig: transformOpenCodeConfig
492
+ },
493
+ vscode: {
494
+ name: "vscode",
495
+ displayName: "VS Code",
496
+ configPath: join2(vscodePath, "mcp.json"),
497
+ localConfigPath: ".vscode/mcp.json",
498
+ projectDetectPaths: [".vscode"],
499
+ configKey: "servers",
500
+ format: "json",
501
+ supportedTransports: ["stdio", "http", "sse"],
502
+ detectGlobalInstall: async () => {
503
+ return existsSync(vscodePath);
504
+ }
505
+ },
506
+ zed: {
507
+ name: "zed",
508
+ displayName: "Zed",
509
+ configPath: process.platform === "darwin" || process.platform === "win32" ? join2(appSupport, "Zed", "settings.json") : join2(appSupport, "zed", "settings.json"),
510
+ localConfigPath: ".zed/settings.json",
511
+ projectDetectPaths: [".zed"],
512
+ configKey: "context_servers",
513
+ format: "json",
514
+ supportedTransports: ["stdio", "http", "sse"],
515
+ detectGlobalInstall: async () => {
516
+ const configDir = process.platform === "darwin" || process.platform === "win32" ? join2(appSupport, "Zed") : join2(appSupport, "zed");
517
+ return existsSync(configDir);
518
+ },
519
+ transformConfig: transformZedConfig
520
+ }
521
+ };
522
+ function getAgentTypes() {
523
+ return Object.keys(agents);
524
+ }
525
+ function supportsProjectConfig(agentType) {
526
+ return agents[agentType].localConfigPath !== void 0;
527
+ }
528
+ function getProjectCapableAgents() {
529
+ return Object.keys(agents).filter(
530
+ (type) => supportsProjectConfig(type)
531
+ );
532
+ }
533
+ function detectProjectAgents(cwd) {
534
+ const dir = cwd || process.cwd();
535
+ const detected = [];
536
+ for (const [type, config] of Object.entries(agents)) {
537
+ if (!config.localConfigPath) continue;
538
+ for (const detectPath of config.projectDetectPaths) {
539
+ if (existsSync(join2(dir, detectPath))) {
540
+ detected.push(type);
541
+ break;
542
+ }
543
+ }
544
+ }
545
+ return detected;
546
+ }
547
+ async function detectGlobalAgents() {
548
+ const detected = [];
549
+ for (const [type, config] of Object.entries(agents)) {
550
+ if (await config.detectGlobalInstall()) {
551
+ detected.push(type);
552
+ }
553
+ }
554
+ return detected;
555
+ }
556
+ function isTransportSupported(agentType, transport) {
557
+ return agents[agentType].supportedTransports.includes(transport);
558
+ }
559
+ function buildAgentSelectionChoices(options) {
560
+ const { availableAgents, detectedAgents, agentRouting, lastSelected } = options;
561
+ const detectedSet = new Set(detectedAgents);
562
+ const validLastSelected = lastSelected?.filter(
563
+ (agent) => availableAgents.includes(agent) && !detectedSet.has(agent)
564
+ ) ?? [];
565
+ const remainingAgents = availableAgents.filter(
566
+ (agent) => !detectedSet.has(agent) && !validLastSelected.includes(agent)
567
+ );
568
+ const orderedAgents = [
569
+ ...detectedAgents,
570
+ ...validLastSelected,
571
+ ...remainingAgents
572
+ ];
573
+ const choices = orderedAgents.map((agentType) => {
574
+ const routing = agentRouting.get(agentType);
575
+ const baseHint = routing === "local" ? "project" : routing === "global" ? "global" : shortenPath(agents[agentType].configPath);
576
+ const lastSelectedHint = validLastSelected.includes(agentType) ? "selected last time" : "";
577
+ const hint = lastSelectedHint ? `${baseHint} \xB7 ${lastSelectedHint}` : baseHint;
578
+ return {
579
+ value: agentType,
580
+ label: agents[agentType].displayName,
581
+ hint
582
+ };
583
+ });
584
+ return { choices, initialValues: detectedAgents };
585
+ }
586
+ async function promptForAgents(message, choices, defaultToAll = false) {
587
+ let lastSelected;
588
+ try {
589
+ lastSelected = await getLastSelectedAgents();
590
+ } catch {
591
+ }
592
+ const validAgents = choices.map((c) => c.value);
593
+ let initialValues;
594
+ if (lastSelected && lastSelected.length > 0) {
595
+ initialValues = lastSelected.filter(
596
+ (a) => validAgents.includes(a)
597
+ );
598
+ if (initialValues.length === 0 && defaultToAll) {
599
+ initialValues = validAgents;
600
+ }
601
+ } else {
602
+ initialValues = defaultToAll ? validAgents : [];
603
+ }
604
+ const selected = await p.multiselect({
605
+ message,
606
+ options: choices,
607
+ required: true,
608
+ initialValues
609
+ });
610
+ if (!p.isCancel(selected)) {
611
+ try {
612
+ await saveSelectedAgents(selected);
613
+ } catch {
614
+ }
615
+ }
616
+ return selected;
617
+ }
618
+ async function selectAgentsInteractive(availableAgents, options) {
619
+ let lastSelected;
620
+ try {
621
+ lastSelected = await getLastSelectedAgents();
622
+ } catch {
623
+ }
624
+ const validLastSelected = lastSelected?.filter(
625
+ (a) => availableAgents.includes(a)
626
+ );
627
+ const selectOptions = [];
628
+ const hasPrevious = validLastSelected && validLastSelected.length > 0;
629
+ if (hasPrevious) {
630
+ const agentNames = validLastSelected.map((a) => agents[a].displayName).join(", ");
631
+ selectOptions.push({
632
+ value: "previous",
633
+ label: "Same as last time",
634
+ hint: agentNames
635
+ });
636
+ }
637
+ selectOptions.push({
638
+ value: "all",
639
+ label: hasPrevious ? "All available agents" : "All available agents",
640
+ hint: `Install to all ${availableAgents.length} available agents`
641
+ });
642
+ selectOptions.push({
643
+ value: "select",
644
+ label: "Select specific agents",
645
+ hint: "Choose which agents to install to"
646
+ });
647
+ const installChoice = await p.select({
648
+ message: "Install to",
649
+ options: selectOptions
650
+ });
651
+ if (p.isCancel(installChoice)) {
652
+ return installChoice;
653
+ }
654
+ if (installChoice === "all") {
655
+ return availableAgents;
656
+ }
657
+ if (installChoice === "previous" && validLastSelected) {
658
+ return validLastSelected;
659
+ }
660
+ const agentChoices = availableAgents.map((agentType) => {
661
+ const localPath = agents[agentType].localConfigPath;
662
+ const hint = options.global ? shortenPath(agents[agentType].configPath) : localPath ?? shortenPath(agents[agentType].configPath);
663
+ return {
664
+ value: agentType,
665
+ label: agents[agentType].displayName,
666
+ hint
667
+ };
668
+ });
669
+ return promptForAgents("Select agents to install to", agentChoices, false);
670
+ }
671
+
672
+ // src/formats/utils.ts
673
+ function deepMerge(target, source) {
674
+ const result = { ...target };
675
+ for (const key in source) {
676
+ const sourceValue = source[key];
677
+ const targetValue = result[key];
678
+ if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue)) {
679
+ result[key] = deepMerge(
680
+ targetValue && typeof targetValue === "object" ? targetValue : {},
681
+ sourceValue
682
+ );
683
+ } else {
684
+ result[key] = sourceValue;
685
+ }
686
+ }
687
+ return result;
688
+ }
689
+ function getNestedValue(obj, path) {
690
+ const keys = path.split(".");
691
+ let current = obj;
692
+ for (const key of keys) {
693
+ if (current && typeof current === "object" && key in current) {
694
+ current = current[key];
695
+ } else {
696
+ return void 0;
697
+ }
698
+ }
699
+ return current;
700
+ }
701
+
702
+ // src/formats/json.ts
703
+ import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
704
+ import { dirname as dirname3 } from "path";
705
+ import * as jsonc from "jsonc-parser";
706
+ function detectIndent(text) {
707
+ let result = null;
708
+ jsonc.visit(text, {
709
+ onObjectProperty: (_property, offset, _length, startLine, startCharacter) => {
710
+ if (result === null && startLine > 0 && startCharacter > 0) {
711
+ const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
712
+ const whitespace = text.slice(lineStart, offset);
713
+ result = {
714
+ tabSize: startCharacter,
715
+ insertSpaces: !whitespace.includes(" ")
716
+ };
717
+ }
718
+ }
719
+ });
720
+ return result || { tabSize: 2, insertSpaces: true };
721
+ }
722
+ function readJsonConfig(filePath) {
723
+ if (!existsSync2(filePath)) {
724
+ return {};
725
+ }
726
+ const content = readFileSync(filePath, "utf-8");
727
+ const parsed = jsonc.parse(content);
728
+ return parsed;
729
+ }
730
+ function writeJsonConfig(filePath, config, configKey) {
731
+ const dir = dirname3(filePath);
732
+ if (!existsSync2(dir)) {
733
+ mkdirSync(dir, { recursive: true });
734
+ }
735
+ let originalContent = "";
736
+ let existingConfig = {};
737
+ if (existsSync2(filePath)) {
738
+ originalContent = readFileSync(filePath, "utf-8");
739
+ existingConfig = jsonc.parse(originalContent);
740
+ }
741
+ const mergedConfig = deepMerge(existingConfig, config);
742
+ if (originalContent) {
743
+ try {
744
+ const configKeyPath = configKey.split(".");
745
+ const newValue = getNestedValue(mergedConfig, configKey);
746
+ const edits = jsonc.modify(originalContent, configKeyPath, newValue, {
747
+ formattingOptions: detectIndent(originalContent)
748
+ });
749
+ const updatedContent = jsonc.applyEdits(originalContent, edits);
750
+ writeFileSync(filePath, updatedContent);
751
+ return;
752
+ } catch {
753
+ }
754
+ }
755
+ writeFileSync(filePath, JSON.stringify(mergedConfig, null, 2));
756
+ }
757
+ function removeJsonConfigKey(filePath, configKey, serverName) {
758
+ if (!existsSync2(filePath)) {
759
+ return;
760
+ }
761
+ const originalContent = readFileSync(filePath, "utf-8");
762
+ const configKeyPath = configKey.split(".");
763
+ try {
764
+ const edits = jsonc.modify(
765
+ originalContent,
766
+ [...configKeyPath, serverName],
767
+ void 0,
768
+ { formattingOptions: detectIndent(originalContent) }
769
+ );
770
+ const updatedContent = jsonc.applyEdits(originalContent, edits);
771
+ writeFileSync(filePath, updatedContent);
772
+ return;
773
+ } catch {
774
+ }
775
+ const parsed = jsonc.parse(originalContent);
776
+ const servers = getNestedValue(parsed, configKey);
777
+ if (servers && typeof servers === "object" && serverName in servers) {
778
+ delete servers[serverName];
779
+ }
780
+ writeFileSync(filePath, JSON.stringify(parsed, null, 2));
781
+ }
782
+ function setNestedValue(obj, path, value) {
783
+ const keys = path.split(".");
784
+ const lastKey = keys.pop();
785
+ if (!lastKey) return;
786
+ let current = obj;
787
+ for (const key of keys) {
788
+ if (!current[key] || typeof current[key] !== "object") {
789
+ current[key] = {};
790
+ }
791
+ current = current[key];
792
+ }
793
+ current[lastKey] = value;
794
+ }
795
+
796
+ // src/formats/yaml.ts
797
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
798
+ import { dirname as dirname4 } from "path";
799
+ import yaml from "js-yaml";
800
+ function readYamlConfig(filePath) {
801
+ if (!existsSync3(filePath)) {
802
+ return {};
803
+ }
804
+ const content = readFileSync2(filePath, "utf-8");
805
+ const parsed = yaml.load(content);
806
+ return parsed || {};
807
+ }
808
+ function removeYamlConfigKey(filePath, configKey, serverName) {
809
+ if (!existsSync3(filePath)) {
810
+ return;
811
+ }
812
+ const existing = readYamlConfig(filePath);
813
+ const keys = configKey.split(".");
814
+ let current = existing;
815
+ for (const key of keys) {
816
+ if (current && typeof current === "object" && key in current) {
817
+ current = current[key];
818
+ } else {
819
+ return;
820
+ }
821
+ }
822
+ if (current && typeof current === "object" && serverName in current) {
823
+ delete current[serverName];
824
+ }
825
+ const content = yaml.dump(existing, {
826
+ indent: 2,
827
+ lineWidth: -1,
828
+ noRefs: true
829
+ });
830
+ writeFileSync2(filePath, content);
831
+ }
832
+ function writeYamlConfig(filePath, config) {
833
+ const dir = dirname4(filePath);
834
+ if (!existsSync3(dir)) {
835
+ mkdirSync2(dir, { recursive: true });
836
+ }
837
+ let existingConfig = {};
838
+ if (existsSync3(filePath)) {
839
+ existingConfig = readYamlConfig(filePath);
840
+ }
841
+ const mergedConfig = deepMerge(existingConfig, config);
842
+ const content = yaml.dump(mergedConfig, {
843
+ indent: 2,
844
+ lineWidth: -1,
845
+ noRefs: true
846
+ });
847
+ writeFileSync2(filePath, content);
848
+ }
849
+
850
+ // src/formats/toml.ts
851
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
852
+ import { dirname as dirname5 } from "path";
853
+ import * as TOML from "@iarna/toml";
854
+ function readTomlConfig(filePath) {
855
+ if (!existsSync4(filePath)) {
856
+ return {};
857
+ }
858
+ const content = readFileSync3(filePath, "utf-8");
859
+ const parsed = TOML.parse(content);
860
+ return parsed;
861
+ }
862
+ function removeTomlConfigKey(filePath, configKey, serverName) {
863
+ if (!existsSync4(filePath)) {
864
+ return;
865
+ }
866
+ const existing = readTomlConfig(filePath);
867
+ const keys = configKey.split(".");
868
+ let current = existing;
869
+ for (const key of keys) {
870
+ if (current && typeof current === "object" && key in current) {
871
+ current = current[key];
872
+ } else {
873
+ return;
874
+ }
875
+ }
876
+ if (current && typeof current === "object" && serverName in current) {
877
+ delete current[serverName];
878
+ }
879
+ const content = TOML.stringify(existing);
880
+ writeFileSync3(filePath, content);
881
+ }
882
+ function writeTomlConfig(filePath, config) {
883
+ const dir = dirname5(filePath);
884
+ if (!existsSync4(dir)) {
885
+ mkdirSync3(dir, { recursive: true });
886
+ }
887
+ let existingConfig = {};
888
+ if (existsSync4(filePath)) {
889
+ existingConfig = readTomlConfig(filePath);
890
+ }
891
+ const mergedConfig = deepMerge(existingConfig, config);
892
+ const content = TOML.stringify(mergedConfig);
893
+ writeFileSync3(filePath, content);
894
+ }
895
+
896
+ // src/formats/index.ts
897
+ function readConfig2(filePath, format) {
898
+ switch (format) {
899
+ case "json":
900
+ return readJsonConfig(filePath);
901
+ case "yaml":
902
+ return readYamlConfig(filePath);
903
+ case "toml":
904
+ return readTomlConfig(filePath);
905
+ default:
906
+ throw new Error(`Unsupported config format: ${format}`);
907
+ }
908
+ }
909
+ function removeServerFromConfig(filePath, format, configKey, serverName) {
910
+ switch (format) {
911
+ case "json":
912
+ removeJsonConfigKey(filePath, configKey, serverName);
913
+ break;
914
+ case "yaml":
915
+ removeYamlConfigKey(filePath, configKey, serverName);
916
+ break;
917
+ case "toml":
918
+ removeTomlConfigKey(filePath, configKey, serverName);
919
+ break;
920
+ default:
921
+ throw new Error(`Unsupported config format: ${format}`);
922
+ }
923
+ }
924
+ function writeConfig2(filePath, config, format, configKey) {
925
+ switch (format) {
926
+ case "json":
927
+ writeJsonConfig(filePath, config, configKey);
928
+ break;
929
+ case "yaml":
930
+ writeYamlConfig(filePath, config);
931
+ break;
932
+ case "toml":
933
+ writeTomlConfig(filePath, config);
934
+ break;
935
+ default:
936
+ throw new Error(`Unsupported config format: ${format}`);
937
+ }
938
+ }
939
+ function buildConfigWithKey(configKey, serverName, serverConfig) {
940
+ const config = {};
941
+ const servers = {};
942
+ servers[serverName] = serverConfig;
943
+ setNestedValue(config, configKey, servers);
944
+ return config;
945
+ }
946
+
947
+ // src/installer.ts
948
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
949
+ import { join as join3, dirname as dirname6, isAbsolute, relative, sep } from "path";
950
+ function buildServerConfig(parsed, options = {}) {
951
+ if (parsed.type === "remote") {
952
+ const config2 = {
953
+ type: options.transport ?? "http",
954
+ url: parsed.value
955
+ };
956
+ if (options.headers && Object.keys(options.headers).length > 0) {
957
+ config2.headers = options.headers;
958
+ }
959
+ return config2;
960
+ }
961
+ if (parsed.type === "command") {
962
+ const parts = parsed.value.split(" ");
963
+ const command = parts[0];
964
+ const args = [...parts.slice(1), ...options.args ?? []];
965
+ const config2 = {
966
+ command,
967
+ args
968
+ };
969
+ if (options.env && Object.keys(options.env).length > 0) {
970
+ config2.env = options.env;
971
+ }
972
+ return config2;
973
+ }
974
+ const config = {
975
+ command: "npx",
976
+ args: ["-y", parsed.value, ...options.args ?? []]
977
+ };
978
+ if (options.env && Object.keys(options.env).length > 0) {
979
+ config.env = options.env;
980
+ }
981
+ return config;
982
+ }
983
+ function updateGitignoreWithPaths(paths, options = {}) {
984
+ const cwd = options.cwd || process.cwd();
985
+ const gitignorePath = join3(cwd, ".gitignore");
986
+ const existingContent = existsSync5(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : "";
987
+ const existingEntries = new Set(
988
+ existingContent.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
989
+ );
990
+ const entriesToAdd = [];
991
+ for (const filePath of paths) {
992
+ const relativePath = isAbsolute(filePath) ? relative(cwd, filePath) : filePath;
993
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
994
+ continue;
995
+ }
996
+ const normalizedPath = relativePath.split(sep).join("/");
997
+ const cleanPath = normalizedPath.startsWith("./") ? normalizedPath.slice(2) : normalizedPath;
998
+ if (!cleanPath || cleanPath === ".gitignore" || existingEntries.has(cleanPath)) {
999
+ continue;
1000
+ }
1001
+ existingEntries.add(cleanPath);
1002
+ entriesToAdd.push(cleanPath);
1003
+ }
1004
+ if (entriesToAdd.length > 0) {
1005
+ let nextContent = existingContent;
1006
+ if (nextContent.length > 0 && !nextContent.endsWith("\n")) {
1007
+ nextContent += "\n";
1008
+ }
1009
+ nextContent += `${entriesToAdd.join("\n")}
1010
+ `;
1011
+ writeFileSync4(gitignorePath, nextContent, "utf-8");
1012
+ }
1013
+ return {
1014
+ path: gitignorePath,
1015
+ added: entriesToAdd
1016
+ };
1017
+ }
1018
+ function getConfigPath2(agent, options = {}) {
1019
+ const local = Boolean(options.local);
1020
+ const cwd = options.cwd || process.cwd();
1021
+ if (agent.resolveConfigPath) {
1022
+ return agent.resolveConfigPath(agent, { local, cwd });
1023
+ }
1024
+ if (local && agent.localConfigPath) {
1025
+ return join3(cwd, agent.localConfigPath);
1026
+ }
1027
+ return agent.configPath;
1028
+ }
1029
+ function getConfigKey(agent, options = {}) {
1030
+ if (options.local && agent.localConfigKey) {
1031
+ return agent.localConfigKey;
1032
+ }
1033
+ return agent.configKey;
1034
+ }
1035
+ function installServerForAgent(serverName, serverConfig, agentType, options = {}) {
1036
+ const agent = agents[agentType];
1037
+ const configPath = getConfigPath2(agent, options);
1038
+ try {
1039
+ const dir = dirname6(configPath);
1040
+ if (!existsSync5(dir)) {
1041
+ mkdirSync4(dir, { recursive: true });
1042
+ }
1043
+ const transformedConfig = agent.transformConfig ? agent.transformConfig(serverName, serverConfig, {
1044
+ local: Boolean(options.local)
1045
+ }) : serverConfig;
1046
+ const configKey = getConfigKey(agent, options);
1047
+ const config = buildConfigWithKey(configKey, serverName, transformedConfig);
1048
+ writeConfig2(configPath, config, agent.format, configKey);
1049
+ return {
1050
+ success: true,
1051
+ path: configPath
1052
+ };
1053
+ } catch (error) {
1054
+ return {
1055
+ success: false,
1056
+ path: configPath,
1057
+ error: error instanceof Error ? error.message : "Unknown error"
1058
+ };
1059
+ }
1060
+ }
1061
+ function installServer(serverName, serverConfig, agentTypes, options = {}) {
1062
+ const results = /* @__PURE__ */ new Map();
1063
+ for (const agentType of agentTypes) {
1064
+ const routing = options.routing?.get(agentType);
1065
+ const installOptions = {
1066
+ local: routing === "local",
1067
+ cwd: options.cwd
1068
+ };
1069
+ const result = installServerForAgent(
1070
+ serverName,
1071
+ serverConfig,
1072
+ agentType,
1073
+ installOptions
1074
+ );
1075
+ results.set(agentType, result);
1076
+ }
1077
+ return results;
1078
+ }
1079
+
1080
+ // src/reader.ts
1081
+ function extractServerIdentity(serverConfig) {
1082
+ for (const key of ["url", "uri", "serverUrl"]) {
1083
+ const value = serverConfig[key];
1084
+ if (typeof value === "string" && value.length > 0) {
1085
+ return value;
1086
+ }
1087
+ }
1088
+ const command = typeof serverConfig.command === "string" ? serverConfig.command : typeof serverConfig.cmd === "string" ? serverConfig.cmd : void 0;
1089
+ if (!command) {
1090
+ return "";
1091
+ }
1092
+ const rawArgs = Array.isArray(serverConfig.args) ? serverConfig.args.filter((a) => typeof a === "string") : Array.isArray(serverConfig.command) ? serverConfig.command.slice(1).filter((a) => typeof a === "string") : [];
1093
+ if (command === "npx" || command === "bunx") {
1094
+ const yIndex = rawArgs.indexOf("-y");
1095
+ const pkgIndex = yIndex >= 0 ? yIndex + 1 : 0;
1096
+ const pkg = rawArgs[pkgIndex];
1097
+ if (pkg && !pkg.startsWith("-")) {
1098
+ return pkg;
1099
+ }
1100
+ }
1101
+ if (Array.isArray(serverConfig.command)) {
1102
+ return serverConfig.command.join(" ");
1103
+ }
1104
+ if (rawArgs.length > 0) {
1105
+ return `${command} ${rawArgs.join(" ")}`;
1106
+ }
1107
+ return command;
1108
+ }
1109
+ function readServersForAgent(agentType, options) {
1110
+ const agent = agents[agentType];
1111
+ const installOptions = {
1112
+ local: options.scope === "local",
1113
+ cwd: options.cwd
1114
+ };
1115
+ const configPath = getConfigPath2(agent, installOptions);
1116
+ const configKey = getConfigKey(agent, installOptions);
1117
+ const fullConfig = readConfig2(configPath, agent.format);
1118
+ const serversObj = getNestedValue(fullConfig, configKey);
1119
+ const servers = [];
1120
+ if (serversObj && typeof serversObj === "object" && !Array.isArray(serversObj)) {
1121
+ for (const [serverName, serverConfig] of Object.entries(serversObj)) {
1122
+ if (serverConfig && typeof serverConfig === "object") {
1123
+ const config = serverConfig;
1124
+ servers.push({
1125
+ serverName,
1126
+ config,
1127
+ identity: extractServerIdentity(config),
1128
+ agentType,
1129
+ scope: options.scope,
1130
+ configPath
1131
+ });
1132
+ }
1133
+ }
1134
+ }
1135
+ return {
1136
+ agentType,
1137
+ displayName: agent.displayName,
1138
+ detected: true,
1139
+ scope: options.scope,
1140
+ configPath,
1141
+ servers
1142
+ };
1143
+ }
1144
+ async function listInstalledServers(options) {
1145
+ const scope = options.global ? "global" : "local";
1146
+ const results = [];
1147
+ if (options.agents && options.agents.length > 0) {
1148
+ const detectedSet = new Set(
1149
+ options.global ? await detectGlobalAgents() : detectProjectAgents(options.cwd)
1150
+ );
1151
+ for (const agentType of options.agents) {
1152
+ const detected = detectedSet.has(agentType);
1153
+ if (detected) {
1154
+ results.push(
1155
+ readServersForAgent(agentType, { scope, cwd: options.cwd })
1156
+ );
1157
+ } else {
1158
+ const agent = agents[agentType];
1159
+ const installOptions = {
1160
+ local: scope === "local",
1161
+ cwd: options.cwd
1162
+ };
1163
+ results.push({
1164
+ agentType,
1165
+ displayName: agent.displayName,
1166
+ detected: false,
1167
+ scope,
1168
+ configPath: getConfigPath2(agent, installOptions),
1169
+ servers: []
1170
+ });
1171
+ }
1172
+ }
1173
+ } else {
1174
+ const detected = options.global ? await detectGlobalAgents() : detectProjectAgents(options.cwd);
1175
+ for (const agentType of detected) {
1176
+ results.push(readServersForAgent(agentType, { scope, cwd: options.cwd }));
1177
+ }
1178
+ }
1179
+ return results;
1180
+ }
1181
+ function findMatchingServers(agentServersList, query) {
1182
+ const lowerQuery = query.toLowerCase();
1183
+ const matches = [];
1184
+ for (const agentServers of agentServersList) {
1185
+ for (const server of agentServers.servers) {
1186
+ const nameMatch = server.serverName.toLowerCase().includes(lowerQuery);
1187
+ const identityMatch = server.identity === query;
1188
+ if (nameMatch || identityMatch) {
1189
+ matches.push(server);
1190
+ }
1191
+ }
1192
+ }
1193
+ return matches;
1194
+ }
1195
+
1196
+ export {
1197
+ getConfigPath,
1198
+ getLastSelectedAgents,
1199
+ getFindRegistries,
1200
+ saveFindRegistries,
1201
+ agents,
1202
+ getAgentTypes,
1203
+ supportsProjectConfig,
1204
+ getProjectCapableAgents,
1205
+ detectProjectAgents,
1206
+ detectGlobalAgents,
1207
+ isTransportSupported,
1208
+ buildAgentSelectionChoices,
1209
+ selectAgentsInteractive,
1210
+ getNestedValue,
1211
+ readConfig2 as readConfig,
1212
+ removeServerFromConfig,
1213
+ buildServerConfig,
1214
+ updateGitignoreWithPaths,
1215
+ getConfigPath2,
1216
+ getConfigKey,
1217
+ installServerForAgent,
1218
+ installServer,
1219
+ listInstalledServers,
1220
+ findMatchingServers
1221
+ };