mcpick 0.0.22 → 0.0.23

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 (43) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -0
  3. package/dist/add-LJQa2my2.js +164 -0
  4. package/dist/add-json-TEdYweZ5.js +95 -0
  5. package/dist/{backup-bdg6dvsb.js → backup-kyS5IVIr.js} +5 -5
  6. package/dist/{cache-CSUcGdZP.js → cache-DTfzTsEE.js} +3 -3
  7. package/dist/{cli-avr5R1LO.js → cli-By-0nYNQ.js} +22 -21
  8. package/dist/{clients-CSQgqHzb.js → clients-qMozizys.js} +3 -3
  9. package/dist/{clone-CQ0skkT6.js → clone-BVhYjRGO.js} +5 -5
  10. package/dist/{config-BhX4eAgg.js → config-DzMmTJYL.js} +4 -4
  11. package/dist/{dev-CTDg5g-c.js → dev-Cst8WkQ-.js} +6 -6
  12. package/dist/disable-BaOs9lrm.js +83 -0
  13. package/dist/enable--3mjSmTq.js +84 -0
  14. package/dist/{get-l-eAJhBy.js → get-CjhNWyRj.js} +2 -2
  15. package/dist/{hooks-BWZ_Kgx3.js → hooks-DFmxgD0t.js} +3 -4
  16. package/dist/index.js +533 -68
  17. package/dist/{list-By--kltj.js → list-D5CkCXpP.js} +5 -5
  18. package/dist/{marketplace-DdiKDDKK.js → marketplace-C3EGyIG0.js} +3 -3
  19. package/dist/{output-BgN9Uuxf.js → output-HtT5HCof.js} +2 -2
  20. package/dist/{paths-6wrIM8yh.js → paths-BPISiJi4.js} +1 -1
  21. package/dist/{plugin-cache-DKcW8LGV.js → plugin-cache-BSgB42wa.js} +35 -16
  22. package/dist/{plugins-CsXE8AH4.js → plugins-Dn2mPFKm.js} +4 -5
  23. package/dist/{profile-DzGPsdsl.js → profile-Dq3ORPil.js} +4 -5
  24. package/dist/{redact-Dltz2gde.js → redact-wBMtzbno.js} +1 -1
  25. package/dist/{reload-C29-vuvy.js → reload-257iU7Z7.js} +2 -2
  26. package/dist/remove-26XFzkPd.js +87 -0
  27. package/dist/{reset-project-choices-Dhh4CxIC.js → reset-project-choices-D2F04LfC.js} +2 -2
  28. package/dist/{restore-BI8aiszM.js → restore-BYYsoNqF.js} +5 -6
  29. package/dist/rollback-CPdaME91.js +55 -0
  30. package/dist/{skills-DPBDmION.js → skills-DfWk9mpk.js} +2 -2
  31. package/dist/{validation-qWlF51fw.js → validation-xMlbgGCF.js} +1 -1
  32. package/package.json +4 -4
  33. package/.github/copilot-instructions.md +0 -50
  34. package/.github/workflows/ci.yml +0 -26
  35. package/.vscode/settings.json +0 -5
  36. package/CONTEXT.md +0 -49
  37. package/dist/add-Bok0qbXi.js +0 -112
  38. package/dist/add-json-C44vy2A_.js +0 -58
  39. package/dist/atomic-write-4lANmzsO.js +0 -26
  40. package/dist/disable-DLlOj7sc.js +0 -38
  41. package/dist/enable-CGFYYC2A.js +0 -39
  42. package/dist/remove-B5q4rQRU.js +0 -30
  43. package/dist/settings-CZR8bVfh.js +0 -201
package/dist/index.js CHANGED
@@ -1,16 +1,311 @@
1
1
  #!/usr/bin/env node
2
- import { _ as get_profiles_dir, 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-6wrIM8yh.js";
3
- import { r as validate_server_registry, t as validate_claude_config } from "./validation-qWlF51fw.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-BhX4eAgg.js";
5
- import { a as read_claude_settings, i as get_all_plugins, n as build_enabled_plugins, r as get_all_hooks, s as write_claude_settings } from "./settings-CZR8bVfh.js";
6
- import { a as redact_url, i as redact_text } from "./redact-Dltz2gde.js";
7
- 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-DKcW8LGV.js";
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-DzMmTJYL.js";
5
+ import { a as redact_url, i as redact_text } 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-BSgB42wa.js";
8
7
  import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, text } from "@clack/prompts";
