mcpick 0.0.24 → 0.0.26

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 (39) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +8 -2
  3. package/dist/{add-Qzd8i-5k.js → add-DwUPxD5U.js} +21 -35
  4. package/dist/{add-json-DGmsjB0O.js → add-json-Dk3C8QSR.js} +18 -34
  5. package/dist/atomic-write-9pfXia-x.js +22 -0
  6. package/dist/{backup-C7fvikFw.js → backup-Ber7st-_.js} +4 -5
  7. package/dist/{cache-D3jjh5dD.js → cache-BVNAOxqk.js} +4 -3
  8. package/dist/{cli-CZOlaqoZ.js → cli-Dfein2li.js} +22 -22
  9. package/dist/{clients-Bh93TGP4.js → clients-C1mWlnsn.js} +3 -3
  10. package/dist/{clone-MI8jJhTz.js → clone-BE904ik7.js} +3 -6
  11. package/dist/{dev-51esdZG9.js → dev-BW_2JDPM.js} +5 -6
  12. package/dist/{disable-csYAn2Vk.js → disable-CVwuJLhT.js} +18 -37
  13. package/dist/{enable-B5GbmhL-.js → enable-BEi8TUd1.js} +18 -37
  14. package/dist/{get-DacRZmwv.js → get-eDOm6sRK.js} +3 -3
  15. package/dist/{hooks-C_x49qap.js → hooks-DzGBYYId.js} +4 -3
  16. package/dist/index.js +752 -645
  17. package/dist/{list-BeBtsiae.js → list-CvpW_mNZ.js} +3 -5
  18. package/dist/{marketplace-BDC2YtvT.js → marketplace-BKGJ3UGr.js} +4 -4
  19. package/dist/mutation-BBMIFB8H.js +19 -0
  20. package/dist/{output-HtT5HCof.js → output-DGPi2IWN.js} +2 -2
  21. package/dist/{plugin-cache-DmLbh89d.js → plugin-cache-Dj-lS1mA.js} +177 -22
  22. package/dist/{plugins-Bkw-SKkZ.js → plugins-elIQYM38.js} +4 -4
  23. package/dist/profile-NVbTnJKI.js +163 -0
  24. package/dist/{reload-Bl1mYK1I.js → reload-BkVtWaf0.js} +2 -2
  25. package/dist/{remove-BSHgva79.js → remove-BC2vomi_.js} +18 -33
  26. package/dist/{reset-project-choices-BNLus9J9.js → reset-project-choices-DhJsWQx2.js} +3 -3
  27. package/dist/{restore-YisgARhc.js → restore-CeYSOFt8.js} +3 -5
  28. package/dist/{rollback-GR1RkpXW.js → rollback-g-WX6b3o.js} +3 -3
  29. package/dist/safe-apply-B5flt8QC.js +185 -0
  30. package/dist/{skills-rDTDqqZA.js → skills-zuQ5kCL1.js} +3 -3
  31. package/package.json +6 -6
  32. package/pnpm-workspace.yaml +1 -0
  33. package/dist/config-DE58Fik_.js +0 -176
  34. package/dist/dry-run-XQ32fxPT.js +0 -20
  35. package/dist/paths-BPISiJi4.js +0 -124
  36. package/dist/profile-DwJTVXiz.js +0 -161
  37. package/dist/redact-wBMtzbno.js +0 -88
  38. package/dist/rolldown-runtime-CiIaOW0V.js +0 -13
  39. package/dist/validation-xMlbgGCF.js +0 -44
package/dist/index.js CHANGED
@@ -1,115 +1,222 @@
1
1
  #!/usr/bin/env node
2
- import { _ as get_profiles_dir, a as get_claude_settings_path, c as get_disabled_hooks_path, f as get_marketplaces_dir, g as get_profile_path, i as get_claude_config_path, m as get_plugin_backup_filename, n as get_backup_filename, p as get_mcpick_dir, r as get_backups_dir, t as ensure_directory_exists, y as get_server_registry_path } from "./paths-BPISiJi4.js";
3
- import { r as validate_server_registry, t as validate_claude_config } from "./validation-xMlbgGCF.js";
4
- import { a as get_enabled_servers, c as write_claude_config, n as create_config_from_servers, o as get_enabled_servers_for_scope, s as read_claude_config } from "./config-DE58Fik_.js";
5
- import { a as redact_url, i as redact_text, o as redact_value } from "./redact-wBMtzbno.js";
6
- import { d as refresh_all_marketplaces, l as read_known_marketplaces, n as clear_plugin_caches, r as get_cached_plugins_info, t as clean_orphaned_versions, u as read_marketplace_manifest } from "./plugin-cache-DmLbh89d.js";
2
+ import { S as get_server_registry_path, _ as get_plugin_backup_filename, a as get_backup_filename, b as get_profiles_dir, c as get_claude_settings_path, g as get_mcpick_dir, h as get_marketplaces_dir, i as ensure_directory_exists, l as get_current_project_path, o as get_backups_dir, r as safe_json_write, s as get_claude_config_path, x as get_project_mcp_json_path, y as get_profile_path } from "./safe-apply-B5flt8QC.js";
3
+ import { t as atomic_json_write } from "./atomic-write-9pfXia-x.js";
4
+ import { _ as redisable_restored_hooks, c as read_known_marketplaces, g as read_disabled_hooks, h as enable_plugin_hook, l as read_marketplace_manifest, m as disable_plugin_hook, n as clear_plugin_caches, p as check_restored_hooks, r as get_cached_plugins_info, s as read_installed_plugins, t as clean_orphaned_versions, u as refresh_all_marketplaces } from "./plugin-cache-Dj-lS1mA.js";
7
5
  import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, text } from "@clack/prompts";
