mcpick 0.0.22 → 0.0.24
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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -0
- package/dist/add-Qzd8i-5k.js +184 -0
- package/dist/add-json-DGmsjB0O.js +115 -0
- package/dist/{backup-bdg6dvsb.js → backup-C7fvikFw.js} +5 -5
- package/dist/{cache-CSUcGdZP.js → cache-D3jjh5dD.js} +3 -3
- package/dist/{cli-avr5R1LO.js → cli-CZOlaqoZ.js} +22 -21
- package/dist/{clients-CSQgqHzb.js → clients-Bh93TGP4.js} +3 -3
- package/dist/{clone-CQ0skkT6.js → clone-MI8jJhTz.js} +6 -6
- package/dist/{config-BhX4eAgg.js → config-DE58Fik_.js} +4 -4
- package/dist/{dev-CTDg5g-c.js → dev-51esdZG9.js} +6 -6
- package/dist/disable-csYAn2Vk.js +106 -0
- package/dist/dry-run-XQ32fxPT.js +20 -0
- package/dist/enable-B5GbmhL-.js +107 -0
- package/dist/{get-l-eAJhBy.js → get-DacRZmwv.js} +3 -3
- package/dist/{hooks-BWZ_Kgx3.js → hooks-C_x49qap.js} +3 -4
- package/dist/index.js +717 -116
- package/dist/{list-By--kltj.js → list-BeBtsiae.js} +5 -5
- package/dist/{marketplace-DdiKDDKK.js → marketplace-BDC2YtvT.js} +4 -4
- package/dist/{output-BgN9Uuxf.js → output-HtT5HCof.js} +2 -2
- package/dist/{paths-6wrIM8yh.js → paths-BPISiJi4.js} +1 -1
- package/dist/{plugin-cache-DKcW8LGV.js → plugin-cache-DmLbh89d.js} +35 -16
- package/dist/{plugins-CsXE8AH4.js → plugins-Bkw-SKkZ.js} +4 -5
- package/dist/profile-DwJTVXiz.js +161 -0
- package/dist/{redact-Dltz2gde.js → redact-wBMtzbno.js} +1 -1
- package/dist/{reload-C29-vuvy.js → reload-Bl1mYK1I.js} +2 -2
- package/dist/remove-BSHgva79.js +107 -0
- package/dist/{reset-project-choices-Dhh4CxIC.js → reset-project-choices-BNLus9J9.js} +3 -3
- package/dist/{restore-BI8aiszM.js → restore-YisgARhc.js} +5 -6
- package/dist/rollback-GR1RkpXW.js +55 -0
- package/dist/{skills-DPBDmION.js → skills-rDTDqqZA.js} +3 -3
- package/dist/{validation-qWlF51fw.js → validation-xMlbgGCF.js} +1 -1
- package/package.json +4 -4
- package/.github/copilot-instructions.md +0 -50
- package/.github/workflows/ci.yml +0 -26
- package/.vscode/settings.json +0 -5
- package/CONTEXT.md +0 -49
- package/dist/add-Bok0qbXi.js +0 -112
- package/dist/add-json-C44vy2A_.js +0 -58
- package/dist/atomic-write-4lANmzsO.js +0 -26
- package/dist/disable-DLlOj7sc.js +0 -38
- package/dist/enable-CGFYYC2A.js +0 -39
- package/dist/profile-DzGPsdsl.js +0 -120
- package/dist/remove-B5q4rQRU.js +0 -30
- 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-
|
|
3
|
-
import { r as validate_server_registry, t as validate_claude_config } from "./validation-
|
|
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-
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
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-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";
|
|
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-DmLbh89d.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() {
|
|
@@ -49,6 +344,43 @@ async function cleanup_old_backups(prefix) {
|
|
|
49
344
|
} catch {}
|
|
50
345
|
}
|
|
51
346
|
//#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
|
|
52
384
|
//#region src/core/client-config.ts
|
|
53
385
|
const client_options_to_skip = new Set([
|
|
54
386
|
"name",
|
|
@@ -82,10 +414,7 @@ async function read_json_file(path) {
|
|
|
82
414
|
}
|
|
83
415
|
}
|
|
84
416
|
async function write_json_file(path, data) {
|
|
85
|
-
await
|
|
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);
|
|
417
|
+
await safe_json_write(path, data, 2);
|
|
89
418
|
}
|
|
90
419
|
function parse_json_or_jsonc(content) {
|
|
91
420
|
try {
|
|
@@ -194,8 +523,8 @@ function infer_transport(config) {
|
|
|
194
523
|
function normalize_mcp_server(name, config) {
|
|
195
524
|
const transport = infer_transport(config);
|
|
196
525
|
const url = typeof config.httpUrl === "string" ? config.httpUrl : typeof config.serverUrl === "string" ? config.serverUrl : typeof config.url === "string" ? config.url : void 0;
|
|
197
|
-
const
|
|
198
|
-
for (const [key, value] of Object.entries(config)) if (!client_options_to_skip.has(key))
|
|
526
|
+
const client_options = {};
|
|
527
|
+
for (const [key, value] of Object.entries(config)) if (!client_options_to_skip.has(key)) client_options[key] = value;
|
|
199
528
|
const command_array = string_array(config.command);
|
|
200
529
|
const command = typeof config.command === "string" ? config.command : command_array?.[0];
|
|
201
530
|
const args = string_array(config.args) ?? command_array?.slice(1);
|
|
@@ -211,7 +540,7 @@ function normalize_mcp_server(name, config) {
|
|
|
211
540
|
...string_record(config.headers) ? { headers: string_record(config.headers) } : {},
|
|
212
541
|
...typeof config.description === "string" ? { description: config.description } : {},
|
|
213
542
|
...typeof disabled === "boolean" ? { disabled } : {},
|
|
214
|
-
...Object.keys(
|
|
543
|
+
...Object.keys(client_options).length > 0 ? { client_options } : {}
|
|
215
544
|
};
|
|
216
545
|
}
|
|
217
546
|
function get_server_record(data, key) {
|
|
@@ -233,7 +562,32 @@ function set_server_enabled(config, enabled, mode) {
|
|
|
233
562
|
}
|
|
234
563
|
config.disabled = !enabled;
|
|
235
564
|
}
|
|
565
|
+
function portable_to_json(server, mode) {
|
|
566
|
+
const result = { ...server.client_options };
|
|
567
|
+
if (server.transport !== "stdio") result.type = server.transport;
|
|
568
|
+
if (server.command) result.command = server.command;
|
|
569
|
+
if (server.args && server.args.length > 0) result.args = server.args;
|
|
570
|
+
if (server.url) result.url = server.url;
|
|
571
|
+
if (server.env) result.env = server.env;
|
|
572
|
+
if (server.headers) result.headers = server.headers;
|
|
573
|
+
if (server.description) result.description = server.description;
|
|
574
|
+
if (typeof server.disabled === "boolean") set_server_enabled(result, !server.disabled, mode);
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
236
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
|
+
});
|
|
590
|
+
}
|
|
237
591
|
return {
|
|
238
592
|
id: options.id,
|
|
239
593
|
label: options.label,
|
|
@@ -247,13 +601,67 @@ function create_json_adapter(options) {
|
|
|
247
601
|
async readLocation(location) {
|
|
248
602
|
return read_server_map(await read_json_file(location.path), options.serverKey);
|
|
249
603
|
},
|
|
250
|
-
async writeEnabled(location,
|
|
251
|
-
const data = await
|
|
604
|
+
async writeEnabled(location, enabled_names) {
|
|
605
|
+
const data = await read_data(location);
|
|
252
606
|
const servers = get_server_record(data, options.serverKey);
|
|
253
|
-
const enabled = new Set(
|
|
607
|
+
const enabled = new Set(enabled_names);
|
|
254
608
|
for (const [name, config] of Object.entries(servers)) set_server_enabled(config, enabled.has(name), options.disabledMode ?? "disabled");
|
|
255
609
|
data[options.serverKey] = servers;
|
|
256
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);
|
|
620
|
+
},
|
|
621
|
+
async write_server(location, server) {
|
|
622
|
+
const data = await read_data(location);
|
|
623
|
+
const servers = get_server_record(data, options.serverKey);
|
|
624
|
+
servers[server.name] = portable_to_json(server, options.disabledMode ?? "disabled");
|
|
625
|
+
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);
|
|
635
|
+
},
|
|
636
|
+
async write_server_config(location, name, config) {
|
|
637
|
+
const data = await read_data(location);
|
|
638
|
+
const servers = get_server_record(data, options.serverKey);
|
|
639
|
+
servers[name] = config;
|
|
640
|
+
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);
|
|
650
|
+
},
|
|
651
|
+
async remove_server(location, name) {
|
|
652
|
+
const data = await read_data(location);
|
|
653
|
+
const servers = get_server_record(data, options.serverKey);
|
|
654
|
+
delete servers[name];
|
|
655
|
+
data[options.serverKey] = servers;
|
|
656
|
+
await write_json_file(location.path, data);
|
|
657
|
+
},
|
|
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);
|
|
257
665
|
}
|
|
258
666
|
};
|
|
259
667
|
}
|
|
@@ -392,6 +800,63 @@ const client_adapters = [
|
|
|
392
800
|
function get_client_adapter(id) {
|
|
393
801
|
return client_adapters.find((adapter) => adapter.id === id) ?? null;
|
|
394
802
|
}
|
|
803
|
+
function resolve_client_location(adapter, scope, path) {
|
|
804
|
+
let locations = adapter.locations();
|
|
805
|
+
if (path) locations = locations.filter((location) => location.path === path);
|
|
806
|
+
else if (scope) locations = locations.filter((location) => location.scope === scope);
|
|
807
|
+
if (locations.length === 1) return locations[0];
|
|
808
|
+
if (locations.length === 0) throw new Error(`No ${adapter.label} config location matches${scope ? ` scope '${scope}'` : ""}${path ? ` path '${path}'` : ""}.`);
|
|
809
|
+
throw new Error(`${adapter.label} has multiple matching config locations. Pass --location with one of: ${locations.map((location) => location.path).join(", ")}`);
|
|
810
|
+
}
|
|
811
|
+
async function add_client_server(adapter, location, server) {
|
|
812
|
+
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);
|
|
818
|
+
}
|
|
819
|
+
async function add_client_server_config(adapter, location, name, config) {
|
|
820
|
+
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);
|
|
826
|
+
}
|
|
827
|
+
async function remove_client_server(adapter, location, server_name) {
|
|
828
|
+
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);
|
|
834
|
+
}
|
|
835
|
+
function assert_enabled_server_names(location, servers, enabled_names) {
|
|
836
|
+
const known_names = new Set(servers.map((server) => server.name));
|
|
837
|
+
const unknown_names = enabled_names.filter((name) => !known_names.has(name));
|
|
838
|
+
if (unknown_names.length > 0) throw new Error(`Server '${unknown_names[0]}' not found at ${location.path}.`);
|
|
839
|
+
}
|
|
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);
|
|
850
|
+
}
|
|
851
|
+
async function set_client_server_enabled(adapter, location, server_name, enabled) {
|
|
852
|
+
const servers = await adapter.readLocation(location);
|
|
853
|
+
const server = servers.find((candidate) => candidate.name === server_name);
|
|
854
|
+
if (!server) throw new Error(`Server '${server_name}' not found at ${location.path}.`);
|
|
855
|
+
const enabled_names = new Set(servers.filter((candidate) => candidate.disabled !== true).map((candidate) => candidate.name));
|
|
856
|
+
if (enabled) enabled_names.add(server.name);
|
|
857
|
+
else enabled_names.delete(server.name);
|
|
858
|
+
return set_client_enabled_servers(adapter, location, [...enabled_names]);
|
|
859
|
+
}
|
|
395
860
|
async function list_client_locations() {
|
|
396
861
|
return await Promise.all(client_adapters.flatMap((adapter) => adapter.locations().map(async (location) => ({
|
|
397
862
|
client: adapter.id,
|
|
@@ -421,7 +886,7 @@ async function read_server_registry() {
|
|
|
421
886
|
async function write_server_registry(registry) {
|
|
422
887
|
const registry_path = get_server_registry_path();
|
|
423
888
|
await ensure_directory_exists(get_mcpick_dir());
|
|
424
|
-
await
|
|
889
|
+
await atomic_json_write(registry_path, () => registry);
|
|
425
890
|
}
|
|
426
891
|
async function add_server_to_registry(server) {
|
|
427
892
|
const registry = await read_server_registry();
|
|
@@ -431,7 +896,7 @@ async function add_server_to_registry(server) {
|
|
|
431
896
|
await write_server_registry(registry);
|
|
432
897
|
}
|
|
433
898
|
async function get_all_available_servers() {
|
|
434
|
-
const { get_enabled_servers, read_claude_config } = await import("./config-
|
|
899
|
+
const { get_enabled_servers, read_claude_config } = await import("./config-DE58Fik_.js").then((n) => n.t);
|
|
435
900
|
const registry = await read_server_registry();
|
|
436
901
|
const config_servers = get_enabled_servers(await read_claude_config());
|
|
437
902
|
const config_by_name = new Map(config_servers.map((s) => [s.name, s]));
|
|
@@ -566,17 +1031,26 @@ async function add_mcp_via_cli(server, scope) {
|
|
|
566
1031
|
/**
|
|
567
1032
|
* Remove an MCP server using Claude CLI
|
|
568
1033
|
*/
|
|
569
|
-
|
|
1034
|
+
function build_remove_args(name, scope) {
|
|
1035
|
+
return scope ? [
|
|
1036
|
+
"mcp",
|
|
1037
|
+
"remove",
|
|
1038
|
+
name,
|
|
1039
|
+
"--scope",
|
|
1040
|
+
scope
|
|
1041
|
+
] : [
|
|
1042
|
+
"mcp",
|
|
1043
|
+
"remove",
|
|
1044
|
+
name
|
|
1045
|
+
];
|
|
1046
|
+
}
|
|
1047
|
+
async function remove_mcp_via_cli(name, scope) {
|
|
570
1048
|
if (!await check_claude_cli()) return {
|
|
571
1049
|
success: false,
|
|
572
1050
|
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
573
1051
|
};
|
|
574
1052
|
try {
|
|
575
|
-
await run_claude(
|
|
576
|
-
"mcp",
|
|
577
|
-
"remove",
|
|
578
|
-
name
|
|
579
|
-
]);
|
|
1053
|
+
await run_claude(build_remove_args(name, scope));
|
|
580
1054
|
return { success: true };
|
|
581
1055
|
} catch (error) {
|
|
582
1056
|
return {
|
|
@@ -657,77 +1131,176 @@ async function update_plugin_via_cli(key, scope = "user") {
|
|
|
657
1131
|
};
|
|
658
1132
|
}
|
|
659
1133
|
}
|
|
1134
|
+
const GITHUB_OWNER_PATTERN = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?";
|
|
1135
|
+
const GITHUB_REPO_PATTERN = "[A-Za-z0-9._-]+";
|
|
660
1136
|
/**
|
|
661
1137
|
* Extract GitHub owner/repo from various source formats.
|
|
662
1138
|
* Returns null if not a recognizable GitHub reference.
|
|
663
1139
|
*/
|
|
664
1140
|
function parse_github_repo(source) {
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1141
|
+
const trimmed = source.trim();
|
|
1142
|
+
try {
|
|
1143
|
+
const url = new URL(trimmed);
|
|
1144
|
+
if (url.hostname !== "github.com") return null;
|
|
1145
|
+
const [owner, raw_repo, ...rest] = url.pathname.split("/").filter(Boolean);
|
|
1146
|
+
if (!owner || !raw_repo || rest.length > 0) return null;
|
|
1147
|
+
const repo = raw_repo.replace(/\.git$/, "");
|
|
1148
|
+
if (!is_valid_github_repo(owner, repo)) return null;
|
|
1149
|
+
return {
|
|
1150
|
+
owner,
|
|
1151
|
+
repo,
|
|
1152
|
+
kind: "https"
|
|
1153
|
+
};
|
|
1154
|
+
} catch {}
|
|
1155
|
+
const ssh_match = trimmed.match(new RegExp(`^git@github\\.com:(${GITHUB_OWNER_PATTERN})/(${GITHUB_REPO_PATTERN})(?:\\.git)?$`));
|
|
671
1156
|
if (ssh_match) return {
|
|
672
1157
|
owner: ssh_match[1],
|
|
673
|
-
repo: ssh_match[2]
|
|
1158
|
+
repo: ssh_match[2].replace(/\.git$/, ""),
|
|
1159
|
+
kind: "ssh"
|
|
674
1160
|
};
|
|
675
|
-
const shorthand_match =
|
|
1161
|
+
const shorthand_match = trimmed.match(new RegExp(`^(${GITHUB_OWNER_PATTERN})/(${GITHUB_REPO_PATTERN})$`));
|
|
676
1162
|
if (shorthand_match) return {
|
|
677
1163
|
owner: shorthand_match[1],
|
|
678
|
-
repo: shorthand_match[2]
|
|
1164
|
+
repo: shorthand_match[2],
|
|
1165
|
+
kind: "shorthand"
|
|
679
1166
|
};
|
|
680
1167
|
return null;
|
|
681
1168
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1169
|
+
function is_valid_github_repo(owner, repo) {
|
|
1170
|
+
return new RegExp(`^${GITHUB_OWNER_PATTERN}$`).test(owner) && new RegExp(`^${GITHUB_REPO_PATTERN}$`).test(repo);
|
|
1171
|
+
}
|
|
1172
|
+
function build_marketplace_add_args(source) {
|
|
1173
|
+
return [
|
|
1174
|
+
"plugin",
|
|
1175
|
+
"marketplace",
|
|
1176
|
+
"add",
|
|
1177
|
+
source
|
|
1178
|
+
];
|
|
1179
|
+
}
|
|
1180
|
+
function get_github_repo_id(ref) {
|
|
1181
|
+
return `${ref.owner}/${ref.repo}`;
|
|
1182
|
+
}
|
|
1183
|
+
function get_github_clone_urls(ref) {
|
|
1184
|
+
const repo_id = get_github_repo_id(ref);
|
|
1185
|
+
const https_url = `https://github.com/${repo_id}.git`;
|
|
1186
|
+
const ssh_url = `git@github.com:${repo_id}.git`;
|
|
1187
|
+
if (ref.kind === "https") return [https_url];
|
|
1188
|
+
if (ref.kind === "ssh") return [ssh_url];
|
|
1189
|
+
return [https_url, ssh_url];
|
|
1190
|
+
}
|
|
1191
|
+
function get_process_error_output(error) {
|
|
1192
|
+
const process_error = error;
|
|
1193
|
+
return redact_text([
|
|
1194
|
+
process_error.message,
|
|
1195
|
+
process_error.stderr?.toString(),
|
|
1196
|
+
process_error.stdout?.toString()
|
|
1197
|
+
].filter(Boolean).join("\n"));
|
|
1198
|
+
}
|
|
1199
|
+
async function can_access_with_git(url) {
|
|
687
1200
|
try {
|
|
688
|
-
|
|
1201
|
+
await exec_file_async$1("git", [
|
|
1202
|
+
"ls-remote",
|
|
1203
|
+
"--exit-code",
|
|
1204
|
+
url,
|
|
1205
|
+
"HEAD"
|
|
1206
|
+
], {
|
|
1207
|
+
env: {
|
|
1208
|
+
...process.env,
|
|
1209
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1210
|
+
GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
|
|
1211
|
+
},
|
|
1212
|
+
timeout: 15e3
|
|
1213
|
+
});
|
|
1214
|
+
return null;
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
return get_process_error_output(error);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
async function can_view_with_gh(repo_id) {
|
|
1220
|
+
try {
|
|
1221
|
+
await exec_file_async$1("gh", [
|
|
1222
|
+
"repo",
|
|
1223
|
+
"view",
|
|
1224
|
+
repo_id,
|
|
1225
|
+
"--json",
|
|
1226
|
+
"nameWithOwner"
|
|
1227
|
+
], { timeout: 15e3 });
|
|
1228
|
+
return true;
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
const message = get_process_error_output(error).toLowerCase();
|
|
1231
|
+
if (message.includes("could not resolve") || message.includes("not found") || message.includes("repository not found")) return false;
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
async function get_github_api_status(repo_id) {
|
|
1236
|
+
try {
|
|
1237
|
+
return (await fetch(`https://api.github.com/repos/${repo_id}`, {
|
|
689
1238
|
method: "GET",
|
|
690
1239
|
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}'.`;
|
|
1240
|
+
})).status;
|
|
696
1241
|
} catch {
|
|
697
1242
|
return null;
|
|
698
1243
|
}
|
|
699
1244
|
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Validate that a GitHub repository can be accessed by git without cloning it.
|
|
1247
|
+
* Returns an error message if validation fails, null if OK.
|
|
1248
|
+
*/
|
|
1249
|
+
async function validate_github_repo(ref) {
|
|
1250
|
+
const repo_id = get_github_repo_id(ref);
|
|
1251
|
+
const git_errors = [];
|
|
1252
|
+
for (const url of get_github_clone_urls(ref)) {
|
|
1253
|
+
const git_error = await can_access_with_git(url);
|
|
1254
|
+
if (!git_error) return null;
|
|
1255
|
+
git_errors.push(git_error);
|
|
1256
|
+
}
|
|
1257
|
+
const gh_visible = await can_view_with_gh(repo_id);
|
|
1258
|
+
if (gh_visible === false) return `Repository '${repo_id}' not found or not accessible with your GitHub account.`;
|
|
1259
|
+
const api_status = await get_github_api_status(repo_id);
|
|
1260
|
+
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.`;
|
|
1261
|
+
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.`;
|
|
1262
|
+
return format_git_access_error(ref, git_errors.join("\n"));
|
|
1263
|
+
}
|
|
1264
|
+
function format_git_access_error(ref, message) {
|
|
1265
|
+
const repo_id = get_github_repo_id(ref);
|
|
1266
|
+
const lower = message.toLowerCase();
|
|
1267
|
+
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.`;
|
|
1268
|
+
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.`;
|
|
1269
|
+
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.`;
|
|
1270
|
+
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.`;
|
|
1271
|
+
}
|
|
700
1272
|
async function marketplace_add_via_cli(source) {
|
|
701
1273
|
if (!await check_claude_cli()) return {
|
|
702
1274
|
success: false,
|
|
703
1275
|
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
704
1276
|
};
|
|
705
1277
|
const gh = parse_github_repo(source);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const validation_error = await validate_github_repo(gh.owner, gh.repo);
|
|
1278
|
+
if (gh) {
|
|
1279
|
+
const validation_error = await validate_github_repo(gh);
|
|
709
1280
|
if (validation_error) return {
|
|
710
1281
|
success: false,
|
|
711
1282
|
error: validation_error
|
|
712
1283
|
};
|
|
713
1284
|
}
|
|
714
1285
|
try {
|
|
715
|
-
await run_claude(
|
|
716
|
-
"plugin",
|
|
717
|
-
"marketplace",
|
|
718
|
-
"add",
|
|
719
|
-
source
|
|
720
|
-
]);
|
|
1286
|
+
await run_claude(build_marketplace_add_args(source));
|
|
721
1287
|
return { success: true };
|
|
722
1288
|
} catch (error) {
|
|
723
1289
|
const message = get_redacted_error_message(error);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1290
|
+
const lower_message = message.toLowerCase();
|
|
1291
|
+
if (gh) {
|
|
1292
|
+
if (lower_message.includes("https authentication failed") || lower_message.includes("could not read username") || lower_message.includes("authentication failed")) return {
|
|
1293
|
+
success: false,
|
|
1294
|
+
error: format_git_access_error(gh, message)
|
|
1295
|
+
};
|
|
1296
|
+
if (message.includes("SSH") || message.includes("Permission denied (publickey)")) return {
|
|
1297
|
+
success: false,
|
|
1298
|
+
error: format_git_access_error(gh, message)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
if (lower_message.includes("not found") || lower_message.includes("does not exist")) return {
|
|
729
1302
|
success: false,
|
|
730
|
-
error: `Repository '${source}' not found. Check the name and your access permissions.`
|
|
1303
|
+
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
1304
|
};
|
|
732
1305
|
return {
|
|
733
1306
|
success: false,
|
|
@@ -870,20 +1443,23 @@ async function mcp_get_via_cli(name) {
|
|
|
870
1443
|
/**
|
|
871
1444
|
* Add an MCP server from raw JSON via Claude CLI
|
|
872
1445
|
*/
|
|
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
|
+
}
|
|
873
1456
|
async function mcp_add_json_via_cli(name, json, scope = "local") {
|
|
874
1457
|
if (!await check_claude_cli()) return {
|
|
875
1458
|
success: false,
|
|
876
1459
|
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
877
1460
|
};
|
|
878
1461
|
try {
|
|
879
|
-
await run_claude(
|
|
880
|
-
"mcp",
|
|
881
|
-
"add-json",
|
|
882
|
-
name,
|
|
883
|
-
json,
|
|
884
|
-
"--scope",
|
|
885
|
-
scope
|
|
886
|
-
]);
|
|
1462
|
+
await run_claude(build_add_json_args(name, json, scope));
|
|
887
1463
|
return { success: true };
|
|
888
1464
|
} catch (error) {
|
|
889
1465
|
return {
|
|
@@ -979,7 +1555,16 @@ async function edit_client_config(adapter) {
|
|
|
979
1555
|
required: false
|
|
980
1556
|
});
|
|
981
1557
|
if (typeof selected_names === "symbol") return;
|
|
982
|
-
await adapter
|
|
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);
|
|
983
1568
|
note(`Configuration updated!\nClient: ${adapter.label}\nConfig: ${location.path}\nEnabled servers: ${selected_names.length}`);
|
|
984
1569
|
}
|
|
985
1570
|
async function select_config_location(adapter) {
|
|
@@ -1053,7 +1638,7 @@ async function edit_claude_config() {
|
|
|
1053
1638
|
}
|
|
1054
1639
|
}
|
|
1055
1640
|
for (const name of servers_to_remove) {
|
|
1056
|
-
const result = await remove_mcp_via_cli(name);
|
|
1641
|
+
const result = await remove_mcp_via_cli(name, scope);
|
|
1057
1642
|
if (!result.success) {
|
|
1058
1643
|
error_count++;
|
|
1059
1644
|
log.warn(`Failed to remove ${name}: ${result.error}`);
|
|
@@ -1358,7 +1943,7 @@ async function read_disabled_hooks() {
|
|
|
1358
1943
|
}
|
|
1359
1944
|
async function write_disabled_hooks(entries) {
|
|
1360
1945
|
await ensure_directory_exists(get_mcpick_dir());
|
|
1361
|
-
await
|
|
1946
|
+
await safe_json_write(get_disabled_hooks_path(), entries, " ");
|
|
1362
1947
|
}
|
|
1363
1948
|
/**
|
|
1364
1949
|
* Remove a specific hook handler from a hooks.json file by matching the handler.
|
|
@@ -1387,7 +1972,7 @@ async function remove_hook_from_file(hooks_path, event, handler) {
|
|
|
1387
1972
|
}
|
|
1388
1973
|
if (!removed) return false;
|
|
1389
1974
|
if (matchers.length === 0) delete hooks_obj[event];
|
|
1390
|
-
await
|
|
1975
|
+
await safe_json_write(hooks_path, hooks_data, " ");
|
|
1391
1976
|
return true;
|
|
1392
1977
|
}
|
|
1393
1978
|
/**
|
|
@@ -1446,7 +2031,7 @@ async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
|
|
|
1446
2031
|
}
|
|
1447
2032
|
if (matcher.hooks.some((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt)) return;
|
|
1448
2033
|
matcher.hooks.push(handler);
|
|
1449
|
-
await
|
|
2034
|
+
await safe_json_write(hooks_path, hooks_data, " ");
|
|
1450
2035
|
}
|
|
1451
2036
|
/**
|
|
1452
2037
|
* Re-enable a previously disabled plugin hook.
|
|
@@ -2072,6 +2657,21 @@ async function load_profile(name) {
|
|
|
2072
2657
|
throw error;
|
|
2073
2658
|
}
|
|
2074
2659
|
}
|
|
2660
|
+
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;
|
|
2664
|
+
let pluginCount = 0;
|
|
2665
|
+
if (profile.enabledPlugins) {
|
|
2666
|
+
await write_claude_settings({ enabledPlugins: profile.enabledPlugins });
|
|
2667
|
+
pluginCount = Object.keys(profile.enabledPlugins).length;
|
|
2668
|
+
}
|
|
2669
|
+
return {
|
|
2670
|
+
profile: name,
|
|
2671
|
+
serverCount,
|
|
2672
|
+
pluginCount
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2075
2675
|
async function list_profiles() {
|
|
2076
2676
|
const profiles_dir = get_profiles_dir();
|
|
2077
2677
|
try {
|
|
@@ -2107,12 +2707,20 @@ async function save_profile(name) {
|
|
|
2107
2707
|
await ensure_directory_exists(get_profiles_dir());
|
|
2108
2708
|
const profile_data = { mcpServers: servers };
|
|
2109
2709
|
if (plugin_count > 0) profile_data.enabledPlugins = plugins;
|
|
2110
|
-
await
|
|
2710
|
+
await safe_json_write(get_profile_path(name), profile_data, 2);
|
|
2111
2711
|
return {
|
|
2112
2712
|
serverCount: server_count,
|
|
2113
2713
|
pluginCount: plugin_count
|
|
2114
2714
|
};
|
|
2115
2715
|
}
|
|
2716
|
+
async function save_current_claude_profile(name) {
|
|
2717
|
+
const counts = await save_profile(name);
|
|
2718
|
+
return {
|
|
2719
|
+
profile: name,
|
|
2720
|
+
serverCount: counts.serverCount,
|
|
2721
|
+
pluginCount: counts.pluginCount
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2116
2724
|
//#endregion
|
|
2117
2725
|
//#region src/index.ts
|
|
2118
2726
|
function parse_args() {
|
|
@@ -2127,17 +2735,13 @@ function parse_args() {
|
|
|
2127
2735
|
} else if (args[i] === "--list-profiles" || args[i] === "-l") result.listProfiles = true;
|
|
2128
2736
|
return result;
|
|
2129
2737
|
}
|
|
2130
|
-
async function
|
|
2131
|
-
intro(`MCPick - Loading profile: ${name}`);
|
|
2738
|
+
async function apply_claude_profile(name) {
|
|
2739
|
+
intro(`MCPick - Loading Claude Code profile: ${name}`);
|
|
2132
2740
|
try {
|
|
2133
|
-
const
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
await write_claude_settings({ enabledPlugins: profile.enabledPlugins });
|
|
2138
|
-
parts.push(`${Object.keys(profile.enabledPlugins).length} plugins`);
|
|
2139
|
-
}
|
|
2140
|
-
log.success(`Profile '${name}' applied (${parts.join(", ")})`);
|
|
2741
|
+
const result = await apply_profile_to_claude(name);
|
|
2742
|
+
const parts = [`${result.serverCount} servers`];
|
|
2743
|
+
if (result.pluginCount > 0) parts.push(`${result.pluginCount} plugins`);
|
|
2744
|
+
log.success(`Profile '${result.profile}' applied (${parts.join(", ")})`);
|
|
2141
2745
|
outro("Done!");
|
|
2142
2746
|
} catch (error) {
|
|
2143
2747
|
if (error instanceof Error) cancel(error.message);
|
|
@@ -2154,13 +2758,13 @@ async function show_profiles() {
|
|
|
2154
2758
|
} else for (const p of profiles) log.info(`${p.name} (${p.serverCount} servers)`);
|
|
2155
2759
|
outro("");
|
|
2156
2760
|
}
|
|
2157
|
-
async function
|
|
2158
|
-
intro(`MCPick - Saving profile: ${name}`);
|
|
2761
|
+
async function create_claude_profile(name) {
|
|
2762
|
+
intro(`MCPick - Saving Claude Code profile: ${name}`);
|
|
2159
2763
|
try {
|
|
2160
|
-
const
|
|
2161
|
-
const parts = [`${
|
|
2162
|
-
if (
|
|
2163
|
-
log.success(`Profile '${
|
|
2764
|
+
const result = await save_current_claude_profile(name);
|
|
2765
|
+
const parts = [`${result.serverCount} servers`];
|
|
2766
|
+
if (result.pluginCount > 0) parts.push(`${result.pluginCount} plugins`);
|
|
2767
|
+
log.success(`Profile '${result.profile}' saved (${parts.join(", ")})`);
|
|
2164
2768
|
outro("Done!");
|
|
2165
2769
|
} catch (error) {
|
|
2166
2770
|
if (error instanceof Error) cancel(error.message);
|
|
@@ -2168,7 +2772,7 @@ async function create_profile(name) {
|
|
|
2168
2772
|
process.exit(1);
|
|
2169
2773
|
}
|
|
2170
2774
|
}
|
|
2171
|
-
async function
|
|
2775
|
+
async function handle_load_claude_profile() {
|
|
2172
2776
|
const profiles = await list_profiles();
|
|
2173
2777
|
if (profiles.length === 0) {
|
|
2174
2778
|
log.warn("No profiles found");
|
|
@@ -2188,16 +2792,12 @@ async function handle_load_profile() {
|
|
|
2188
2792
|
})
|
|
2189
2793
|
});
|
|
2190
2794
|
if (isCancel(profile_name)) return;
|
|
2191
|
-
const
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
await write_claude_settings({ enabledPlugins: profile.enabledPlugins });
|
|
2196
|
-
parts.push(`${Object.keys(profile.enabledPlugins).length} plugins`);
|
|
2197
|
-
}
|
|
2198
|
-
log.success(`Profile '${profile_name}' applied (${parts.join(", ")})`);
|
|
2795
|
+
const result = await apply_profile_to_claude(profile_name);
|
|
2796
|
+
const parts = [`${result.serverCount} servers`];
|
|
2797
|
+
if (result.pluginCount > 0) parts.push(`${result.pluginCount} plugins`);
|
|
2798
|
+
log.success(`Profile '${result.profile}' applied (${parts.join(", ")})`);
|
|
2199
2799
|
}
|
|
2200
|
-
async function
|
|
2800
|
+
async function handle_save_claude_profile() {
|
|
2201
2801
|
const name = await text({
|
|
2202
2802
|
message: "Profile name:",
|
|
2203
2803
|
placeholder: "e.g. database, web-dev, minimal",
|
|
@@ -2207,10 +2807,10 @@ async function handle_save_profile() {
|
|
|
2207
2807
|
}
|
|
2208
2808
|
});
|
|
2209
2809
|
if (isCancel(name)) return;
|
|
2210
|
-
const
|
|
2211
|
-
const parts = [`${
|
|
2212
|
-
if (
|
|
2213
|
-
log.success(`Profile '${
|
|
2810
|
+
const result = await save_current_claude_profile(name);
|
|
2811
|
+
const parts = [`${result.serverCount} servers`];
|
|
2812
|
+
if (result.pluginCount > 0) parts.push(`${result.pluginCount} plugins`);
|
|
2813
|
+
log.success(`Profile '${result.profile}' saved (${parts.join(", ")})`);
|
|
2214
2814
|
}
|
|
2215
2815
|
async function handle_client_tools() {
|
|
2216
2816
|
const client_id = await select({
|
|
@@ -2278,11 +2878,11 @@ async function main() {
|
|
|
2278
2878
|
return;
|
|
2279
2879
|
}
|
|
2280
2880
|
if (args.saveProfile) {
|
|
2281
|
-
await
|
|
2881
|
+
await create_claude_profile(args.saveProfile);
|
|
2282
2882
|
return;
|
|
2283
2883
|
}
|
|
2284
2884
|
if (args.profile) {
|
|
2285
|
-
await
|
|
2885
|
+
await apply_claude_profile(args.profile);
|
|
2286
2886
|
return;
|
|
2287
2887
|
}
|
|
2288
2888
|
intro("MCPick - MCP Configuration Manager");
|
|
@@ -2308,13 +2908,13 @@ async function main() {
|
|
|
2308
2908
|
},
|
|
2309
2909
|
{
|
|
2310
2910
|
value: "load-profile",
|
|
2311
|
-
label: "Load profile",
|
|
2312
|
-
hint: "Apply a saved profile"
|
|
2911
|
+
label: "Load Claude Code profile",
|
|
2912
|
+
hint: "Apply a saved Claude Code profile"
|
|
2313
2913
|
},
|
|
2314
2914
|
{
|
|
2315
2915
|
value: "save-profile",
|
|
2316
|
-
label: "Save profile",
|
|
2317
|
-
hint: "Save current config as profile"
|
|
2916
|
+
label: "Save Claude Code profile",
|
|
2917
|
+
hint: "Save current Claude Code config as profile"
|
|
2318
2918
|
},
|
|
2319
2919
|
{
|
|
2320
2920
|
value: "backup",
|
|
@@ -2354,10 +2954,10 @@ async function main() {
|
|
|
2354
2954
|
await restore_config();
|
|
2355
2955
|
break;
|
|
2356
2956
|
case "load-profile":
|
|
2357
|
-
await
|
|
2957
|
+
await handle_load_claude_profile();
|
|
2358
2958
|
break;
|
|
2359
2959
|
case "save-profile":
|
|
2360
|
-
await
|
|
2960
|
+
await handle_save_claude_profile();
|
|
2361
2961
|
break;
|
|
2362
2962
|
case "exit":
|
|
2363
2963
|
outro("Goodbye!");
|
|
@@ -2402,17 +3002,18 @@ const SUBCOMMANDS = new Set([
|
|
|
2402
3002
|
"cache",
|
|
2403
3003
|
"dev",
|
|
2404
3004
|
"marketplace",
|
|
2405
|
-
"reload"
|
|
3005
|
+
"reload",
|
|
3006
|
+
"rollback"
|
|
2406
3007
|
]);
|
|
2407
3008
|
const arg = process.argv[2];
|
|
2408
3009
|
if (arg && SUBCOMMANDS.has(arg) || arg === "--help" || arg === "-h" || !process.stdout.isTTY) {
|
|
2409
3010
|
if (!arg && !process.stdout.isTTY) process.argv.push("--help");
|
|
2410
|
-
import("./cli-
|
|
3011
|
+
import("./cli-CZOlaqoZ.js").then((m) => m.run());
|
|
2411
3012
|
} else main().catch((error) => {
|
|
2412
3013
|
console.error("Fatal error:", error);
|
|
2413
3014
|
process.exit(1);
|
|
2414
3015
|
});
|
|
2415
3016
|
//#endregion
|
|
2416
|
-
export {
|
|
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 };
|
|
2417
3018
|
|
|
2418
3019
|
//# sourceMappingURL=index.js.map
|