9
- import { access, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
10
- import { dirname, join } from "node:path";
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";
11
11
  import { homedir } from "node:os";
12
12
  import { execFile } from "node:child_process";
13
13
  import { promisify } from "node:util";
14
+ //#region src/utils/safe-apply.ts
15
+ async function file_exists$1(path) {
16
+ try {
17
+ await access(path);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
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`;
27
+ }
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;
38
+ }
39
+ /**
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.
42
+ */
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;
63
+ }
64
+ }
65
+ async function list_config_backups() {
66
+ const backups_dir = get_backups_dir();
67
+ 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));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
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;
96
+ }
97
+ //#endregion
98
+ //#region src/utils/atomic-write.ts
99
+ /**
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)
105
+ */
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);
113
+ }
114
+ //#endregion
115
+ //#region src/core/settings.ts
116
+ async function read_claude_settings() {
117
+ const settings_path = get_claude_settings_path();
118
+ try {
119
+ await access(settings_path);
120
+ const content = await readFile(settings_path, "utf-8");
121
+ return JSON.parse(content);
122
+ } catch (error) {
123
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return {};
124
+ throw error;
125
+ }
126
+ }
127
+ async function write_claude_settings(updates) {
128
+ await atomic_json_write(get_claude_settings_path(), (existing) => {
129
+ for (const [key, value] of Object.entries(updates)) existing[key] = value;
130
+ return existing;
131
+ });
132
+ }
133
+ /**
134
+ * Parse enabledPlugins into structured list.
135
+ * Keys are in format "plugin-name@marketplace-name"
136
+ */
137
+ function get_all_plugins(settings) {
138
+ const enabled_plugins = settings.enabledPlugins || {};
139
+ return Object.entries(enabled_plugins).map(([key, enabled]) => {
140
+ const at_index = key.lastIndexOf("@");
141
+ return {
142
+ name: at_index > 0 ? key.substring(0, at_index) : key,
143
+ marketplace: at_index > 0 ? key.substring(at_index + 1) : "unknown",
144
+ enabled
145
+ };
146
+ });
147
+ }
148
+ /**
149
+ * Build the enabledPlugins record from a list of PluginInfo
150
+ */
151
+ function build_enabled_plugins(plugins) {
152
+ const result = {};
153
+ for (const plugin of plugins) {
154
+ const key = `${plugin.name}@${plugin.marketplace}`;
155
+ result[key] = plugin.enabled;
156
+ }
157
+ return result;
158
+ }
159
+ async function read_settings_file(path) {
160
+ try {
161
+ await access(path);
162
+ const content = await readFile(path, "utf-8");
163
+ return JSON.parse(content);
164
+ } catch {
165
+ return {};
166
+ }
167
+ }
168
+ function get_settings_paths() {
169
+ return [
170
+ {
171
+ scope: "user",
172
+ path: resolve(process.env.HOME || process.env.USERPROFILE || "", ".claude", "settings.json")
173
+ },
174
+ {
175
+ scope: "project",
176
+ path: resolve(process.cwd(), ".claude", "settings.json")
177
+ },
178
+ {
179
+ scope: "project-local",
180
+ path: resolve(process.cwd(), ".claude", "settings.local.json")
181
+ }
182
+ ];
183
+ }
184
+ /**
185
+ * Read all hooks across all scopes (settings + plugins), flattened for display.
186
+ */
187
+ async function get_all_hooks() {
188
+ const entries = [];
189
+ for (const { scope, path } of get_settings_paths()) {
190
+ const hooks = (await read_settings_file(path)).hooks;
191
+ if (!hooks) continue;
192
+ for (const [event, matchers] of Object.entries(hooks)) {
193
+ if (!Array.isArray(matchers)) continue;
194
+ for (let mi = 0; mi < matchers.length; mi++) {
195
+ const m = matchers[mi];
196
+ if (!m.hooks?.length) continue;
197
+ for (let hi = 0; hi < m.hooks.length; hi++) entries.push({
198
+ event,
199
+ matcher: m.matcher,
200
+ handler: m.hooks[hi],
201
+ scope,
202
+ source: scope,
203
+ matcher_index: mi,
204
+ hook_index: hi
205
+ });
206
+ }
207
+ }
208
+ }
209
+ const plugin_hooks = await get_all_plugin_hooks();
210
+ entries.push(...plugin_hooks);
211
+ return entries;
212
+ }
213
+ /**
214
+ * Scan all installed plugins for hooks.json and return flattened hook entries.
215
+ * Checks both cache and marketplace source paths since Claude Code reads from both.
216
+ */
217
+ async function get_all_plugin_hooks() {
218
+ const { read_installed_plugins } = await import("./plugin-cache-BSgB42wa.js").then((n) => n.s);
219
+ const { get_marketplaces_dir } = await import("./paths-BPISiJi4.js").then((n) => n.b);
220
+ const installed = await read_installed_plugins();
221
+ const entries = [];
222
+ const seen_hooks = /* @__PURE__ */ new Set();
223
+ for (const [plugin_key, installs] of Object.entries(installed.plugins)) {
224
+ if (!installs?.length) continue;
225
+ const install = installs[0];
226
+ const at_index = plugin_key.lastIndexOf("@");
227
+ const plugin_name = at_index > 0 ? plugin_key.substring(0, at_index) : plugin_key;
228
+ const marketplace_name = at_index > 0 ? plugin_key.substring(at_index + 1) : "";
229
+ const hooks_paths = [join(install.installPath, "hooks", "hooks.json")];
230
+ if (marketplace_name) hooks_paths.push(join(get_marketplaces_dir(), marketplace_name, "plugins", plugin_name, "hooks", "hooks.json"));
231
+ for (const hooks_path of hooks_paths) {
232
+ let hooks_data;
233
+ try {
234
+ const content = await readFile(hooks_path, "utf-8");
235
+ hooks_data = JSON.parse(content);
236
+ } catch {
237
+ continue;
238
+ }
239
+ const hooks = hooks_data.hooks || hooks_data;
240
+ for (const [event, matchers] of Object.entries(hooks)) {
241
+ if (!Array.isArray(matchers)) continue;
242
+ for (let mi = 0; mi < matchers.length; mi++) {
243
+ const m = matchers[mi];
244
+ if (!m.hooks?.length) continue;
245
+ for (let hi = 0; hi < m.hooks.length; hi++) {
246
+ const h = m.hooks[hi];
247
+ const dedup_key = `${plugin_key}:${event}:${h.type}:${h.command || h.url || h.prompt}`;
248
+ if (seen_hooks.has(dedup_key)) continue;
249
+ seen_hooks.add(dedup_key);
250
+ entries.push({
251
+ event,
252
+ matcher: m.matcher,
253
+ handler: h,
254
+ scope: "user",
255
+ source: "plugin",
256
+ matcher_index: mi,
257
+ hook_index: hi,
258
+ plugin_key,
259
+ hooks_json_path: hooks_path
260
+ });
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ return entries;
267
+ }
268
+ /**
269
+ * Remove a specific hook entry by scope/event/indices.
270
+ */
271
+ async function remove_hook(entry) {
272
+ const scope_path = get_settings_paths().find((s) => s.scope === entry.scope);
273
+ if (!scope_path) throw new Error(`Unknown scope: ${entry.scope}`);
274
+ await atomic_json_write(scope_path.path, (existing) => {
275
+ const hooks = existing.hooks;
276
+ if (!hooks) return existing;
277
+ const matchers = hooks[entry.event];
278
+ if (!matchers?.[entry.matcher_index]) return existing;
279
+ const matcher = matchers[entry.matcher_index];
280
+ matcher.hooks.splice(entry.hook_index, 1);
281
+ if (matcher.hooks.length === 0) matchers.splice(entry.matcher_index, 1);
282
+ if (matchers.length === 0) delete hooks[entry.event];
283
+ if (Object.keys(hooks).length === 0) delete existing.hooks;
284
+ return existing;
285
+ });
286
+ }
287
+ /**
288
+ * Add a hook to a specific scope.
289
+ */
290
+ async function add_hook(scope, event, matcher, handler) {
291
+ const scope_path = get_settings_paths().find((s) => s.scope === scope);
292
+ if (!scope_path) throw new Error(`Unknown scope: ${scope}`);
293
+ await atomic_json_write(scope_path.path, (existing) => {
294
+ if (!existing.hooks) existing.hooks = {};
295
+ const hooks = existing.hooks;
296
+ if (!hooks[event]) hooks[event] = [];
297
+ const matchers = hooks[event];
298
+ const existing_matcher = matchers.find((m) => (m.matcher || void 0) === matcher);
299
+ if (existing_matcher) existing_matcher.hooks.push(handler);
300
+ else {
301
+ const new_matcher = { hooks: [handler] };
302
+ if (matcher) new_matcher.matcher = matcher;
303
+ matchers.push(new_matcher);
304
+ }
305
+ return existing;
306
+ });
307
+ }
308
+ //#endregion
14
309
  //#region src/commands/backup.ts
15
310
  const MAX_BACKUPS = 10;
16
311
  async function backup_config() {
@@ -82,10 +377,7 @@ async function read_json_file(path) {
82
377
  }
83
378
  }
84
379
  async function write_json_file(path, data) {
85
- await mkdir(dirname(path), { recursive: true });
86
- const tmp_path = join(dirname(path), `.${Date.now()}.tmp`);
87
- await writeFile(tmp_path, JSON.stringify(data, null, 2), "utf-8");
88
- await rename(tmp_path, path);
380
+ await safe_json_write(path, data, 2);
89
381
  }
90
382
  function parse_json_or_jsonc(content) {
91
383
  try {
@@ -194,8 +486,8 @@ function infer_transport(config) {
194
486
  function normalize_mcp_server(name, config) {
195
487
  const transport = infer_transport(config);
196
488
  const url = typeof config.httpUrl === "string" ? config.httpUrl : typeof config.serverUrl === "string" ? config.serverUrl : typeof config.url === "string" ? config.url : void 0;
197
- const clientOptions = {};
198
- for (const [key, value] of Object.entries(config)) if (!client_options_to_skip.has(key)) clientOptions[key] = value;
489
+ const client_options = {};
490
+ for (const [key, value] of Object.entries(config)) if (!client_options_to_skip.has(key)) client_options[key] = value;
199
491
  const command_array = string_array(config.command);
200
492
  const command = typeof config.command === "string" ? config.command : command_array?.[0];
201
493
  const args = string_array(config.args) ?? command_array?.slice(1);
@@ -211,7 +503,7 @@ function normalize_mcp_server(name, config) {
211
503
  ...string_record(config.headers) ? { headers: string_record(config.headers) } : {},
212
504
  ...typeof config.description === "string" ? { description: config.description } : {},
213
505
  ...typeof disabled === "boolean" ? { disabled } : {},
214
- ...Object.keys(clientOptions).length > 0 ? { clientOptions } : {}
506
+ ...Object.keys(client_options).length > 0 ? { client_options } : {}
215
507
  };
216
508
  }
217
509
  function get_server_record(data, key) {
@@ -233,6 +525,18 @@ function set_server_enabled(config, enabled, mode) {
233
525
  }
234
526
  config.disabled = !enabled;
235
527
  }
528
+ function portable_to_json(server, mode) {
529
+ const result = { ...server.client_options };
530
+ if (server.transport !== "stdio") result.type = server.transport;
531
+ if (server.command) result.command = server.command;
532
+ if (server.args && server.args.length > 0) result.args = server.args;
533
+ if (server.url) result.url = server.url;
534
+ if (server.env) result.env = server.env;
535
+ if (server.headers) result.headers = server.headers;
536
+ if (server.description) result.description = server.description;
537
+ if (typeof server.disabled === "boolean") set_server_enabled(result, !server.disabled, mode);
538
+ return result;
539
+ }
236
540
  function create_json_adapter(options) {
237
541
  return {
238
542
  id: options.id,
@@ -247,13 +551,34 @@ function create_json_adapter(options) {
247
551
  async readLocation(location) {
248
552
  return read_server_map(await read_json_file(location.path), options.serverKey);
249
553
  },
250
- async writeEnabled(location, enabledNames) {
554
+ async writeEnabled(location, enabled_names) {
251
555
  const data = await read_json_file(location.path) ?? {};
252
556
  const servers = get_server_record(data, options.serverKey);
253
- const enabled = new Set(enabledNames);
557
+ const enabled = new Set(enabled_names);
254
558
  for (const [name, config] of Object.entries(servers)) set_server_enabled(config, enabled.has(name), options.disabledMode ?? "disabled");
255
559
  data[options.serverKey] = servers;
256
560
  await write_json_file(location.path, data);
561
+ },
562
+ async write_server(location, server) {
563
+ const data = await read_json_file(location.path) ?? {};
564
+ const servers = get_server_record(data, options.serverKey);
565
+ servers[server.name] = portable_to_json(server, options.disabledMode ?? "disabled");
566
+ data[options.serverKey] = servers;
567
+ await write_json_file(location.path, data);
568
+ },
569
+ async write_server_config(location, name, config) {
570
+ const data = await read_json_file(location.path) ?? {};
571
+ const servers = get_server_record(data, options.serverKey);
572
+ servers[name] = config;
573
+ data[options.serverKey] = servers;
574
+ await write_json_file(location.path, data);
575
+ },
576
+ async remove_server(location, name) {
577
+ const data = await read_json_file(location.path) ?? {};
578
+ const servers = get_server_record(data, options.serverKey);
579
+ delete servers[name];
580
+ data[options.serverKey] = servers;
581
+ await write_json_file(location.path, data);
257
582
  }
258
583
  };
259
584
  }
@@ -392,6 +717,37 @@ const client_adapters = [
392
717
  function get_client_adapter(id) {
393
718
  return client_adapters.find((adapter) => adapter.id === id) ?? null;
394
719
  }
720
+ function resolve_client_location(adapter, scope, path) {
721
+ let locations = adapter.locations();
722
+ if (path) locations = locations.filter((location) => location.path === path);
723
+ else if (scope) locations = locations.filter((location) => location.scope === scope);
724
+ if (locations.length === 1) return locations[0];
725
+ if (locations.length === 0) throw new Error(`No ${adapter.label} config location matches${scope ? ` scope '${scope}'` : ""}${path ? ` path '${path}'` : ""}.`);
726
+ throw new Error(`${adapter.label} has multiple matching config locations. Pass --location with one of: ${locations.map((location) => location.path).join(", ")}`);
727
+ }
728
+ async function add_client_server(adapter, location, server) {
729
+ if (!adapter.write_server) throw new Error(`${adapter.label} support cannot add servers yet.`);
730
+ await adapter.write_server(location, server);
731
+ }
732
+ async function add_client_server_config(adapter, location, name, config) {
733
+ if (!adapter.write_server_config) throw new Error(`${adapter.label} support cannot add servers yet.`);
734
+ await adapter.write_server_config(location, name, config);
735
+ }
736
+ async function remove_client_server(adapter, location, server_name) {
737
+ if (!adapter.remove_server) throw new Error(`${adapter.label} support cannot remove servers yet.`);
738
+ await adapter.remove_server(location, server_name);
739
+ }
740
+ async function set_client_server_enabled(adapter, location, server_name, enabled) {
741
+ if (!adapter.writeEnabled) throw new Error(`${adapter.label} support is read-only.`);
742
+ const servers = await adapter.readLocation(location);
743
+ const server = servers.find((candidate) => candidate.name === server_name);
744
+ if (!server) throw new Error(`Server '${server_name}' not found at ${location.path}.`);
745
+ const enabled_names = new Set(servers.filter((candidate) => candidate.disabled !== true).map((candidate) => candidate.name));
746
+ if (enabled) enabled_names.add(server.name);
747
+ else enabled_names.delete(server.name);
748
+ await adapter.writeEnabled(location, [...enabled_names]);
749
+ return enabled_names.size;
750
+ }
395
751
  async function list_client_locations() {
396
752
  return await Promise.all(client_adapters.flatMap((adapter) => adapter.locations().map(async (location) => ({
397
753
  client: adapter.id,
@@ -421,7 +777,7 @@ async function read_server_registry() {
421
777
  async function write_server_registry(registry) {
422
778
  const registry_path = get_server_registry_path();
423
779
  await ensure_directory_exists(get_mcpick_dir());
424
- await writeFile(registry_path, JSON.stringify(registry, null, 2), "utf-8");
780
+ await atomic_json_write(registry_path, () => registry);
425
781
  }
426
782
  async function add_server_to_registry(server) {
427
783
  const registry = await read_server_registry();
@@ -431,7 +787,7 @@ async function add_server_to_registry(server) {
431
787
  await write_server_registry(registry);
432
788
  }
433
789
  async function get_all_available_servers() {
434
- const { get_enabled_servers, read_claude_config } = await import("./config-BhX4eAgg.js").then((n) => n.t);
790
+ const { get_enabled_servers, read_claude_config } = await import("./config-DzMmTJYL.js").then((n) => n.t);
435
791
  const registry = await read_server_registry();
436
792
  const config_servers = get_enabled_servers(await read_claude_config());
437
793
  const config_by_name = new Map(config_servers.map((s) => [s.name, s]));
@@ -566,17 +922,26 @@ async function add_mcp_via_cli(server, scope) {
566
922
  /**
567
923
  * Remove an MCP server using Claude CLI
568
924
  */
569
- async function remove_mcp_via_cli(name) {
925
+ function build_remove_args(name, scope) {
926
+ return scope ? [
927
+ "mcp",
928
+ "remove",
929
+ name,
930
+ "--scope",
931
+ scope
932
+ ] : [
933
+ "mcp",
934
+ "remove",
935
+ name
936
+ ];
937
+ }
938
+ async function remove_mcp_via_cli(name, scope) {
570
939
  if (!await check_claude_cli()) return {
571
940
  success: false,
572
941
  error: "Claude CLI not found. Please install Claude Code CLI."
573
942
  };
574
943
  try {
575
- await run_claude([
576
- "mcp",
577
- "remove",
578
- name
579
- ]);
944
+ await run_claude(build_remove_args(name, scope));
580
945
  return { success: true };
581
946
  } catch (error) {
582
947
  return {
@@ -657,77 +1022,176 @@ async function update_plugin_via_cli(key, scope = "user") {
657
1022
  };
658
1023
  }
659
1024
  }
1025
+ const GITHUB_OWNER_PATTERN = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?";
1026
+ const GITHUB_REPO_PATTERN = "[A-Za-z0-9._-]+";
660
1027
  /**
661
1028
  * Extract GitHub owner/repo from various source formats.
662
1029
  * Returns null if not a recognizable GitHub reference.
663
1030
  */
664
1031
  function parse_github_repo(source) {
665
- const https_match = source.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/);
666
- if (https_match) return {
667
- owner: https_match[1],
668
- repo: https_match[2]
669
- };
670
- const ssh_match = source.match(/^git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/);
1032
+ const trimmed = source.trim();
1033
+ try {
1034
+ const url = new URL(trimmed);
1035
+ if (url.hostname !== "github.com") return null;
1036
+ const [owner, raw_repo, ...rest] = url.pathname.split("/").filter(Boolean);
1037
+ if (!owner || !raw_repo || rest.length > 0) return null;
1038
+ const repo = raw_repo.replace(/\.git$/, "");
1039
+ if (!is_valid_github_repo(owner, repo)) return null;
1040
+ return {
1041
+ owner,
1042
+ repo,
1043
+ kind: "https"
1044
+ };
1045
+ } catch {}
1046
+ const ssh_match = trimmed.match(new RegExp(`^git@github\\.com:(${GITHUB_OWNER_PATTERN})/(${GITHUB_REPO_PATTERN})(?:\\.git)?$`));
671
1047
  if (ssh_match) return {
672
1048
  owner: ssh_match[1],
673
- repo: ssh_match[2]
1049
+ repo: ssh_match[2].replace(/\.git$/, ""),
1050
+ kind: "ssh"
674
1051
  };
675
- const shorthand_match = source.match(/^([^/\s]+)\/([^/\s]+)$/);
1052
+ const shorthand_match = trimmed.match(new RegExp(`^(${GITHUB_OWNER_PATTERN})/(${GITHUB_REPO_PATTERN})$`));
676
1053
  if (shorthand_match) return {
677
1054
  owner: shorthand_match[1],
678
- repo: shorthand_match[2]
1055
+ repo: shorthand_match[2],
1056
+ kind: "shorthand"
679
1057
  };
680
1058
  return null;
681
1059
  }
682
- /**
683
- * Validate that a GitHub repository exists and is accessible.
684
- * Returns an error message if validation fails, null if OK.
685
- */
686
- async function validate_github_repo(owner, repo) {
1060
+ function is_valid_github_repo(owner, repo) {
1061
+ return new RegExp(`^${GITHUB_OWNER_PATTERN}$`).test(owner) && new RegExp(`^${GITHUB_REPO_PATTERN}$`).test(repo);
1062
+ }
1063
+ function build_marketplace_add_args(source) {
1064
+ return [
1065
+ "plugin",
1066
+ "marketplace",
1067
+ "add",
1068
+ source
1069
+ ];
1070
+ }
1071
+ function get_github_repo_id(ref) {
1072
+ return `${ref.owner}/${ref.repo}`;
1073
+ }
1074
+ function get_github_clone_urls(ref) {
1075
+ const repo_id = get_github_repo_id(ref);
1076
+ const https_url = `https://github.com/${repo_id}.git`;
1077
+ const ssh_url = `git@github.com:${repo_id}.git`;
1078
+ if (ref.kind === "https") return [https_url];
1079
+ if (ref.kind === "ssh") return [ssh_url];
1080
+ return [https_url, ssh_url];
1081
+ }
1082
+ function get_process_error_output(error) {
1083
+ const process_error = error;
1084
+ return redact_text([
1085
+ process_error.message,
1086
+ process_error.stderr?.toString(),
1087
+ process_error.stdout?.toString()
1088
+ ].filter(Boolean).join("\n"));
1089
+ }
1090
+ async function can_access_with_git(url) {
1091
+ try {
1092
+ await exec_file_async$1("git", [
1093
+ "ls-remote",
1094
+ "--exit-code",
1095
+ url,
1096
+ "HEAD"
1097
+ ], {
1098
+ env: {
1099
+ ...process.env,
1100
+ GIT_TERMINAL_PROMPT: "0",
1101
+ GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
1102
+ },
1103
+ timeout: 15e3
1104
+ });
1105
+ return null;
1106
+ } catch (error) {
1107
+ return get_process_error_output(error);
1108
+ }
1109
+ }
1110
+ async function can_view_with_gh(repo_id) {
687
1111
  try {
688
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
1112
+ await exec_file_async$1("gh", [
1113
+ "repo",
1114
+ "view",
1115
+ repo_id,
1116
+ "--json",
1117
+ "nameWithOwner"
1118
+ ], { timeout: 15e3 });
1119
+ return true;
1120
+ } catch (error) {
1121
+ const message = get_process_error_output(error).toLowerCase();
1122
+ if (message.includes("could not resolve") || message.includes("not found") || message.includes("repository not found")) return false;
1123
+ return null;
1124
+ }
1125
+ }
1126
+ async function get_github_api_status(repo_id) {
1127
+ try {
1128
+ return (await fetch(`https://api.github.com/repos/${repo_id}`, {
689
1129
  method: "GET",
690
1130
  headers: { Accept: "application/vnd.github.v3+json" }
691
- });
692
- if (response.status === 200) return null;
693
- if (response.status === 404) return `Repository '${owner}/${repo}' not found on GitHub. Check the name or ensure it's not private.`;
694
- if (response.status === 403) return `Access denied for '${owner}/${repo}'. The repository may be private — configure a GitHub token or use SSH.`;
695
- return `GitHub API returned status ${response.status} for '${owner}/${repo}'.`;
1131
+ })).status;
696
1132
  } catch {
697
1133
  return null;
698
1134
  }
699
1135
  }