8
- import { access, mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
9
- import { basename, dirname, join, resolve } from "node:path";
10
- import { createHash, randomUUID } from "node:crypto";
6
+ import { access, readFile, readdir, unlink, writeFile } from "node:fs/promises";
7
+ import { join, resolve } from "node:path";
11
8
  import { homedir } from "node:os";
9
+ import * as v from "valibot";
12
10
  import { execFile } from "node:child_process";
13
11
  import { promisify } from "node:util";
14
- //#region src/utils/safe-apply.ts
15
- async function file_exists$1(path) {
12
+ //#region src/core/validation.ts
13
+ const mcp_server_schema_stdio = v.object({
14
+ type: v.optional(v.literal("stdio")),
15
+ command: v.pipe(v.string(), v.minLength(1)),
16
+ args: v.optional(v.array(v.string())),
17
+ env: v.optional(v.record(v.string(), v.string())),
18
+ description: v.optional(v.string())
19
+ });
20
+ const mcp_server_schema_sse = v.object({
21
+ type: v.literal("sse"),
22
+ env: v.optional(v.record(v.string(), v.string())),
23
+ url: v.pipe(v.string(), v.minLength(1)),
24
+ headers: v.optional(v.record(v.string(), v.string())),
25
+ description: v.optional(v.string())
26
+ });
27
+ const mcp_server_schema_http = v.object({
28
+ type: v.literal("http"),
29
+ env: v.optional(v.record(v.string(), v.string())),
30
+ url: v.pipe(v.string(), v.minLength(1)),
31
+ headers: v.optional(v.record(v.string(), v.string())),
32
+ description: v.optional(v.string())
33
+ });
34
+ const mcp_server_schema_base = v.union([
35
+ mcp_server_schema_stdio,
36
+ mcp_server_schema_sse,
37
+ mcp_server_schema_http
38
+ ]);
39
+ const mcp_server_schema = v.intersect([v.object({ name: v.pipe(v.string(), v.minLength(1)) }), mcp_server_schema_base]);
40
+ const claude_config_schema = v.object({ mcpServers: v.optional(v.record(v.string(), mcp_server_schema_base)) });
41
+ const portable_mcp_server_schema = v.object({
42
+ name: v.pipe(v.string(), v.minLength(1)),
43
+ transport: v.union([
44
+ v.literal("stdio"),
45
+ v.literal("http"),
46
+ v.literal("sse")
47
+ ]),
48
+ command: v.optional(v.string()),
49
+ args: v.optional(v.array(v.string())),
50
+ url: v.optional(v.string()),
51
+ env: v.optional(v.record(v.string(), v.string())),
52
+ headers: v.optional(v.record(v.string(), v.string())),
53
+ description: v.optional(v.string()),
54
+ disabled: v.optional(v.boolean()),
55
+ client_options: v.optional(v.record(v.string(), v.unknown()))
56
+ });
57
+ const server_registry_schema = v.object({
58
+ version: v.literal(3),
59
+ servers: v.array(portable_mcp_server_schema)
60
+ });
61
+ function validate_mcp_server(data) {
62
+ return v.parse(mcp_server_schema, data);
63
+ }
64
+ function validate_claude_config(data) {
65
+ return v.parse(claude_config_schema, data);
66
+ }
67
+ function validate_server_registry(data) {
68
+ return v.parse(server_registry_schema, data);
69
+ }
70
+ //#endregion
71
+ //#region src/core/config.ts
72
+ async function read_claude_config() {
73
+ const config_path = get_claude_config_path();
16
74
  try {
17
- await access(path);
18
- return true;
19
- } catch {
20
- return false;
75
+ await access(config_path);
76
+ const config_content = await readFile(config_path, "utf-8");
77
+ return validate_claude_config(JSON.parse(config_content));
78
+ } catch (error) {
79
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return { mcpServers: {} };
80
+ throw error;
21
81
  }
22
82
  }
23
- function backup_name(path) {
24
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
25
- const hash = createHash("sha256").update(path).digest("hex").slice(0, 10);
26
- return `config-${basename(path).replace(/[^A-Za-z0-9._-]/g, "_")}-${stamp}-${hash}.json`;
83
+ async function write_claude_config(config) {
84
+ await atomic_json_write(get_claude_config_path(), (existing) => {
85
+ existing.mcpServers = config.mcpServers;
86
+ return existing;
87
+ });
27
88
  }
28
- async function create_backup(path, content) {
29
- const backups_dir = get_backups_dir();
30
- await ensure_directory_exists(backups_dir);
31
- const backup_path = join(backups_dir, backup_name(path));
32
- await writeFile(backup_path, content, "utf-8");
33
- await writeFile(`${backup_path}.meta.json`, JSON.stringify({
34
- original_path: path,
35
- created_at: (/* @__PURE__ */ new Date()).toISOString()
36
- }, null, 2), "utf-8");
37
- return backup_path;
89
+ function get_enabled_servers(config) {
90
+ if (!config.mcpServers) return [];
91
+ return Object.entries(config.mcpServers).map(([name, server]) => ({
92
+ ...server,
93
+ name
94
+ }));
38
95
  }
39
96
  /**
40
- * Safely replace a JSON file: backup existing content, write via temp+rename,
41
- * verify the result parses, and restore the original content on failure.
97
+ * Find a server's full config in a specific scope.
42
98
  */
43
- async function safe_json_write(path, data, indent = 2) {
44
- await mkdir(dirname(path), { recursive: true });
45
- const original_content = await file_exists$1(path) ? await readFile(path, "utf-8") : void 0;
46
- const backup_path = original_content !== void 0 ? await create_backup(path, original_content) : void 0;
47
- const tmp_path = join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
48
- const next_content = JSON.stringify(data, null, indent);
49
- try {
50
- await writeFile(tmp_path, next_content, "utf-8");
51
- await rename(tmp_path, path);
52
- const written = await readFile(path, "utf-8");
53
- JSON.parse(written);
54
- return {
55
- path,
56
- ...backup_path ? { backup_path } : {}
57
- };
58
- } catch (error) {
59
- await rm(tmp_path, { force: true }).catch(() => void 0);
60
- if (original_content !== void 0) await writeFile(path, original_content, "utf-8");
61
- else await rm(path, { force: true }).catch(() => void 0);
62
- throw error;
99
+ async function find_server_in_scope(name, scope) {
100
+ if (scope === "user" || scope === "local") {
101
+ const config_path = get_claude_config_path();
102
+ try {
103
+ await access(config_path);
104
+ const content = await readFile(config_path, "utf-8");
105
+ const parsed = JSON.parse(content);
106
+ if (scope === "user") {
107
+ const server = parsed.mcpServers?.[name];
108
+ if (server) return {
109
+ server,
110
+ scope: "user"
111
+ };
112
+ } else {
113
+ const cwd = get_current_project_path();
114
+ const server = parsed.projects?.[cwd]?.mcpServers?.[name];
115
+ if (server) return {
116
+ server,
117
+ scope: "local"
118
+ };
119
+ }
120
+ } catch {}
121
+ } else if (scope === "project") {
122
+ const mcp_path = get_project_mcp_json_path();
123
+ try {
124
+ await access(mcp_path);
125
+ const content = await readFile(mcp_path, "utf-8");
126
+ const server = JSON.parse(content).mcpServers?.[name];
127
+ if (server) return {
128
+ server,
129
+ scope: "project"
130
+ };
131
+ } catch {}
63
132
  }
133
+ return null;
64
134
  }
65
- async function list_config_backups() {
66
- const backups_dir = get_backups_dir();
135
+ /**
136
+ * Auto-detect which scope a server lives in.
137
+ * Searches local → project → user.
138
+ */
139
+ async function detect_server_scope(name) {
140
+ for (const scope of [
141
+ "local",
142
+ "project",
143
+ "user"
144
+ ]) {
145
+ const result = await find_server_in_scope(name, scope);
146
+ if (result) return result;
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Read full Claude config including projects section
152
+ */
153
+ async function read_claude_config_full() {
154
+ const config_path = get_claude_config_path();
67
155
  try {
68
- const files = await readdir(backups_dir);
69
- const backups = [];
70
- for (const file of files) {
71
- if (!file.startsWith("config-") || !file.endsWith(".json")) continue;
72
- if (file.endsWith(".meta.json")) continue;
73
- const backup_path = join(backups_dir, file);
74
- try {
75
- const meta = JSON.parse(await readFile(`${backup_path}.meta.json`, "utf-8"));
76
- if (typeof meta.original_path !== "string" || typeof meta.created_at !== "string") continue;
77
- backups.push({
78
- path: backup_path,
79
- original_path: meta.original_path,
80
- created_at: meta.created_at
81
- });
82
- } catch {}
83
- }
84
- return backups.sort((a, b) => b.created_at.localeCompare(a.created_at));
156
+ await access(config_path);
157
+ const config_content = await readFile(config_path, "utf-8");
158
+ return JSON.parse(config_content);
85
159
  } catch {
86
- return [];
160
+ return {
161
+ mcpServers: {},
162
+ projects: {}
163
+ };
87
164
  }
88
165
  }
89
- async function restore_config_backup(backup_path) {
90
- const backup = (await list_config_backups()).find((candidate) => candidate.path === backup_path || basename(candidate.path) === backup_path);
91
- if (!backup) throw new Error(`Config backup '${backup_path}' not found.`);
92
- const content = await readFile(backup.path, "utf-8");
93
- const parsed = JSON.parse(content);
94
- await safe_json_write(backup.original_path, parsed);
95
- return backup;
166
+ /**
167
+ * Read MCP servers for local scope (current project)
168
+ * Stored in ~/.claude.json -> projects[cwd].mcpServers
169
+ * Also searches parent directories since Claude CLI may store config at parent level
170
+ */
171
+ async function read_local_mcp_servers() {
172
+ const { dirname } = await import("node:path");
173
+ const { homedir } = await import("node:os");
174
+ const full_config = await read_claude_config_full();
175
+ const home = homedir();
176
+ let current_dir = get_current_project_path();
177
+ while (current_dir && current_dir !== "/" && current_dir.length >= home.length) {
178
+ const project_config = full_config.projects?.[current_dir];
179
+ if (project_config?.mcpServers && Object.keys(project_config.mcpServers).length > 0) return Object.keys(project_config.mcpServers);
180
+ current_dir = dirname(current_dir);
181
+ }
182
+ return [];
96
183
  }
97
- //#endregion
98
- //#region src/utils/atomic-write.ts
99
184
  /**
100
- * Atomically write a JSON file with fresh-read merging.
101
- *
102
- * 1. Re-reads the file right before writing to pick up concurrent changes
103
- * 2. Applies the merge function to the freshest data
104
- * 3. Writes to a temp file, then renames (atomic on same filesystem)
185
+ * Read MCP servers from ~/.claude.json -> mcpServers (user scope)
105
186
  */
106
- async function atomic_json_write(file_path, merge) {
107
- let existing = {};
108
- try {
109
- const content = await readFile(file_path, "utf-8");
110
- existing = JSON.parse(content);
111
- } catch {}
112
- await safe_json_write(file_path, merge(existing), 2);
187
+ async function read_user_mcp_servers() {
188
+ const config = await read_claude_config();
189
+ return Object.keys(config.mcpServers || {});
190
+ }
191
+ /**
192
+ * Read MCP servers from .mcp.json files (project scope)
193
+ * Searches current directory and parents for .mcp.json
194
+ */
195
+ async function find_and_read_project_mcp_json() {
196
+ const { dirname } = await import("node:path");
197
+ let current_dir = get_current_project_path();
198
+ const home = (await import("node:os")).homedir();
199
+ while (current_dir && current_dir !== "/" && current_dir.length >= home.length) {
200
+ const mcp_path = `${current_dir}/.mcp.json`;
201
+ try {
202
+ await access(mcp_path);
203
+ const content = await readFile(mcp_path, "utf-8");
204
+ const servers = JSON.parse(content).mcpServers || {};
205
+ return Object.keys(servers);
206
+ } catch {}
207
+ current_dir = dirname(current_dir);
208
+ }
209
+ return [];
210
+ }
211
+ /**
212
+ * Get currently enabled server names for a specific scope
213
+ */
214
+ async function get_enabled_servers_for_scope(scope) {
215
+ switch (scope) {
216
+ case "local": return read_local_mcp_servers();
217
+ case "project": return find_and_read_project_mcp_json();
218
+ case "user": return read_user_mcp_servers();
219
+ }
113
220
  }
114
221
  //#endregion
115
222
  //#region src/core/settings.ts
@@ -215,8 +322,6 @@ async function get_all_hooks() {
215
322
  * Checks both cache and marketplace source paths since Claude Code reads from both.
216
323
  */
217
324
  async function get_all_plugin_hooks() {
218
- const { read_installed_plugins } = await import("./plugin-cache-DmLbh89d.js").then((n) => n.s);
219
- const { get_marketplaces_dir } = await import("./paths-BPISiJi4.js").then((n) => n.b);
220
325
  const installed = await read_installed_plugins();
221
326
  const entries = [];
222
327
  const seen_hooks = /* @__PURE__ */ new Set();
@@ -344,43 +449,6 @@ async function cleanup_old_backups(prefix) {
344
449
  } catch {}
345
450
  }
346
451
  //#endregion
347
- //#region src/utils/config-preview.ts
348
- function build_json_change_preview(input) {
349
- const before = redact_value(input.before);
350
- const after = redact_value(input.after);
351
- return {
352
- dryRun: true,
353
- operation: input.operation,
354
- client: input.client,
355
- scope: input.scope,
356
- location: input.location,
357
- before,
358
- after,
359
- diff: create_json_diff(before, after)
360
- };
361
- }
362
- function build_command_preview(input) {
363
- return {
364
- dryRun: true,
365
- operation: input.operation,
366
- client: input.client,
367
- scope: input.scope,
368
- location: input.location,
369
- command: input.command.map((part) => redact_text(part))
370
- };
371
- }
372
- function create_json_diff(before, after) {
373
- const before_lines = JSON.stringify(before, null, 2).split("\n");
374
- const after_lines = JSON.stringify(after, null, 2).split("\n");
375
- if (before_lines.join("\n") === after_lines.join("\n")) return "";
376
- return [
377
- "--- before",
378
- "+++ after",
379
- ...before_lines.map((line) => `- ${line}`),
380
- ...after_lines.map((line) => `+ ${line}`)
381
- ].join("\n");
382
- }
383
- //#endregion
384
452
  //#region src/core/client-config.ts
385
453
  const client_options_to_skip = new Set([
386
454
  "name",
@@ -414,7 +482,7 @@ async function read_json_file(path) {
414
482
  }
415
483
  }
416
484
  async function write_json_file(path, data) {
417
- await safe_json_write(path, data, 2);
485
+ return safe_json_write(path, data, 2);
418
486
  }
419
487
  function parse_json_or_jsonc(content) {
420
488
  try {
@@ -503,13 +571,13 @@ function remove_trailing_commas(content) {
503
571
  }
504
572
  return result;
505
573
  }
506
- function string_record(value) {
574
+ function string_record$1(value) {
507
575
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
508
576
  const result = {};
509
577
  for (const [key, item] of Object.entries(value)) if (typeof item === "string") result[key] = item;
510
578
  return Object.keys(result).length > 0 ? result : void 0;
511
579
  }
512
- function string_array(value) {
580
+ function string_array$1(value) {
513
581
  if (!Array.isArray(value)) return void 0;
514
582
  const values = value.filter((item) => typeof item === "string");
515
583
  return values.length > 0 ? values : void 0;
@@ -525,10 +593,10 @@ function normalize_mcp_server(name, config) {
525
593
  const url = typeof config.httpUrl === "string" ? config.httpUrl : typeof config.serverUrl === "string" ? config.serverUrl : typeof config.url === "string" ? config.url : void 0;
526
594
  const client_options = {};
527
595
  for (const [key, value] of Object.entries(config)) if (!client_options_to_skip.has(key)) client_options[key] = value;
528
- const command_array = string_array(config.command);
596
+ const command_array = string_array$1(config.command);
529
597
  const command = typeof config.command === "string" ? config.command : command_array?.[0];
530
- const args = string_array(config.args) ?? command_array?.slice(1);
531
- const env = string_record(config.env) ?? string_record(config.environment);
598
+ const args = string_array$1(config.args) ?? command_array?.slice(1);
599
+ const env = string_record$1(config.env) ?? string_record$1(config.environment);
532
600
  const disabled = typeof config.disabled === "boolean" ? config.disabled : typeof config.enabled === "boolean" ? !config.enabled : void 0;
533
601
  return {
534
602
  name,
@@ -537,7 +605,7 @@ function normalize_mcp_server(name, config) {
537
605
  ...args && args.length > 0 ? { args } : {},
538
606
  ...url ? { url } : {},
539
607
  ...env ? { env } : {},
540
- ...string_record(config.headers) ? { headers: string_record(config.headers) } : {},
608
+ ...string_record$1(config.headers) ? { headers: string_record$1(config.headers) } : {},
541
609
  ...typeof config.description === "string" ? { description: config.description } : {},
542
610
  ...typeof disabled === "boolean" ? { disabled } : {},
543
611
  ...Object.keys(client_options).length > 0 ? { client_options } : {}
@@ -574,20 +642,26 @@ function portable_to_json(server, mode) {
574
642
  if (typeof server.disabled === "boolean") set_server_enabled(result, !server.disabled, mode);
575
643
  return result;
576
644
  }
577
- function create_json_adapter(options) {
578
- async function read_data(location) {
579
- return await read_json_file(location.path) ?? {};
580
- }
581
- function preview(location, operation, before, after) {
582
- return build_json_change_preview({
583
- operation,
584
- client: options.id,
585
- scope: location.scope,
586
- location: location.path,
587
- before,
588
- after
589
- });
645
+ function opencode_to_json(server) {
646
+ const result = {};
647
+ const allowed_options = server.transport === "stdio" ? new Set(["cwd", "timeout"]) : new Set(["oauth", "timeout"]);
648
+ for (const [key, value] of Object.entries(server.client_options ?? {})) if (allowed_options.has(key)) result[key] = value;
649
+ if (server.transport === "stdio") {
650
+ result.type = "local";
651
+ if (server.command) result.command = [server.command, ...server.args ?? []];
652
+ if (server.env) result.environment = server.env;
653
+ } else {
654
+ result.type = "remote";
655
+ if (server.url) result.url = server.url;
656
+ if (server.headers) result.headers = server.headers;
590
657
  }
658
+ if (typeof server.disabled === "boolean") result.enabled = !server.disabled;
659
+ return result;
660
+ }
661
+ function portable_server_map(servers, mode, serialize = portable_to_json) {
662
+ return Object.fromEntries(servers.map((server) => [server.name, serialize(server, mode)]));
663
+ }
664
+ function create_json_adapter(options) {
591
665
  return {
592
666
  id: options.id,
593
667
  label: options.label,
@@ -602,66 +676,38 @@ function create_json_adapter(options) {
602
676
  return read_server_map(await read_json_file(location.path), options.serverKey);
603
677
  },
604
678
  async writeEnabled(location, enabled_names) {
605
- const data = await read_data(location);
679
+ const data = await read_json_file(location.path) ?? {};
606
680
  const servers = get_server_record(data, options.serverKey);
607
681
  const enabled = new Set(enabled_names);
608
682
  for (const [name, config] of Object.entries(servers)) set_server_enabled(config, enabled.has(name), options.disabledMode ?? "disabled");
609
683
  data[options.serverKey] = servers;
610
- await write_json_file(location.path, data);
611
- },
612
- async previewEnabled(location, enabled_names) {
613
- const before = await read_data(location);
614
- const after = structuredClone(before);
615
- const servers = get_server_record(after, options.serverKey);
616
- const enabled = new Set(enabled_names);
617
- for (const [name, config] of Object.entries(servers)) set_server_enabled(config, enabled.has(name), options.disabledMode ?? "disabled");
618
- after[options.serverKey] = servers;
619
- return preview(location, "set-enabled", before, after);
684
+ return write_json_file(location.path, data);
620
685
  },
621
686
  async write_server(location, server) {
622
- const data = await read_data(location);
687
+ const data = await read_json_file(location.path) ?? {};
623
688
  const servers = get_server_record(data, options.serverKey);
624
- servers[server.name] = portable_to_json(server, options.disabledMode ?? "disabled");
689
+ servers[server.name] = (options.serialize ?? portable_to_json)(server, options.disabledMode ?? "disabled");
625
690
  data[options.serverKey] = servers;
626
- await write_json_file(location.path, data);
627
- },
628
- async preview_write_server(location, server) {
629
- const before = await read_data(location);
630
- const after = structuredClone(before);
631
- const servers = get_server_record(after, options.serverKey);
632
- servers[server.name] = portable_to_json(server, options.disabledMode ?? "disabled");
633
- after[options.serverKey] = servers;
634
- return preview(location, "add-server", before, after);
691
+ return write_json_file(location.path, data);
635
692
  },
636
693
  async write_server_config(location, name, config) {
637
- const data = await read_data(location);
694
+ const data = await read_json_file(location.path) ?? {};
638
695
  const servers = get_server_record(data, options.serverKey);
639
696
  servers[name] = config;
640
697
  data[options.serverKey] = servers;
641
- await write_json_file(location.path, data);
642
- },
643
- async preview_write_server_config(location, name, config) {
644
- const before = await read_data(location);
645
- const after = structuredClone(before);
646
- const servers = get_server_record(after, options.serverKey);
647
- servers[name] = config;
648
- after[options.serverKey] = servers;
649
- return preview(location, "add-json", before, after);
698
+ return write_json_file(location.path, data);
650
699
  },
651
700
  async remove_server(location, name) {
652
- const data = await read_data(location);
701
+ const data = await read_json_file(location.path) ?? {};
653
702
  const servers = get_server_record(data, options.serverKey);
654
703
  delete servers[name];
655
704
  data[options.serverKey] = servers;
656
- await write_json_file(location.path, data);
705
+ return write_json_file(location.path, data);
657
706
  },
658
- async preview_remove_server(location, name) {
659
- const before = await read_data(location);
660
- const after = structuredClone(before);
661
- const servers = get_server_record(after, options.serverKey);
662
- delete servers[name];
663
- after[options.serverKey] = servers;
664
- return preview(location, "remove-server", before, after);
707
+ async write_servers(location, servers) {
708
+ const data = await read_json_file(location.path) ?? {};
709
+ data[options.serverKey] = portable_server_map(servers, options.disabledMode ?? "disabled", options.serialize);
710
+ return write_json_file(location.path, data);
665
711
  }
666
712
  };
667
713
  }
@@ -704,6 +750,24 @@ const client_adapters = [
704
750
  const project_config = projects[process.cwd()];
705
751
  if (!project_config || typeof project_config !== "object" || Array.isArray(project_config)) return [];
706
752
  return read_server_map(project_config, "mcpServers");
753
+ },
754
+ async write_servers(location, servers) {
755
+ const data = await read_json_file(location.path) ?? (location.scope === "project" ? {} : { projects: {} });
756
+ const mcp_servers = portable_server_map(servers, "disabled");
757
+ if (location.scope === "project") {
758
+ data.mcpServers = mcp_servers;
759
+ return write_json_file(location.path, data);
760
+ }
761
+ if (location.scope === "user") {
762
+ data.mcpServers = mcp_servers;
763
+ return write_json_file(get_claude_config_path(), data);
764
+ }
765
+ const projects = data.projects && typeof data.projects === "object" && !Array.isArray(data.projects) ? data.projects : {};
766
+ const project_config = projects[process.cwd()] && typeof projects[process.cwd()] === "object" && !Array.isArray(projects[process.cwd()]) ? projects[process.cwd()] : {};
767
+ project_config.mcpServers = mcp_servers;
768
+ projects[process.cwd()] = project_config;
769
+ data.projects = projects;
770
+ return write_json_file(get_claude_config_path(), data);
707
771
  }
708
772
  },
709
773
  create_json_adapter({
@@ -759,6 +823,7 @@ const client_adapters = [
759
823
  label: "OpenCode",
760
824
  serverKey: "mcp",
761
825
  disabledMode: "enabled",
826
+ serialize: opencode_to_json,
762
827
  locations: () => [{
763
828
  scope: "project",
764
829
  path: project_path("opencode.json"),
@@ -800,6 +865,17 @@ const client_adapters = [
800
865
  function get_client_adapter(id) {
801
866
  return client_adapters.find((adapter) => adapter.id === id) ?? null;
802
867
  }
868
+ function mutation_result(adapter, location, operation, servers, write_result, enabledCount) {
869
+ return {
870
+ operation,
871
+ client: adapter.id,
872
+ scope: location.scope,
873
+ location: write_result.path,
874
+ servers,
875
+ ...enabledCount !== void 0 ? { enabledCount } : {},
876
+ ...write_result.backup_path ? { backup_path: write_result.backup_path } : {}
877
+ };
878
+ }
803
879
  function resolve_client_location(adapter, scope, path) {
804
880
  let locations = adapter.locations();
805
881
  if (path) locations = locations.filter((location) => location.path === path);
@@ -810,43 +886,31 @@ function resolve_client_location(adapter, scope, path) {
810
886
  }
811
887
  async function add_client_server(adapter, location, server) {
812
888
  if (!adapter.write_server) throw new Error(`${adapter.label} support cannot add servers yet.`);
813
- await adapter.write_server(location, server);
814
- }
815
- async function preview_add_client_server(adapter, location, server) {
816
- if (!adapter.preview_write_server) throw new Error(`${adapter.label} support cannot preview server additions yet.`);
817
- return adapter.preview_write_server(location, server);
889
+ const write_result = await adapter.write_server(location, server);
890
+ return mutation_result(adapter, location, "add", [server.name], write_result);
818
891
  }
819
892
  async function add_client_server_config(adapter, location, name, config) {
820
893
  if (!adapter.write_server_config) throw new Error(`${adapter.label} support cannot add servers yet.`);
821
- await adapter.write_server_config(location, name, config);
822
- }
823
- async function preview_add_client_server_config(adapter, location, name, config) {
824
- if (!adapter.preview_write_server_config) throw new Error(`${adapter.label} support cannot preview server additions yet.`);
825
- return adapter.preview_write_server_config(location, name, config);
894
+ const write_result = await adapter.write_server_config(location, name, config);
895
+ return mutation_result(adapter, location, "add", [name], write_result);
826
896
  }
827
897
  async function remove_client_server(adapter, location, server_name) {
828
898
  if (!adapter.remove_server) throw new Error(`${adapter.label} support cannot remove servers yet.`);
829
- await adapter.remove_server(location, server_name);
830
- }
831
- async function preview_remove_client_server(adapter, location, server_name) {
832
- if (!adapter.preview_remove_server) throw new Error(`${adapter.label} support cannot preview server removals yet.`);
833
- return adapter.preview_remove_server(location, server_name);
899
+ const write_result = await adapter.remove_server(location, server_name);
900
+ return mutation_result(adapter, location, "remove", [server_name], write_result);
834
901
  }
835
- function assert_enabled_server_names(location, servers, enabled_names) {
902
+ async function set_client_enabled_servers(adapter, location, enabled_names) {
903
+ if (!adapter.writeEnabled) throw new Error(`${adapter.label} support is read-only.`);
904
+ const servers = await adapter.readLocation(location);
836
905
  const known_names = new Set(servers.map((server) => server.name));
837
906
  const unknown_names = enabled_names.filter((name) => !known_names.has(name));
838
907
  if (unknown_names.length > 0) throw new Error(`Server '${unknown_names[0]}' not found at ${location.path}.`);
908
+ return mutation_result(adapter, location, "set-enabled", enabled_names, await adapter.writeEnabled(location, enabled_names), enabled_names.length);
839
909
  }
840
- async function set_client_enabled_servers(adapter, location, enabled_names) {
841
- if (!adapter.writeEnabled) throw new Error(`${adapter.label} support is read-only.`);
842
- assert_enabled_server_names(location, await adapter.readLocation(location), enabled_names);
843
- await adapter.writeEnabled(location, enabled_names);
844
- return enabled_names.length;
845
- }
846
- async function preview_set_client_enabled_servers(adapter, location, enabled_names) {
847
- if (!adapter.previewEnabled) throw new Error(`${adapter.label} support cannot preview toggles yet.`);
848
- assert_enabled_server_names(location, await adapter.readLocation(location), enabled_names);
849
- return adapter.previewEnabled(location, enabled_names);
910
+ async function replace_client_servers(adapter, location, servers) {
911
+ if (!adapter.write_servers) throw new Error(`${adapter.label} support cannot replace server profiles yet.`);
912
+ const write_result = await adapter.write_servers(location, servers);
913
+ return mutation_result(adapter, location, "replace", servers.map((server) => server.name), write_result, servers.filter((server) => server.disabled !== true).length);
850
914
  }
851
915
  async function set_client_server_enabled(adapter, location, server_name, enabled) {
852
916
  const servers = await adapter.readLocation(location);
@@ -855,7 +919,11 @@ async function set_client_server_enabled(adapter, location, server_name, enabled
855
919
  const enabled_names = new Set(servers.filter((candidate) => candidate.disabled !== true).map((candidate) => candidate.name));
856
920
  if (enabled) enabled_names.add(server.name);
857
921
  else enabled_names.delete(server.name);
858
- return set_client_enabled_servers(adapter, location, [...enabled_names]);
922
+ return {
923
+ ...await set_client_enabled_servers(adapter, location, [...enabled_names]),
924
+ operation: enabled ? "enable" : "disable",
925
+ servers: [server_name]
926
+ };
859
927
  }
860
928
  async function list_client_locations() {
861
929
  return await Promise.all(client_adapters.flatMap((adapter) => adapter.locations().map(async (location) => ({
@@ -867,16 +935,56 @@ async function list_client_locations() {
867
935
  }
868
936
  //#endregion
869
937
  //#region src/core/registry.ts
938
+ function is_object$1(value) {
939
+ return !!value && typeof value === "object" && !Array.isArray(value);
940
+ }
941
+ function portable_to_mcp_server(server) {
942
+ return validate_mcp_server({
943
+ name: server.name,
944
+ ...server.transport !== "stdio" ? { type: server.transport } : {},
945
+ ...server.command ? { command: server.command } : {},
946
+ ...server.args ? { args: server.args } : {},
947
+ ...server.url ? { url: server.url } : {},
948
+ ...server.env ? { env: server.env } : {},
949
+ ...server.headers ? { headers: server.headers } : {},
950
+ ...server.description ? { description: server.description } : {}
951
+ });
952
+ }
953
+ function mcp_server_to_portable(server) {
954
+ const { name, ...config } = server;
955
+ return normalize_mcp_server(name, config);
956
+ }
957
+ function parse_portable_registry(data) {
958
+ if (!is_object$1(data) || !Array.isArray(data.servers)) throw new Error("Invalid server registry: expected servers array");
959
+ if (data.version === 3) return validate_server_registry(data);
960
+ return validate_server_registry({
961
+ version: 3,
962
+ servers: data.servers.map((server) => {
963
+ if (!is_object$1(server)) throw new Error("Invalid server registry entry");
964
+ if (typeof server.transport === "string") {
965
+ if (typeof server.name !== "string") throw new Error("Invalid server registry entry");
966
+ return normalize_mcp_server(server.name, server);
967
+ }
968
+ return mcp_server_to_portable(validate_mcp_server(server));
969
+ })
970
+ });
971
+ }
870
972
  async function read_server_registry() {
871
973
  const registry_path = get_server_registry_path();
872
974
  try {
873
975
  await access(registry_path);
874
976
  const registry_content = await readFile(registry_path, "utf-8");
875
- return validate_server_registry(JSON.parse(registry_content));
977
+ const parsed_registry = JSON.parse(registry_content);
978
+ const registry = parse_portable_registry(parsed_registry);
979
+ if (parsed_registry.version !== 3) await write_server_registry(registry);
980
+ return registry;
876
981
  } catch (error) {
877
982
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
878
983
  await ensure_directory_exists(get_mcpick_dir());
879
- const default_registry = { servers: [] };
984
+ const default_registry = {
985
+ version: 3,
986
+ servers: []
987
+ };
880
988
  await write_server_registry(default_registry);
881
989
  return default_registry;
882
990
  }
@@ -890,13 +998,13 @@ async function write_server_registry(registry) {
890
998
  }
891
999
  async function add_server_to_registry(server) {
892
1000
  const registry = await read_server_registry();
1001
+ const portable = mcp_server_to_portable(server);
893
1002
  const existing_index = registry.servers.findIndex((s) => s.name === server.name);
894
- if (existing_index >= 0) registry.servers[existing_index] = server;
895
- else registry.servers.push(server);
1003
+ if (existing_index >= 0) registry.servers[existing_index] = portable;
1004
+ else registry.servers.push(portable);
896
1005
  await write_server_registry(registry);
897
1006
  }
898
1007
  async function get_all_available_servers() {
899
- const { get_enabled_servers, read_claude_config } = await import("./config-DE58Fik_.js").then((n) => n.t);
900
1008
  const registry = await read_server_registry();
901
1009
  const config_servers = get_enabled_servers(await read_claude_config());
902
1010
  const config_by_name = new Map(config_servers.map((s) => [s.name, s]));
@@ -907,25 +1015,16 @@ async function get_all_available_servers() {
907
1015
  known_names.add(name);
908
1016
  const config_server = config_by_name.get(name);
909
1017
  if (config_server) {
910
- registry.servers[i] = config_server;
1018
+ registry.servers[i] = mcp_server_to_portable(config_server);
911
1019
  registry_updated = true;
912
1020
  }
913
1021
  }
914
1022
  for (const server of config_servers) if (!known_names.has(server.name)) {
915
- registry.servers.push(server);
1023
+ registry.servers.push(mcp_server_to_portable(server));
916
1024
  registry_updated = true;
917
1025
  }
918
1026
  if (registry_updated) await write_server_registry(registry);
919
- return registry.servers;
920
- }
921
- async function sync_servers_to_registry(servers) {
922
- const registry = await read_server_registry();
923
- servers.forEach((server) => {
924
- const existing_index = registry.servers.findIndex((s) => s.name === server.name);
925
- if (existing_index >= 0) registry.servers[existing_index] = server;
926
- else registry.servers.push(server);
927
- });
928
- await write_server_registry(registry);
1027
+ return registry.servers.map(portable_to_mcp_server);
929
1028
  }
930
1029
  function parse_backups(prefix, pattern) {
931
1030
  return async () => {
@@ -951,6 +1050,195 @@ function parse_backups(prefix, pattern) {
951
1050
  const list_backups = parse_backups("mcp-servers-", /mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
952
1051
  const list_plugin_backups = parse_backups("plugins-", /plugins-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
953
1052
  //#endregion
1053
+ //#region src/utils/redact.ts
1054
+ const SENSITIVE_KEY_PATTERN = /(api[_-]?key|token|secret|password|passwd|authorization|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key)/i;
1055
+ const ANSI_PATTERN = new RegExp(String.raw`[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))`, "g");
1056
+ const TEXT_REDACTIONS = [
1057
+ [/AKIA[A-Z0-9]{16}/g, "[REDACTED:AWS_ACCESS_KEY]"],
1058
+ [/\bAIza[0-9A-Za-z_-]{35}\b/g, "[REDACTED:GOOGLE_API_KEY]"],
1059
+ [/\bya29\.[0-9A-Za-z_-]{20,}\b/g, "[REDACTED:GOOGLE_OAUTH_TOKEN]"],
1060
+ [/\bsk-(?:live|test)?_?[a-zA-Z0-9._-]{20,}\b/g, "[REDACTED:API_KEY]"],
1061
+ [/\b(?:pk|rk)_(?:live|test)_[a-zA-Z0-9]{20,}\b/g, "[REDACTED:API_KEY]"],
1062
+ [/\bBearer\s+[a-zA-Z0-9._~+/-]+=*/gi, "Bearer [REDACTED]"],
1063
+ [/(:\/\/[^:\s/@]+):([^@\s]+)@/g, "$1:[REDACTED]@"],
1064
+ [/([?&][^=&#\s]*(?:token|key|secret|password|passwd)[^=&#\s]*=)[^&#\s]*/gi, "$1[REDACTED]"],
1065
+ [/\b(Authorization\s*:\s*)(?:Bearer\s+)?[^\s,}\]]+/gi, "$1[REDACTED]"],
1066
+ [/\b((?:api[_-]?key|token|secret|password|passwd|client[_-]?secret|access[_-]?token|refresh[_-]?token)\s*[:=]\s*["']?)[^"'\s,}\]]{8,}/gi, "$1[REDACTED]"],
1067
+ [/\b((?:SecretAccessKey|aws_secret_access_key)\s*[:=]\s*)[A-Za-z0-9/+=]{20,}/gi, "$1[REDACTED]"],
1068
+ [/-----BEGIN\s+[\w\s]*PRIVATE\s+KEY-----[\s\S]*?-----END\s+[\w\s]*PRIVATE\s+KEY-----/g, "[REDACTED:PRIVATE_KEY]"]
1069
+ ];
1070
+ /**
1071
+ * Redact known secret patterns from arbitrary text. This is a safety net for
1072
+ * CLI stdout/stderr; structured MCP config redaction should happen first.
1073
+ */
1074
+ function redact_text(text) {
1075
+ let redacted = text;
1076
+ for (const [pattern, replacement] of TEXT_REDACTIONS) redacted = redacted.replace(pattern, replacement);
1077
+ return redacted.replace(ANSI_PATTERN, "");
1078
+ }
1079
+ function redact_value(value, key = "") {
1080
+ if (typeof value === "string") return SENSITIVE_KEY_PATTERN.test(key) ? "***" : redact_text(value);
1081
+ if (Array.isArray(value)) return value.map((item) => redact_value(item));
1082
+ if (value && typeof value === "object") {
1083
+ const redacted = {};
1084
+ for (const [child_key, child_value] of Object.entries(value)) redacted[child_key] = child_key === "env" || child_key === "headers" ? redact_unknown_record(child_value) : SENSITIVE_KEY_PATTERN.test(child_key) ? "***" : redact_value(child_value, child_key);
1085
+ return redacted;
1086
+ }
1087
+ return value;
1088
+ }
1089
+ /**
1090
+ * Redact sensitive values from a server config.
1091
+ * Shows env/header key names but replaces values with "***".
1092
+ */
1093
+ function redact_server_base(server) {
1094
+ const redacted = redact_value(server);
1095
+ if ("env" in redacted && redacted.env) redacted.env = redact_record(redacted.env);
1096
+ if ("headers" in redacted && redacted.headers) redacted.headers = redact_record(redacted.headers);
1097
+ if ("url" in redacted && redacted.url) redacted.url = redact_url(redacted.url);
1098
+ return redacted;
1099
+ }
1100
+ function redact_server(server) {
1101
+ return {
1102
+ ...redact_server_base(server),
1103
+ name: server.name
1104
+ };
1105
+ }
1106
+ function redact_portable_server(server) {
1107
+ return {
1108
+ ...redact_value(server),
1109
+ ...server.env ? { env: redact_record(server.env) } : {},
1110
+ ...server.headers ? { headers: redact_record(server.headers) } : {},
1111
+ ...server.url ? { url: redact_url(server.url) } : {}
1112
+ };
1113
+ }
1114
+ function redact_url(url) {
1115
+ try {
1116
+ const parsed = new URL(url);
1117
+ parsed.username = "";
1118
+ parsed.password = "";
1119
+ parsed.search = parsed.search ? "?redacted" : "";
1120
+ parsed.hash = "";
1121
+ return parsed.toString();
1122
+ } catch {
1123
+ return redact_text(url.replace(/[?#].*$/, "?redacted"));
1124
+ }
1125
+ }
1126
+ function redact_unknown_record(value) {
1127
+ if (!value || typeof value !== "object" || Array.isArray(value)) return redact_value(value);
1128
+ const redacted = {};
1129
+ for (const key of Object.keys(value)) redacted[key] = "***";
1130
+ return redacted;
1131
+ }
1132
+ function redact_record(record) {
1133
+ const redacted = {};
1134
+ for (const key of Object.keys(record)) redacted[key] = "***";
1135
+ return redacted;
1136
+ }
1137
+ //#endregion
1138
+ //#region src/commands/edit-config.ts
1139
+ async function edit_config() {
1140
+ try {
1141
+ const sorted_adapters = [...client_adapters].sort((a, b) => b.label.localeCompare(a.label));
1142
+ const client_id = await select({
1143
+ message: "Which MCP client do you want to edit?",
1144
+ options: sorted_adapters.map((adapter) => ({
1145
+ value: adapter.id,
1146
+ label: adapter.label
1147
+ })),
1148
+ initialValue: sorted_adapters[0]?.id
1149
+ });
1150
+ if (typeof client_id === "symbol") return;
1151
+ const adapter = client_adapters.find((candidate) => candidate.id === client_id);
1152
+ if (!adapter) return;
1153
+ if (adapter.id === "claude-code") {
1154
+ await edit_claude_config(adapter);
1155
+ return;
1156
+ }
1157
+ await edit_client_config(adapter);
1158
+ } catch (error) {
1159
+ throw new Error(`Failed to edit configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
1160
+ }
1161
+ }
1162
+ async function edit_client_config(adapter) {
1163
+ if (!adapter.writeEnabled) {
1164
+ note(`${adapter.label} support is read-only for now.`);
1165
+ return;
1166
+ }
1167
+ const location = await select_config_location(adapter);
1168
+ if (!location) return;
1169
+ const servers = await adapter.readLocation(location);
1170
+ if (servers.length === 0) {
1171
+ note(`No MCP servers found at ${location.path}.`);
1172
+ return;
1173
+ }
1174
+ const selected_names = await multiselect({
1175
+ message: `Toggle MCP servers for ${adapter.label}:`,
1176
+ options: servers.map((server) => ({
1177
+ value: server.name,
1178
+ label: server.name,
1179
+ hint: server_hint(server)
1180
+ })),
1181
+ initialValues: servers.filter((server) => server.disabled !== true).map((server) => server.name),
1182
+ required: false
1183
+ });
1184
+ if (typeof selected_names === "symbol") return;
1185
+ const mutation = await set_client_enabled_servers(adapter, location, selected_names);
1186
+ note(`Configuration updated!\nClient: ${adapter.label}\nConfig: ${mutation.location}\n` + (mutation.backup_path ? `Backup: ${mutation.backup_path}\n` : "") + `Enabled servers: ${selected_names.length}`);
1187
+ }
1188
+ async function select_config_location(adapter) {
1189
+ const locations = adapter.locations();
1190
+ if (locations.length === 1) return locations[0];
1191
+ const location_path = await select({
1192
+ message: `Which ${adapter.label} configuration do you want to edit?`,
1193
+ options: locations.map((location) => ({
1194
+ value: location.path,
1195
+ label: `${location.scope} — ${location.description}`,
1196
+ hint: location.path
1197
+ }))
1198
+ });
1199
+ if (typeof location_path === "symbol") return null;
1200
+ return locations.find((location) => location.path === location_path) ?? null;
1201
+ }
1202
+ function server_hint(server) {
1203
+ return [
1204
+ server.disabled === true ? "off" : "on",
1205
+ server.command ? [server.command, ...server.args ?? []].join(" ") : server.url ? redact_url(server.url) : server.transport,
1206
+ server.description
1207
+ ].filter(Boolean).join(" · ");
1208
+ }
1209
+ async function edit_claude_config(adapter) {
1210
+ const location = await select_config_location(adapter);
1211
+ if (!location) return;
1212
+ const registry_servers = (await get_all_available_servers()).map((server) => {
1213
+ const { name, ...config } = server;
1214
+ return normalize_mcp_server(name, config);
1215
+ });
1216
+ const current_servers = await adapter.readLocation(location);
1217
+ const servers_by_name = /* @__PURE__ */ new Map();
1218
+ for (const server of registry_servers) servers_by_name.set(server.name, server);
1219
+ for (const server of current_servers) if (!servers_by_name.has(server.name)) servers_by_name.set(server.name, server);
1220
+ const all_servers = [...servers_by_name.values()];
1221
+ if (all_servers.length === 0) {
1222
+ note("No MCP servers found in this Claude Code config or registry. Add servers with the CLI first.");
1223
+ return;
1224
+ }
1225
+ const currently_enabled = current_servers.filter((server) => server.disabled !== true).map((server) => server.name);
1226
+ const selected_server_names = await multiselect({
1227
+ message: `Select MCP servers for ${location.description}:`,
1228
+ options: all_servers.map((server) => ({
1229
+ value: server.name,
1230
+ label: server.name,
1231
+ hint: server_hint(server)
1232
+ })),
1233
+ initialValues: currently_enabled,
1234
+ required: false
1235
+ });
1236
+ if (typeof selected_server_names === "symbol") return;
1237
+ const selected_servers = all_servers.filter((server) => selected_server_names.includes(server.name));
1238
+ const mutation = await replace_client_servers(adapter, location, selected_servers);
1239
+ note(`Configuration updated!\nClient: ${adapter.label}\nConfig: ${mutation.location}\n` + (mutation.backup_path ? `Backup: ${mutation.backup_path}\n` : "") + `Enabled servers: ${selected_servers.length}`);
1240
+ }
1241
+ //#endregion
954
1242
  //#region src/utils/claude-cli.ts
955
1243
  const exec_file_async$1 = promisify(execFile);
956
1244
  /**
@@ -1381,16 +1669,6 @@ async function marketplace_list_via_cli() {
1381
1669
  }
1382
1670
  }
1383
1671
  /**
1384
- * Get the scope description for display
1385
- */
1386
- function get_scope_description(scope) {
1387
- switch (scope) {
1388
- case "local": return "This project only (default)";
1389
- case "project": return "Shared via .mcp.json (version controlled)";
1390
- case "user": return "Global - all projects";
1391
- }
1392
- }
1393
- /**
1394
1672
  * Validate a plugin or marketplace manifest via Claude CLI
1395
1673
  */
1396
1674
  async function validate_plugin_via_cli(path) {
@@ -1443,23 +1721,20 @@ async function mcp_get_via_cli(name) {
1443
1721
  /**
1444
1722
  * Add an MCP server from raw JSON via Claude CLI
1445
1723
  */
1446
- function build_add_json_args(name, json, scope = "local") {
1447
- return [
1448
- "mcp",
1449
- "add-json",
1450
- name,
1451
- json,
1452
- "--scope",
1453
- scope
1454
- ];
1455
- }
1456
1724
  async function mcp_add_json_via_cli(name, json, scope = "local") {
1457
1725
  if (!await check_claude_cli()) return {
1458
1726
  success: false,
1459
1727
  error: "Claude CLI not found. Please install Claude Code CLI."
1460
1728
  };
1461
1729
  try {
1462
- await run_claude(build_add_json_args(name, json, scope));
1730
+ await run_claude([
1731
+ "mcp",
1732
+ "add-json",
1733
+ name,
1734
+ json,
1735
+ "--scope",
1736
+ scope
1737
+ ]);
1463
1738
  return { success: true };
1464
1739
  } catch (error) {
1465
1740
  return {
@@ -1486,174 +1761,6 @@ async function mcp_reset_project_choices_via_cli() {
1486
1761
  };
1487
1762
  }
1488
1763
  }
1489
- /**
1490
- * Get scope options for select prompt
1491
- */
1492
- function get_scope_options() {
1493
- return [
1494
- {
1495
- value: "local",
1496
- label: "Local",
1497
- hint: "This project only (default)"
1498
- },
1499
- {
1500
- value: "project",
1501
- label: "Project",
1502
- hint: "Shared via .mcp.json (git)"
1503
- },
1504
- {
1505
- value: "user",
1506
- label: "User (Global)",
1507
- hint: "Available in all projects"
1508
- }
1509
- ];
1510
- }
1511
- //#endregion
1512
- //#region src/commands/edit-config.ts
1513
- async function edit_config() {
1514
- try {
1515
- const client_id = await select({
1516
- message: "Which MCP client do you want to edit?",
1517
- options: client_adapters.map((adapter) => ({
1518
- value: adapter.id,
1519
- label: adapter.label
1520
- })),
1521
- initialValue: "claude-code"
1522
- });
1523
- if (typeof client_id === "symbol") return;
1524
- const adapter = client_adapters.find((candidate) => candidate.id === client_id);
1525
- if (!adapter) return;
1526
- if (adapter.id === "claude-code") {
1527
- await edit_claude_config();
1528
- return;
1529
- }
1530
- await edit_client_config(adapter);
1531
- } catch (error) {
1532
- throw new Error(`Failed to edit configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
1533
- }
1534
- }
1535
- async function edit_client_config(adapter) {
1536
- if (!adapter.writeEnabled) {
1537
- note(`${adapter.label} support is read-only for now.`);
1538
- return;
1539
- }
1540
- const location = await select_config_location(adapter);
1541
- if (!location) return;
1542
- const servers = await adapter.readLocation(location);
1543
- if (servers.length === 0) {
1544
- note(`No MCP servers found at ${location.path}.`);
1545
- return;
1546
- }
1547
- const selected_names = await multiselect({
1548
- message: `Toggle MCP servers for ${adapter.label}:`,
1549
- options: servers.map((server) => ({
1550
- value: server.name,
1551
- label: server.name,
1552
- hint: server_hint(server)
1553
- })),
1554
- initialValues: servers.filter((server) => server.disabled !== true).map((server) => server.name),
1555
- required: false
1556
- });
1557
- if (typeof selected_names === "symbol") return;
1558
- const preview = await preview_set_client_enabled_servers(adapter, location, selected_names);
1559
- if (preview.diff) {
1560
- note(preview.diff, "Preview");
1561
- const should_apply = await confirm({
1562
- message: "Apply these changes?",
1563
- initialValue: true
1564
- });
1565
- if (typeof should_apply === "symbol" || !should_apply) return;
1566
- }
1567
- await set_client_enabled_servers(adapter, location, selected_names);
1568
- note(`Configuration updated!\nClient: ${adapter.label}\nConfig: ${location.path}\nEnabled servers: ${selected_names.length}`);
1569
- }
1570
- async function select_config_location(adapter) {
1571
- const locations = adapter.locations();
1572
- if (locations.length === 1) return locations[0];
1573
- const location_path = await select({
1574
- message: `Which ${adapter.label} configuration do you want to edit?`,
1575
- options: locations.map((location) => ({
1576
- value: location.path,
1577
- label: `${location.scope} — ${location.description}`,
1578
- hint: location.path
1579
- }))
1580
- });
1581
- if (typeof location_path === "symbol") return null;
1582
- return locations.find((location) => location.path === location_path) ?? null;
1583
- }
1584
- function server_hint(server) {
1585
- return [
1586
- server.disabled === true ? "off" : "on",
1587
- server.command ? [server.command, ...server.args ?? []].join(" ") : server.url ? redact_url(server.url) : server.transport,
1588
- server.description
1589
- ].filter(Boolean).join(" · ");
1590
- }
1591
- async function edit_claude_config() {
1592
- const cli_available = await check_claude_cli();
1593
- const scope = await select({
1594
- message: "Which Claude Code configuration do you want to edit?",
1595
- options: get_scope_options(),
1596
- initialValue: "local"
1597
- });
1598
- if (typeof scope === "symbol") return;
1599
- const current_config = await read_claude_config();
1600
- let all_servers = await get_all_available_servers();
1601
- if (all_servers.length === 0 && current_config.mcpServers) {
1602
- const current_servers = get_enabled_servers(current_config);
1603
- if (current_servers.length > 0) {
1604
- await sync_servers_to_registry(current_servers);
1605
- all_servers = current_servers;
1606
- note(`Imported ${current_servers.length} servers from your .claude.json file into registry.`);
1607
- }
1608
- }
1609
- if (all_servers.length === 0) {
1610
- note("No MCP servers found in .claude.json or registry. Add servers with the CLI first.");
1611
- return;
1612
- }
1613
- const currently_enabled = await get_enabled_servers_for_scope(scope);
1614
- const selected_server_names = await multiselect({
1615
- message: `Select MCP servers for ${get_scope_description(scope)}:`,
1616
- options: all_servers.map((server) => ({
1617
- value: server.name,
1618
- label: server.name,
1619
- hint: server.description || ""
1620
- })),
1621
- initialValues: currently_enabled,
1622
- required: false
1623
- });
1624
- if (typeof selected_server_names === "symbol") return;
1625
- const selected_servers = all_servers.filter((server) => selected_server_names.includes(server.name));
1626
- const servers_to_add = selected_server_names.filter((name) => !currently_enabled.includes(name));
1627
- const servers_to_remove = currently_enabled.filter((name) => !selected_server_names.includes(name));
1628
- if (cli_available && (scope === "local" || scope === "project")) {
1629
- let error_count = 0;
1630
- for (const name of servers_to_add) {
1631
- const server = all_servers.find((s) => s.name === name);
1632
- if (server) {
1633
- const result = await add_mcp_via_cli(server, scope);
1634
- if (!result.success) {
1635
- error_count++;
1636
- log.warn(`Failed to add ${name}: ${result.error}`);
1637
- }
1638
- }
1639
- }
1640
- for (const name of servers_to_remove) {
1641
- const result = await remove_mcp_via_cli(name, scope);
1642
- if (!result.success) {
1643
- error_count++;
1644
- log.warn(`Failed to remove ${name}: ${result.error}`);
1645
- }
1646
- }
1647
- await sync_servers_to_registry(selected_servers);
1648
- if (error_count > 0) note(`Configuration updated with ${error_count} errors.\nScope: ${get_scope_description(scope)}\nAdded: ${servers_to_add.length}, Removed: ${servers_to_remove.length}`);
1649
- else note(`Configuration updated!\nScope: ${get_scope_description(scope)}\nEnabled servers: ${selected_servers.length}`);
1650
- } else {
1651
- await write_claude_config(create_config_from_servers(selected_servers));
1652
- await sync_servers_to_registry(selected_servers);
1653
- if (!cli_available && scope !== "user") log.warn(`Claude CLI not available. Changes written to ~/.claude.json (user scope) instead of ${scope} scope.`);
1654
- note(`Configuration updated!\nEnabled servers: ${selected_servers.length}`);
1655
- }
1656
- }
1657
1764
  //#endregion
1658
1765
  //#region src/commands/edit-plugins.ts
1659
1766
  async function handle_toggle() {
@@ -1858,6 +1965,7 @@ async function handle_clear() {
1858
1965
  if (isCancel(should_clear) || !should_clear) return;
1859
1966
  const result = await clear_plugin_caches(selected);
1860
1967
  for (const key of result.cleared) log.success(`Cleared: ${key}`);
1968
+ if (result.redisabledHooks?.success) log.success(`Re-disabled restored hooks: ${result.redisabledHooks.success}`);
1861
1969
  for (const err of result.errors) log.error(`Error: ${err}`);
1862
1970
  if (result.cleared.length > 0) note("Run /reload-plugins in Claude Code or restart your session to apply changes.", "Next Steps");
1863
1971
  }
@@ -1932,171 +2040,6 @@ async function manage_cache() {
1932
2040
  }
1933
2041
  }
1934
2042
  //#endregion
1935
- //#region src/core/hook-state.ts
1936
- async function read_disabled_hooks() {
1937
- try {
1938
- const content = await readFile(get_disabled_hooks_path(), "utf-8");
1939
- return JSON.parse(content);
1940
- } catch {
1941
- return [];
1942
- }
1943
- }
1944
- async function write_disabled_hooks(entries) {
1945
- await ensure_directory_exists(get_mcpick_dir());
1946
- await safe_json_write(get_disabled_hooks_path(), entries, " ");
1947
- }
1948
- /**
1949
- * Remove a specific hook handler from a hooks.json file by matching the handler.
1950
- * Returns true if the hook was found and removed.
1951
- */
1952
- async function remove_hook_from_file(hooks_path, event, handler) {
1953
- let content;
1954
- try {
1955
- content = await readFile(hooks_path, "utf-8");
1956
- } catch {
1957
- return false;
1958
- }
1959
- const hooks_data = JSON.parse(content);
1960
- const hooks_obj = hooks_data.hooks || hooks_data;
1961
- const matchers = hooks_obj[event];
1962
- if (!matchers) return false;
1963
- let removed = false;
1964
- for (const m of matchers) {
1965
- const idx = m.hooks?.findIndex((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt);
1966
- if (idx !== void 0 && idx >= 0) {
1967
- m.hooks.splice(idx, 1);
1968
- removed = true;
1969
- if (m.hooks.length === 0) matchers.splice(matchers.indexOf(m), 1);
1970
- break;
1971
- }
1972
- }
1973
- if (!removed) return false;
1974
- if (matchers.length === 0) delete hooks_obj[event];
1975
- await safe_json_write(hooks_path, hooks_data, " ");
1976
- return true;
1977
- }
1978
- /**
1979
- * Get all hooks.json paths for a plugin (cache + marketplace source).
1980
- */
1981
- function get_all_hooks_paths(plugin_key, primary_path) {
1982
- const paths = [primary_path];
1983
- const at_index = plugin_key.lastIndexOf("@");
1984
- if (at_index > 0) {
1985
- const plugin_name = plugin_key.substring(0, at_index);
1986
- const marketplace_name = plugin_key.substring(at_index + 1);
1987
- paths.push(join(get_marketplaces_dir(), marketplace_name, "plugins", plugin_name, "hooks", "hooks.json"));
1988
- }
1989
- return [...new Set(paths)];
1990
- }
1991
- /**
1992
- * Disable a specific hook from a plugin.
1993
- * Removes from both cache and marketplace source hooks.json files.
1994
- */
1995
- async function disable_plugin_hook(entry) {
1996
- if (!entry.hooks_json_path || !entry.plugin_key) throw new Error("Not a plugin hook");
1997
- const disabled = await read_disabled_hooks();
1998
- disabled.push({
1999
- plugin_key: entry.plugin_key,
2000
- hooks_json_path: entry.hooks_json_path,
2001
- event: entry.event,
2002
- matcher: entry.matcher,
2003
- matcher_index: entry.matcher_index,
2004
- hook_index: entry.hook_index,
2005
- original_handler: entry.handler,
2006
- disabled_at: (/* @__PURE__ */ new Date()).toISOString()
2007
- });
2008
- await write_disabled_hooks(disabled);
2009
- const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
2010
- for (const hooks_path of all_paths) await remove_hook_from_file(hooks_path, entry.event, entry.handler);
2011
- }
2012
- /**
2013
- * Add a hook handler back into a hooks.json file.
2014
- */
2015
- async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
2016
- let hooks_data;
2017
- try {
2018
- const content = await readFile(hooks_path, "utf-8");
2019
- hooks_data = JSON.parse(content);
2020
- } catch {
2021
- hooks_data = { hooks: {} };
2022
- }
2023
- const hooks_obj = hooks_data.hooks || (hooks_data.hooks = {});
2024
- if (!hooks_obj[event]) hooks_obj[event] = [];
2025
- const matchers = hooks_obj[event];
2026
- let matcher = matchers.find((m) => (m.matcher || void 0) === matcher_pattern);
2027
- if (!matcher) {
2028
- matcher = { hooks: [] };
2029
- if (matcher_pattern) matcher.matcher = matcher_pattern;
2030
- matchers.push(matcher);
2031
- }
2032
- if (matcher.hooks.some((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt)) return;
2033
- matcher.hooks.push(handler);
2034
- await safe_json_write(hooks_path, hooks_data, " ");
2035
- }
2036
- /**
2037
- * Re-enable a previously disabled plugin hook.
2038
- * Restores to both cache and marketplace source hooks.json files.
2039
- */
2040
- async function enable_plugin_hook(disabled_entry) {
2041
- const all_paths = get_all_hooks_paths(disabled_entry.plugin_key, disabled_entry.hooks_json_path);
2042
- for (const hooks_path of all_paths) await add_hook_to_file(hooks_path, disabled_entry.event, disabled_entry.matcher, disabled_entry.original_handler);
2043
- await write_disabled_hooks((await read_disabled_hooks()).filter((d) => !(d.plugin_key === disabled_entry.plugin_key && d.event === disabled_entry.event && d.disabled_at === disabled_entry.disabled_at)));
2044
- }
2045
- /**
2046
- * Check if any previously disabled hooks have been restored (e.g. by marketplace update).
2047
- * Returns entries that were re-added and need to be re-disabled.
2048
- */
2049
- async function check_restored_hooks() {
2050
- const disabled = await read_disabled_hooks();
2051
- if (disabled.length === 0) return [];
2052
- const restored = [];
2053
- for (const entry of disabled) {
2054
- const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
2055
- let found = false;
2056
- for (const hooks_path of all_paths) {
2057
- let hooks_data;
2058
- try {
2059
- const content = await readFile(hooks_path, "utf-8");
2060
- hooks_data = JSON.parse(content);
2061
- } catch {
2062
- continue;
2063
- }
2064
- const matchers = (hooks_data.hooks || hooks_data)[entry.event];
2065
- if (!matchers) continue;
2066
- for (const m of matchers) {
2067
- if ((m.matcher || void 0) !== entry.matcher) continue;
2068
- if (m.hooks?.some((h) => h.type === entry.original_handler.type && (h.command === entry.original_handler.command || h.url === entry.original_handler.url || h.prompt === entry.original_handler.prompt))) {
2069
- found = true;
2070
- break;
2071
- }
2072
- }
2073
- if (found) break;
2074
- }
2075
- if (found) restored.push(entry);
2076
- }
2077
- return restored;
2078
- }
2079
- /**
2080
- * Re-disable hooks that were restored by a marketplace update.
2081
- */
2082
- async function redisable_restored_hooks(restored) {
2083
- let success = 0;
2084
- let failed = 0;
2085
- for (const entry of restored) try {
2086
- const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
2087
- let any_removed = false;
2088
- for (const hooks_path of all_paths) if (await remove_hook_from_file(hooks_path, entry.event, entry.original_handler)) any_removed = true;
2089
- if (any_removed) success++;
2090
- else failed++;
2091
- } catch {
2092
- failed++;
2093
- }
2094
- return {
2095
- success,
2096
- failed
2097
- };
2098
- }
2099
- //#endregion
2100
2043
  //#region src/commands/manage-hooks.ts
2101
2044
  function format_hook(entry) {
2102
2045
  const detail = entry.handler.command || entry.handler.url || entry.handler.prompt || "(unknown)";
@@ -2638,56 +2581,144 @@ function format_time_ago(date) {
2638
2581
  }
2639
2582
  //#endregion
2640
2583
  //#region src/core/profile.ts
2641
- async function load_profile(name) {
2584
+ function is_object(value) {
2585
+ return !!value && typeof value === "object" && !Array.isArray(value);
2586
+ }
2587
+ function string_record(value) {
2588
+ if (!is_object(value)) return void 0;
2589
+ const result = {};
2590
+ for (const [key, item] of Object.entries(value)) if (typeof item === "string") result[key] = item;
2591
+ return Object.keys(result).length > 0 ? result : void 0;
2592
+ }
2593
+ function boolean_record(value) {
2594
+ if (!is_object(value)) return void 0;
2595
+ const result = {};
2596
+ for (const [key, item] of Object.entries(value)) if (typeof item === "boolean") result[key] = item;
2597
+ return Object.keys(result).length > 0 ? result : void 0;
2598
+ }
2599
+ function string_array(value) {
2600
+ if (!Array.isArray(value)) return void 0;
2601
+ const result = value.filter((item) => typeof item === "string");
2602
+ return result.length > 0 ? result : void 0;
2603
+ }
2604
+ function parse_portable_server(value) {
2605
+ if (!is_object(value) || typeof value.name !== "string") return null;
2606
+ const transport = value.transport === "http" || value.transport === "sse" || value.transport === "stdio" ? value.transport : "stdio";
2607
+ const client_options = is_object(value.client_options) ? value.client_options : void 0;
2608
+ return {
2609
+ name: value.name,
2610
+ transport,
2611
+ ...typeof value.command === "string" ? { command: value.command } : {},
2612
+ ...string_array(value.args) ? { args: string_array(value.args) } : {},
2613
+ ...typeof value.url === "string" ? { url: value.url } : {},
2614
+ ...string_record(value.env) ? { env: string_record(value.env) } : {},
2615
+ ...string_record(value.headers) ? { headers: string_record(value.headers) } : {},
2616
+ ...typeof value.description === "string" ? { description: value.description } : {},
2617
+ ...typeof value.disabled === "boolean" ? { disabled: value.disabled } : {},
2618
+ ...client_options ? { client_options } : {}
2619
+ };
2620
+ }
2621
+ async function read_profile_json(name) {
2642
2622
  const profile_path = get_profile_path(name);
2643
2623
  try {
2644
2624
  await access(profile_path);
2645
- const content = await readFile(profile_path, "utf-8");
2646
- const parsed = JSON.parse(content);
2647
- let config;
2648
- if (parsed.mcpServers) config = validate_claude_config(parsed);
2649
- else if (!parsed.enabledPlugins) config = validate_claude_config({ mcpServers: parsed });
2650
- else config = validate_claude_config({ mcpServers: parsed.mcpServers || {} });
2651
- return {
2652
- config,
2653
- enabledPlugins: parsed.enabledPlugins
2654
- };
2625
+ return JSON.parse(await readFile(profile_path, "utf-8"));
2655
2626
  } catch (error) {
2656
2627
  if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new Error(`Profile '${name}' not found at ${profile_path}`);
2657
2628
  throw error;
2658
2629
  }
2659
2630
  }
2631
+ function legacy_profile_to_claude_config(parsed) {
2632
+ if (parsed.mcpServers) return validate_claude_config(parsed);
2633
+ if (!parsed.enabledPlugins && !parsed.plugins) return validate_claude_config({ mcpServers: parsed });
2634
+ return validate_claude_config({ mcpServers: parsed.mcpServers || {} });
2635
+ }
2636
+ function claude_config_to_portable(config) {
2637
+ return Object.entries(config.mcpServers || {}).map(([name, server]) => normalize_mcp_server(name, server));
2638
+ }
2639
+ function portable_to_claude_config(servers) {
2640
+ const mcpServers = {};
2641
+ for (const server of servers) {
2642
+ const config = {};
2643
+ if (server.transport !== "stdio") config.type = server.transport;
2644
+ if (server.command) config.command = server.command;
2645
+ if (server.args) config.args = server.args;
2646
+ if (server.url) config.url = server.url;
2647
+ if (server.env) config.env = server.env;
2648
+ if (server.headers) config.headers = server.headers;
2649
+ if (server.description) config.description = server.description;
2650
+ mcpServers[server.name] = config;
2651
+ }
2652
+ return validate_claude_config({ mcpServers });
2653
+ }
2654
+ async function load_portable_profile(name) {
2655
+ const parsed = await read_profile_json(name);
2656
+ const plugins = boolean_record(parsed.plugins) ?? boolean_record(parsed.enabledPlugins);
2657
+ if (Array.isArray(parsed.servers)) return {
2658
+ version: 2,
2659
+ servers: parsed.servers.map(parse_portable_server).filter((server) => !!server),
2660
+ ...plugins ? { plugins } : {},
2661
+ ...is_object(parsed.client_overrides) ? { client_overrides: parsed.client_overrides } : {}
2662
+ };
2663
+ return {
2664
+ version: 2,
2665
+ servers: claude_config_to_portable(legacy_profile_to_claude_config(parsed)),
2666
+ ...plugins ? { plugins } : {}
2667
+ };
2668
+ }
2660
2669
  async function apply_profile_to_claude(name) {
2661
- const profile = await load_profile(name);
2662
- await write_claude_config(profile.config);
2663
- const serverCount = Object.keys(profile.config.mcpServers || {}).length;
2670
+ const profile = await load_portable_profile(name);
2671
+ await write_claude_config(portable_to_claude_config(profile.servers));
2664
2672
  let pluginCount = 0;
2665
- if (profile.enabledPlugins) {
2666
- await write_claude_settings({ enabledPlugins: profile.enabledPlugins });
2667
- pluginCount = Object.keys(profile.enabledPlugins).length;
2673
+ if (profile.plugins) {
2674
+ await write_claude_settings({ enabledPlugins: profile.plugins });
2675
+ pluginCount = Object.keys(profile.plugins).length;
2668
2676
  }
2669
2677
  return {
2670
2678
  profile: name,
2671
- serverCount,
2672
- pluginCount
2679
+ serverCount: profile.servers.length,
2680
+ pluginCount,
2681
+ client: "claude-code",
2682
+ scope: "user"
2683
+ };
2684
+ }
2685
+ async function apply_profile_to_client(input) {
2686
+ const adapter = get_client_adapter(input.client);
2687
+ if (!adapter) throw new Error(`Invalid client: ${input.client}`);
2688
+ const location = resolve_client_location(adapter, input.scope, input.location);
2689
+ const profile = await load_portable_profile(input.name);
2690
+ const mutation = await replace_client_servers(adapter, location, profile.servers);
2691
+ let pluginCount = 0;
2692
+ if (adapter.id === "claude-code" && profile.plugins) {
2693
+ await write_claude_settings({ enabledPlugins: profile.plugins });
2694
+ pluginCount = Object.keys(profile.plugins).length;
2695
+ }
2696
+ return {
2697
+ profile: input.name,
2698
+ serverCount: profile.servers.length,
2699
+ pluginCount,
2700
+ client: adapter.id,
2701
+ scope: location.scope,
2702
+ location: mutation.location,
2703
+ ...mutation.backup_path ? { backup_path: mutation.backup_path } : {}
2673
2704
  };
2674
2705
  }
2675
2706
  async function list_profiles() {
2676
2707
  const profiles_dir = get_profiles_dir();
2677
2708
  try {
2678
2709
  await access(profiles_dir);
2679
- const json_files = (await readdir(profiles_dir)).filter((f) => f.endsWith(".json"));
2710
+ const files = (await readdir(profiles_dir)).filter((file) => file.endsWith(".json"));
2680
2711
  const profiles = [];
2681
- for (const file of json_files) try {
2712
+ for (const file of files) try {
2713
+ const name = file.replace(/\.json$/, "");
2682
2714
  const path = get_profile_path(file);
2683
- const content = await readFile(path, "utf-8");
2684
- const parsed = JSON.parse(content);
2685
- const servers = parsed.mcpServers || parsed;
2686
- const plugins = parsed.enabledPlugins || {};
2715
+ const parsed = JSON.parse(await readFile(path, "utf-8"));
2716
+ const servers = Array.isArray(parsed.servers) ? parsed.servers : parsed.mcpServers || parsed;
2717
+ const plugins = parsed.plugins || parsed.enabledPlugins || {};
2687
2718
  profiles.push({
2688
- name: file.replace(".json", ""),
2719
+ name,
2689
2720
  path,
2690
- serverCount: Object.keys(servers).length,
2721
+ serverCount: Array.isArray(servers) ? servers.length : Object.keys(servers).length,
2691
2722
  pluginCount: Object.keys(plugins).length
2692
2723
  });
2693
2724
  } catch {}
@@ -2696,21 +2727,43 @@ async function list_profiles() {
2696
2727
  return [];
2697
2728
  }
2698
2729
  }
2730
+ async function save_portable_profile(name, servers, plugins) {
2731
+ await ensure_directory_exists(get_profiles_dir());
2732
+ const profile_data = {
2733
+ version: 2,
2734
+ servers,
2735
+ ...plugins && Object.keys(plugins).length > 0 ? { plugins } : {}
2736
+ };
2737
+ await safe_json_write(get_profile_path(name), profile_data, 2);
2738
+ }
2699
2739
  async function save_profile(name) {
2700
2740
  const config = await read_claude_config();
2701
2741
  const settings = await read_claude_settings();
2702
- const servers = config.mcpServers || {};
2742
+ const servers = get_enabled_servers(config).map((server) => normalize_mcp_server(server.name, server));
2703
2743
  const plugins = settings.enabledPlugins || {};
2704
- const server_count = Object.keys(servers).length;
2705
- const plugin_count = Object.keys(plugins).length;
2706
- if (server_count === 0 && plugin_count === 0) throw new Error("No MCP servers or plugins configured to save");
2707
- await ensure_directory_exists(get_profiles_dir());
2708
- const profile_data = { mcpServers: servers };
2709
- if (plugin_count > 0) profile_data.enabledPlugins = plugins;
2710
- await safe_json_write(get_profile_path(name), profile_data, 2);
2744
+ if (servers.length === 0 && Object.keys(plugins).length === 0) throw new Error("No MCP servers or plugins configured to save");
2745
+ await save_portable_profile(name, servers, plugins);
2711
2746
  return {
2712
- serverCount: server_count,
2713
- pluginCount: plugin_count
2747
+ serverCount: servers.length,
2748
+ pluginCount: Object.keys(plugins).length
2749
+ };
2750
+ }
2751
+ async function save_profile_for_client(input) {
2752
+ const adapter = get_client_adapter(input.client);
2753
+ if (!adapter) throw new Error(`Invalid client: ${input.client}`);
2754
+ const location = resolve_client_location(adapter, input.scope, input.location);
2755
+ const servers = await adapter.readLocation(location);
2756
+ let plugins;
2757
+ if (adapter.id === "claude-code") plugins = (await read_claude_settings()).enabledPlugins;
2758
+ if (servers.length === 0 && !plugins) throw new Error("No MCP servers or plugins configured to save");
2759
+ await save_portable_profile(input.name, servers, plugins);
2760
+ return {
2761
+ profile: input.name,
2762
+ serverCount: servers.length,
2763
+ pluginCount: Object.keys(plugins || {}).length,
2764
+ client: adapter.id,
2765
+ scope: location.scope,
2766
+ location: location.path
2714
2767
  };
2715
2768
  }
2716
2769
  async function save_current_claude_profile(name) {
@@ -2718,7 +2771,9 @@ async function save_current_claude_profile(name) {
2718
2771
  return {
2719
2772
  profile: name,
2720
2773
  serverCount: counts.serverCount,
2721
- pluginCount: counts.pluginCount
2774
+ pluginCount: counts.pluginCount,
2775
+ client: "claude-code",
2776
+ scope: "user"
2722
2777
  };
2723
2778
  }
2724
2779
  //#endregion
@@ -2772,7 +2827,37 @@ async function create_claude_profile(name) {
2772
2827
  process.exit(1);
2773
2828
  }
2774
2829
  }
2775
- async function handle_load_claude_profile() {
2830
+ function sorted_client_adapters() {
2831
+ return [...client_adapters].sort((a, b) => b.label.localeCompare(a.label));
2832
+ }
2833
+ async function select_client_adapter() {
2834
+ const adapters = sorted_client_adapters();
2835
+ const client_id = await select({
2836
+ message: "Which MCP client?",
2837
+ options: adapters.map((adapter) => ({
2838
+ value: adapter.id,
2839
+ label: adapter.label
2840
+ })),
2841
+ initialValue: adapters[0]?.id
2842
+ });
2843
+ if (isCancel(client_id)) return null;
2844
+ return client_adapters.find((adapter) => adapter.id === client_id) ?? null;
2845
+ }
2846
+ async function select_client_location(adapter) {
2847
+ const locations = adapter.locations();
2848
+ if (locations.length === 1) return locations[0];
2849
+ const location_path = await select({
2850
+ message: `Which ${adapter.label} configuration?`,
2851
+ options: locations.map((location) => ({
2852
+ value: location.path,
2853
+ label: `${location.scope} — ${location.description}`,
2854
+ hint: location.path
2855
+ }))
2856
+ });
2857
+ if (isCancel(location_path)) return null;
2858
+ return locations.find((location) => location.path === location_path) ?? null;
2859
+ }
2860
+ async function handle_load_profile() {
2776
2861
  const profiles = await list_profiles();
2777
2862
  if (profiles.length === 0) {
2778
2863
  log.warn("No profiles found");
@@ -2792,12 +2877,23 @@ async function handle_load_claude_profile() {
2792
2877
  })
2793
2878
  });
2794
2879
  if (isCancel(profile_name)) return;
2795
- const result = await apply_profile_to_claude(profile_name);
2880
+ const adapter = await select_client_adapter();
2881
+ if (!adapter) return;
2882
+ const location = await select_client_location(adapter);
2883
+ if (!location) return;
2884
+ const result = await apply_profile_to_client({
2885
+ name: profile_name,
2886
+ client: adapter.id,
2887
+ scope: location.scope,
2888
+ location: location.path
2889
+ });
2796
2890
  const parts = [`${result.serverCount} servers`];
2797
- if (result.pluginCount > 0) parts.push(`${result.pluginCount} plugins`);
2798
- log.success(`Profile '${result.profile}' applied (${parts.join(", ")})`);
2891
+ if (result.pluginCount > 0) parts.push(`${result.pluginCount} Claude plugins`);
2892
+ log.success(`Profile '${result.profile}' applied to ${adapter.label}:${location.scope} (${parts.join(", ")})`);
2893
+ if (result.location) log.info(`Config: ${result.location}`);
2894
+ if (result.backup_path) log.info(`Backup: ${result.backup_path}`);
2799
2895
  }
2800
- async function handle_save_claude_profile() {
2896
+ async function handle_save_profile() {
2801
2897
  const name = await text({
2802
2898
  message: "Profile name:",
2803
2899
  placeholder: "e.g. database, web-dev, minimal",
@@ -2807,19 +2903,30 @@ async function handle_save_claude_profile() {
2807
2903
  }
2808
2904
  });
2809
2905
  if (isCancel(name)) return;
2810
- const result = await save_current_claude_profile(name);
2906
+ const adapter = await select_client_adapter();
2907
+ if (!adapter) return;
2908
+ const location = await select_client_location(adapter);
2909
+ if (!location) return;
2910
+ const result = await save_profile_for_client({
2911
+ name,
2912
+ client: adapter.id,
2913
+ scope: location.scope,
2914
+ location: location.path
2915
+ });
2811
2916
  const parts = [`${result.serverCount} servers`];
2812
- if (result.pluginCount > 0) parts.push(`${result.pluginCount} plugins`);
2813
- log.success(`Profile '${result.profile}' saved (${parts.join(", ")})`);
2917
+ if (result.pluginCount > 0) parts.push(`${result.pluginCount} Claude plugins`);
2918
+ log.success(`Profile '${result.profile}' saved from ${adapter.label}:${location.scope} (${parts.join(", ")})`);
2919
+ if (result.location) log.info(`Config: ${result.location}`);
2814
2920
  }
2815
2921
  async function handle_client_tools() {
2922
+ const adapters = sorted_client_adapters();
2816
2923
  const client_id = await select({
2817
2924
  message: "Which client?",
2818
- options: client_adapters.map((adapter) => ({
2925
+ options: adapters.map((adapter) => ({
2819
2926
  value: adapter.id,
2820
2927
  label: adapter.label
2821
2928
  })),
2822
- initialValue: "claude-code"
2929
+ initialValue: adapters[0]?.id
2823
2930
  });
2824
2931
  if (isCancel(client_id)) return;
2825
2932
  if (client_id !== "claude-code") {
@@ -2908,13 +3015,13 @@ async function main() {
2908
3015
  },
2909
3016
  {
2910
3017
  value: "load-profile",
2911
- label: "Load Claude Code profile",
2912
- hint: "Apply a saved Claude Code profile"
3018
+ label: "Load profile",
3019
+ hint: "Apply a saved profile to a selected client"
2913
3020
  },
2914
3021
  {
2915
3022
  value: "save-profile",
2916
- label: "Save Claude Code profile",
2917
- hint: "Save current Claude Code config as profile"
3023
+ label: "Save profile",
3024
+ hint: "Save selected client config as a portable profile"
2918
3025
  },
2919
3026
  {
2920
3027
  value: "backup",
@@ -2954,10 +3061,10 @@ async function main() {
2954
3061
  await restore_config();
2955
3062
  break;
2956
3063
  case "load-profile":
2957
- await handle_load_claude_profile();
3064
+ await handle_load_profile();
2958
3065
  break;
2959
3066
  case "save-profile":
2960
- await handle_save_claude_profile();
3067
+ await handle_save_profile();
2961
3068
  break;
2962
3069
  case "exit":
2963
3070
  outro("Goodbye!");
@@ -3008,12 +3115,12 @@ const SUBCOMMANDS = new Set([
3008
3115
  const arg = process.argv[2];
3009
3116
  if (arg && SUBCOMMANDS.has(arg) || arg === "--help" || arg === "-h" || !process.stdout.isTTY) {
3010
3117
  if (!arg && !process.stdout.isTTY) process.argv.push("--help");
3011
- import("./cli-CZOlaqoZ.js").then((m) => m.run());
3118
+ import("./cli-Dfein2li.js").then((m) => m.run());
3012
3119
  } else main().catch((error) => {
3013
3120
  console.error("Fatal error:", error);
3014
3121
  process.exit(1);
3015
3122
  });
3016
3123
  //#endregion
3017
- export { write_claude_settings as $, list_backups as A, preview_remove_client_server as B, mcp_reset_project_choices_via_cli as C, validate_plugin_via_cli as D, update_plugin_via_cli as E, add_client_server_config as F, build_command_preview as G, remove_client_server as H, get_client_adapter as I, build_enabled_plugins as J, build_json_change_preview as K, list_client_locations as L, read_server_registry as M, write_server_registry as N, add_server_to_registry as O, add_client_server as P, remove_hook as Q, preview_add_client_server as R, mcp_get_via_cli as S, uninstall_plugin_via_cli as T, resolve_client_location as U, preview_set_client_enabled_servers as V, set_client_server_enabled as W, get_all_plugins as X, get_all_hooks as Y, read_claude_settings as Z, marketplace_add_via_cli as _, run_skills_cli as a, marketplace_update_via_cli as b, disable_plugin_hook as c, redisable_restored_hooks as d, atomic_json_write as et, add_mcp_via_cli as f, install_plugin_via_cli as g, build_remove_args as h, save_current_claude_profile as i, list_plugin_backups as j, get_all_available_servers as k, enable_plugin_hook as l, build_add_json_args as m, list_profiles as n, restore_config_backup as nt, split_cli_list as o, build_add_args as p, add_hook as q, load_profile as r, check_restored_hooks as s, apply_profile_to_claude as t, list_config_backups as tt, read_disabled_hooks as u, marketplace_list_via_cli as v, remove_mcp_via_cli as w, mcp_add_json_via_cli as x, marketplace_remove_via_cli as y, preview_add_client_server_config as z };
3124
+ export { validate_mcp_server as $, list_plugin_backups as A, add_hook as B, redact_server_base as C, add_server_to_registry as D, redact_value as E, get_client_adapter as F, remove_hook as G, get_all_hooks as H, list_client_locations as I, find_server_in_scope as J, write_claude_settings as K, remove_client_server as L, write_server_registry as M, add_client_server as N, get_all_available_servers as O, add_client_server_config as P, validate_claude_config as Q, resolve_client_location as R, redact_server as S, redact_url as T, get_all_plugins as U, build_enabled_plugins as V, read_claude_settings as W, read_claude_config as X, get_enabled_servers_for_scope as Y, write_claude_config as Z, remove_mcp_via_cli as _, save_profile_for_client as a, validate_plugin_via_cli as b, add_mcp_via_cli as c, marketplace_list_via_cli as d, marketplace_remove_via_cli as f, mcp_reset_project_choices_via_cli as g, mcp_get_via_cli as h, save_current_claude_profile as i, read_server_registry as j, list_backups as k, install_plugin_via_cli as l, mcp_add_json_via_cli as m, apply_profile_to_client as n, run_skills_cli as o, marketplace_update_via_cli as p, detect_server_scope as q, list_profiles as r, split_cli_list as s, apply_profile_to_claude as t, marketplace_add_via_cli as u, uninstall_plugin_via_cli as v, redact_text as w, redact_portable_server as x, update_plugin_via_cli as y, set_client_server_enabled as z };
3018
3125
 
3019
3126
  //# sourceMappingURL=index.js.map