1136
+ /**
1137
+ * Validate that a GitHub repository can be accessed by git without cloning it.
1138
+ * Returns an error message if validation fails, null if OK.
1139
+ */
1140
+ async function validate_github_repo(ref) {
1141
+ const repo_id = get_github_repo_id(ref);
1142
+ const git_errors = [];
1143
+ for (const url of get_github_clone_urls(ref)) {
1144
+ const git_error = await can_access_with_git(url);
1145
+ if (!git_error) return null;
1146
+ git_errors.push(git_error);
1147
+ }
1148
+ const gh_visible = await can_view_with_gh(repo_id);
1149
+ if (gh_visible === false) return `Repository '${repo_id}' not found or not accessible with your GitHub account.`;
1150
+ const api_status = await get_github_api_status(repo_id);
1151
+ if (api_status === 404 && gh_visible !== true) return `Repository '${repo_id}' not found or private/inaccessible. Check the name, sign in with 'gh auth login', or use an SSH URL with access.`;
1152
+ if (api_status === 403 && gh_visible !== true) return `Unable to validate '${repo_id}' because GitHub API access was denied or rate-limited. Git access also failed; check your credentials.`;
1153
+ return format_git_access_error(ref, git_errors.join("\n"));
1154
+ }
1155
+ function format_git_access_error(ref, message) {
1156
+ const repo_id = get_github_repo_id(ref);
1157
+ const lower = message.toLowerCase();
1158
+ if (ref.kind === "shorthand" && (lower.includes("could not read username") || lower.includes("authentication failed")) && (lower.includes("permission denied (publickey)") || lower.includes("ssh authentication"))) return `Repository '${repo_id}' exists, but Git cannot access it over HTTPS or SSH. Run 'gh auth login' and 'gh auth setup-git', or configure an SSH key with GitHub.`;
1159
+ if (ref.kind === "https" || lower.includes("https authentication failed") || lower.includes("could not read username") || lower.includes("authentication failed")) return `Repository '${repo_id}' exists, but HTTPS Git authentication failed. Run 'gh auth login' and 'gh auth setup-git', configure a Git credential helper, or use git@github.com:${repo_id}.git.`;
1160
+ if (ref.kind === "ssh" || lower.includes("permission denied (publickey)") || lower.includes("ssh authentication")) return `SSH authentication failed for '${repo_id}'. Configure an SSH key with GitHub or use https://github.com/${repo_id}.git with a configured Git credential helper.`;
1161
+ return `Unable to access GitHub repository '${repo_id}' with git. Check that the repository exists and that your HTTPS or SSH credentials can clone it.`;
1162
+ }
700
1163
  async function marketplace_add_via_cli(source) {
701
1164
  if (!await check_claude_cli()) return {
702
1165
  success: false,
703
1166
  error: "Claude CLI not found. Please install Claude Code CLI."
704
1167
  };
705
1168
  const gh = parse_github_repo(source);
706
- const is_shorthand = gh && !source.startsWith("http") && !source.startsWith("git@");
707
- if (gh && is_shorthand) {
708
- const validation_error = await validate_github_repo(gh.owner, gh.repo);
1169
+ if (gh) {
1170
+ const validation_error = await validate_github_repo(gh);
709
1171
  if (validation_error) return {
710
1172
  success: false,
711
1173
  error: validation_error
712
1174
  };
713
1175
  }
714
1176
  try {
715
- await run_claude([
716
- "plugin",
717
- "marketplace",
718
- "add",
719
- source
720
- ]);
1177
+ await run_claude(build_marketplace_add_args(source));
721
1178
  return { success: true };
722
1179
  } catch (error) {
723
1180
  const message = get_redacted_error_message(error);
724
- if (message.includes("SSH") || message.includes("Permission denied (publickey)")) return {
725
- success: false,
726
- error: `SSH authentication failed for '${source}'. Either:\n - Configure SSH keys: https://docs.github.com/en/authentication/connecting-to-github-with-ssh\n - Use HTTPS URL instead: https://github.com/${gh ? `${gh.owner}/${gh.repo}` : source}`
727
- };
728
- if (message.includes("not found") || message.includes("does not exist")) return {
1181
+ const lower_message = message.toLowerCase();
1182
+ if (gh) {
1183
+ if (lower_message.includes("https authentication failed") || lower_message.includes("could not read username") || lower_message.includes("authentication failed")) return {
1184
+ success: false,
1185
+ error: format_git_access_error(gh, message)
1186
+ };
1187
+ if (message.includes("SSH") || message.includes("Permission denied (publickey)")) return {
1188
+ success: false,
1189
+ error: format_git_access_error(gh, message)
1190
+ };
1191
+ }
1192
+ if (lower_message.includes("not found") || lower_message.includes("does not exist")) return {
729
1193
  success: false,
730
- error: `Repository '${source}' not found. Check the name and your access permissions.`
1194
+ error: gh ? `Repository '${get_github_repo_id(gh)}' not found or not accessible with your GitHub account.` : `Repository '${source}' not found. Check the name and your access permissions.`
731
1195
  };
732
1196
  return {
733
1197
  success: false,
@@ -1053,7 +1517,7 @@ async function edit_claude_config() {
1053
1517
  }
1054
1518
  }
1055
1519
  for (const name of servers_to_remove) {
1056
- const result = await remove_mcp_via_cli(name);
1520
+ const result = await remove_mcp_via_cli(name, scope);
1057
1521
  if (!result.success) {
1058
1522
  error_count++;
1059
1523
  log.warn(`Failed to remove ${name}: ${result.error}`);
@@ -1358,7 +1822,7 @@ async function read_disabled_hooks() {
1358
1822
  }
1359
1823
  async function write_disabled_hooks(entries) {
1360
1824
  await ensure_directory_exists(get_mcpick_dir());
1361
- await writeFile(get_disabled_hooks_path(), JSON.stringify(entries, null, " "), "utf-8");
1825
+ await safe_json_write(get_disabled_hooks_path(), entries, " ");
1362
1826
  }
1363
1827
  /**
1364
1828
  * Remove a specific hook handler from a hooks.json file by matching the handler.
@@ -1387,7 +1851,7 @@ async function remove_hook_from_file(hooks_path, event, handler) {
1387
1851
  }
1388
1852
  if (!removed) return false;
1389
1853
  if (matchers.length === 0) delete hooks_obj[event];
1390
- await writeFile(hooks_path, JSON.stringify(hooks_data, null, " "), "utf-8");
1854
+ await safe_json_write(hooks_path, hooks_data, " ");
1391
1855
  return true;
1392
1856
  }
1393
1857
  /**
@@ -1446,7 +1910,7 @@ async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
1446
1910
  }
1447
1911
  if (matcher.hooks.some((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt)) return;
1448
1912
  matcher.hooks.push(handler);
1449
- await writeFile(hooks_path, JSON.stringify(hooks_data, null, " "), "utf-8");
1913
+ await safe_json_write(hooks_path, hooks_data, " ");
1450
1914
  }
1451
1915
  /**
1452
1916
  * Re-enable a previously disabled plugin hook.
@@ -2107,7 +2571,7 @@ async function save_profile(name) {
2107
2571
  await ensure_directory_exists(get_profiles_dir());
2108
2572
  const profile_data = { mcpServers: servers };
2109
2573
  if (plugin_count > 0) profile_data.enabledPlugins = plugins;
2110
- await writeFile(get_profile_path(name), JSON.stringify(profile_data, null, 2), "utf-8");
2574
+ await safe_json_write(get_profile_path(name), profile_data, 2);
2111
2575
  return {
2112
2576
  serverCount: server_count,
2113
2577
  pluginCount: plugin_count
@@ -2402,17 +2866,18 @@ const SUBCOMMANDS = new Set([
2402
2866
  "cache",
2403
2867
  "dev",
2404
2868
  "marketplace",
2405
- "reload"
2869
+ "reload",
2870
+ "rollback"
2406
2871
  ]);
2407
2872
  const arg = process.argv[2];
2408
2873
  if (arg && SUBCOMMANDS.has(arg) || arg === "--help" || arg === "-h" || !process.stdout.isTTY) {
2409
2874
  if (!arg && !process.stdout.isTTY) process.argv.push("--help");
2410
- import("./cli-avr5R1LO.js").then((m) => m.run());
2875
+ import("./cli-By-0nYNQ.js").then((m) => m.run());
2411
2876
  } else main().catch((error) => {
2412
2877
  console.error("Fatal error:", error);
2413
2878
  process.exit(1);
2414
2879
  });
2415
2880
  //#endregion
2416
- export { get_client_adapter as A, validate_plugin_via_cli as C, list_plugin_backups as D, list_backups as E, read_server_registry as O, update_plugin_via_cli as S, get_all_available_servers as T, mcp_add_json_via_cli as _, split_cli_list as a, remove_mcp_via_cli as b, enable_plugin_hook as c, add_mcp_via_cli as d, install_plugin_via_cli as f, marketplace_update_via_cli as g, marketplace_remove_via_cli as h, run_skills_cli as i, list_client_locations as j, write_server_registry as k, read_disabled_hooks as l, marketplace_list_via_cli as m, load_profile as n, check_restored_hooks as o, marketplace_add_via_cli as p, save_profile as r, disable_plugin_hook as s, list_profiles as t, redisable_restored_hooks as u, mcp_get_via_cli as v, add_server_to_registry as w, uninstall_plugin_via_cli as x, mcp_reset_project_choices_via_cli as y };
2881
+ export { add_client_server as A, get_all_plugins as B, validate_plugin_via_cli as C, list_plugin_backups as D, list_backups as E, resolve_client_location as F, list_config_backups as G, remove_hook as H, set_client_server_enabled as I, restore_config_backup as K, add_hook as L, get_client_adapter as M, list_client_locations as N, read_server_registry as O, remove_client_server as P, build_enabled_plugins as R, update_plugin_via_cli as S, get_all_available_servers as T, write_claude_settings as U, read_claude_settings as V, atomic_json_write as W, mcp_add_json_via_cli as _, split_cli_list as a, remove_mcp_via_cli as b, enable_plugin_hook as c, add_mcp_via_cli as d, install_plugin_via_cli as f, marketplace_update_via_cli as g, marketplace_remove_via_cli as h, run_skills_cli as i, add_client_server_config as j, write_server_registry as k, read_disabled_hooks as l, marketplace_list_via_cli as m, load_profile as n, check_restored_hooks as o, marketplace_add_via_cli as p, save_profile as r, disable_plugin_hook as s, list_profiles as t, redisable_restored_hooks as u, mcp_get_via_cli as v, add_server_to_registry as w, uninstall_plugin_via_cli as x, mcp_reset_project_choices_via_cli as y, get_all_hooks as z };
2417
2882
 
2418
2883
  //# sourceMappingURL=index.js.map