mcpick 0.0.20 → 0.0.22
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/.github/copilot-instructions.md +39 -21
- package/CHANGELOG.md +24 -4
- package/CONTEXT.md +49 -0
- package/README.md +149 -309
- package/dist/{add-BDyaBew0.js → add-Bok0qbXi.js} +4 -5
- package/dist/{add-json-BjgzdeG-.js → add-json-C44vy2A_.js} +3 -3
- package/dist/{atomic-write-BqEykHp9.js → atomic-write-4lANmzsO.js} +1 -1
- package/dist/{backup-DSDhHI5f.js → backup-bdg6dvsb.js} +5 -5
- package/dist/{cache-D6kd7qE8.js → cache-CSUcGdZP.js} +3 -3
- package/dist/cli-avr5R1LO.js +111 -0
- package/dist/clients-CSQgqHzb.js +30 -0
- package/dist/{clone-DYKPEsar.js → clone-CQ0skkT6.js} +6 -7
- package/dist/{config-DijVdEFn.js → config-BhX4eAgg.js} +4 -4
- package/dist/{dev-DRJRNp7y.js → dev-CTDg5g-c.js} +6 -6
- package/dist/{disable-xJXZfUR_.js → disable-DLlOj7sc.js} +3 -4
- package/dist/{enable-RrpcN6la.js → enable-CGFYYC2A.js} +3 -4
- package/dist/{get-Bb1eOOIZ.js → get-l-eAJhBy.js} +3 -3
- package/dist/{hooks-Bmn7pUZa.js → hooks-BWZ_Kgx3.js} +4 -4
- package/dist/index.js +1485 -318
- package/dist/list-By--kltj.js +100 -0
- package/dist/{marketplace-DcKk5dc1.js → marketplace-DdiKDDKK.js} +4 -5
- package/dist/output-BgN9Uuxf.js +17 -0
- package/dist/{paths-BPISiJi4.js → paths-6wrIM8yh.js} +1 -1
- package/dist/{plugin-cache-Bby9Dxm9.js → plugin-cache-DKcW8LGV.js} +3 -3
- package/dist/{plugins-Dc7DN6R_.js → plugins-CsXE8AH4.js} +5 -5
- package/dist/{profile-CX97sMGp.js → profile-DzGPsdsl.js} +5 -5
- package/dist/redact-Dltz2gde.js +88 -0
- package/dist/{reload-CYDhkCVZ.js → reload-C29-vuvy.js} +2 -2
- package/dist/{remove-D1owHLhG.js → remove-B5q4rQRU.js} +3 -4
- package/dist/{reset-project-choices-BfRSNN3m.js → reset-project-choices-Dhh4CxIC.js} +3 -3
- package/dist/{restore-DdMfUljI.js → restore-BI8aiszM.js} +6 -6
- package/dist/{settings-DEcWtzLE.js → settings-CZR8bVfh.js} +5 -5
- package/dist/skills-DPBDmION.js +216 -0
- package/dist/{validation-xMlbgGCF.js → validation-qWlF51fw.js} +1 -1
- package/package.json +20 -6
- package/dist/claude-cli-DnmBJrjg.js +0 -445
- package/dist/cli-CsFfnWBo.js +0 -84
- package/dist/hook-state-Di8lUsPr.js +0 -171
- package/dist/list-B8YeDWt6.js +0 -64
- package/dist/output-BchYq0mR.js +0 -15
- package/dist/profile-DkY_lBEm.js +0 -70
- package/dist/redact-O35tjnRD.js +0 -26
- package/dist/registry-CfUKT7_C.js +0 -92
package/dist/index.js
CHANGED
|
@@ -1,218 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { m as get_plugin_backup_filename, n as get_backup_filename, r as get_backups_dir, t as ensure_directory_exists } from "./paths-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import { a as
|
|
7
|
-
import {
|
|
8
|
-
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-Bby9Dxm9.js";
|
|
9
|
-
import { a as redisable_restored_hooks, i as read_disabled_hooks, n as disable_plugin_hook, r as enable_plugin_hook, t as check_restored_hooks } from "./hook-state-Di8lUsPr.js";
|
|
10
|
-
import { n as load_profile, r as save_profile, t as list_profiles } from "./profile-DkY_lBEm.js";
|
|
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";
|
|
11
8
|
import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, text } from "@clack/prompts";
|
|
12
|
-
import { readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if ("command" in server) details.push(`Command: ${server.command} ${(server.args || []).join(" ")}`);
|
|
18
|
-
if ("url" in server) details.push(`URL: ${server.url}`);
|
|
19
|
-
details.push(`Description: ${server.description || "None"}`);
|
|
20
|
-
if (server.type) details.push(`Transport: ${server.type}`);
|
|
21
|
-
if (server.env) details.push(`Environment: ${Object.keys(server.env).length} variables`);
|
|
22
|
-
if ("headers" in server && server.headers) details.push(`Headers: ${Object.keys(server.headers).length} headers`);
|
|
23
|
-
return details;
|
|
24
|
-
}
|
|
25
|
-
async function add_server() {
|
|
26
|
-
try {
|
|
27
|
-
const cli_available = await check_claude_cli();
|
|
28
|
-
const scope = await select({
|
|
29
|
-
message: "Where should this server be installed?",
|
|
30
|
-
options: get_scope_options(),
|
|
31
|
-
initialValue: "local"
|
|
32
|
-
});
|
|
33
|
-
if (typeof scope === "symbol") return;
|
|
34
|
-
const config_method = await select({
|
|
35
|
-
message: "How would you like to add the server?",
|
|
36
|
-
options: [{
|
|
37
|
-
value: "json",
|
|
38
|
-
label: "Paste JSON configuration",
|
|
39
|
-
hint: "Paste complete server config as JSON"
|
|
40
|
-
}, {
|
|
41
|
-
value: "form",
|
|
42
|
-
label: "Step-by-step form",
|
|
43
|
-
hint: "Fill out fields one by one"
|
|
44
|
-
}],
|
|
45
|
-
initialValue: "json"
|
|
46
|
-
});
|
|
47
|
-
if (typeof config_method === "symbol") return;
|
|
48
|
-
if (config_method === "json") return await add_server_from_json(scope, cli_available);
|
|
49
|
-
const name = await text({
|
|
50
|
-
message: "Server name:",
|
|
51
|
-
placeholder: "e.g., mcp-sqlite-tools",
|
|
52
|
-
validate: (value) => {
|
|
53
|
-
if (!value || value.trim().length === 0) return "Server name is required";
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
if (typeof name === "symbol") return;
|
|
57
|
-
const command = await text({
|
|
58
|
-
message: "Command to run:",
|
|
59
|
-
placeholder: "e.g., uvx, npx, node",
|
|
60
|
-
validate: (value) => {
|
|
61
|
-
if (!value || value.trim().length === 0) return "Command is required";
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
if (typeof command === "symbol") return;
|
|
65
|
-
const args_input = await text({
|
|
66
|
-
message: "Arguments (comma-separated):",
|
|
67
|
-
placeholder: "e.g., mcp-sqlite-tools, --port, 3000",
|
|
68
|
-
defaultValue: ""
|
|
69
|
-
});
|
|
70
|
-
if (typeof args_input === "symbol") return;
|
|
71
|
-
const args = args_input.split(",").map((arg) => arg.trim()).filter((arg) => arg.length > 0);
|
|
72
|
-
const description = await text({
|
|
73
|
-
message: "Description (optional):",
|
|
74
|
-
placeholder: "Brief description of what this server provides"
|
|
75
|
-
});
|
|
76
|
-
if (typeof description === "symbol") return;
|
|
77
|
-
const configure_advanced = await confirm({
|
|
78
|
-
message: "Configure advanced settings (env variables, transport, etc.)?",
|
|
79
|
-
initialValue: false
|
|
80
|
-
});
|
|
81
|
-
if (typeof configure_advanced === "symbol") return;
|
|
82
|
-
let server_data = {
|
|
83
|
-
name: name.trim(),
|
|
84
|
-
type: "stdio",
|
|
85
|
-
command: command.trim(),
|
|
86
|
-
args,
|
|
87
|
-
...description && description.trim() && { description: description.trim() }
|
|
88
|
-
};
|
|
89
|
-
if (configure_advanced) {
|
|
90
|
-
const transport_type = await select({
|
|
91
|
-
message: "Transport type:",
|
|
92
|
-
options: [
|
|
93
|
-
{
|
|
94
|
-
value: "stdio",
|
|
95
|
-
label: "stdio (default)",
|
|
96
|
-
hint: "Standard input/output"
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
value: "sse",
|
|
100
|
-
label: "sse",
|
|
101
|
-
hint: "Server-sent events"
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
value: "http",
|
|
105
|
-
label: "http",
|
|
106
|
-
hint: "HTTP transport"
|
|
107
|
-
}
|
|
108
|
-
],
|
|
109
|
-
initialValue: "stdio"
|
|
110
|
-
});
|
|
111
|
-
if (typeof transport_type === "symbol") return;
|
|
112
|
-
server_data.type = transport_type;
|
|
113
|
-
if (transport_type === "sse" || transport_type === "http") {
|
|
114
|
-
delete server_data.command;
|
|
115
|
-
delete server_data.args;
|
|
116
|
-
const url = await text({
|
|
117
|
-
message: "Server URL:",
|
|
118
|
-
placeholder: "e.g., http://localhost:3000",
|
|
119
|
-
validate: (value) => {
|
|
120
|
-
if (!value || value.trim().length === 0) return "URL is required for non-stdio transport";
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
if (typeof url === "symbol") return;
|
|
124
|
-
server_data.url = url.trim();
|
|
125
|
-
}
|
|
126
|
-
const env_input = await text({
|
|
127
|
-
message: "Environment variables (KEY=value, comma-separated):",
|
|
128
|
-
placeholder: "e.g., API_KEY=abc123, TIMEOUT=30"
|
|
129
|
-
});
|
|
130
|
-
if (typeof env_input === "symbol") return;
|
|
131
|
-
if (env_input && env_input.trim()) {
|
|
132
|
-
const env = {};
|
|
133
|
-
env_input.split(",").forEach((pair) => {
|
|
134
|
-
const [key, ...valueParts] = pair.split("=");
|
|
135
|
-
if (key && valueParts.length > 0) env[key.trim()] = valueParts.join("=").trim();
|
|
136
|
-
});
|
|
137
|
-
if (Object.keys(env).length > 0) server_data.env = env;
|
|
138
|
-
}
|
|
139
|
-
if (transport_type === "http") {
|
|
140
|
-
const headers_input = await text({
|
|
141
|
-
message: "HTTP headers (KEY=value, comma-separated):",
|
|
142
|
-
placeholder: "e.g., Authorization=Bearer token, Content-Type=application/json"
|
|
143
|
-
});
|
|
144
|
-
if (typeof headers_input === "symbol") return;
|
|
145
|
-
if (headers_input && headers_input.trim()) {
|
|
146
|
-
const headers = {};
|
|
147
|
-
headers_input.split(",").forEach((pair) => {
|
|
148
|
-
const [key, ...valueParts] = pair.split("=");
|
|
149
|
-
if (key && valueParts.length > 0) headers[key.trim()] = valueParts.join("=").trim();
|
|
150
|
-
});
|
|
151
|
-
if (Object.keys(headers).length > 0) server_data.headers = headers;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const validated_server = validate_mcp_server(server_data);
|
|
156
|
-
const details = format_server_details(validated_server);
|
|
157
|
-
details.push(`Scope: ${get_scope_description(scope)}`);
|
|
158
|
-
note(`Server to add:\n${details.join("\n")}`);
|
|
159
|
-
const should_add = await confirm({ message: "Add this server?" });
|
|
160
|
-
if (typeof should_add === "symbol" || !should_add) return;
|
|
161
|
-
await add_server_to_registry(validated_server);
|
|
162
|
-
if (cli_available) {
|
|
163
|
-
const result = await add_mcp_via_cli(validated_server, scope);
|
|
164
|
-
if (result.success) note(`Server "${validated_server.name}" installed successfully!\nScope: ${get_scope_description(scope)}\nAlso added to mcpick registry for profile management.`);
|
|
165
|
-
else log.warn(`CLI installation failed: ${result.error}\nServer added to registry only. Use 'claude mcp add' manually.`);
|
|
166
|
-
} else log.warn("Claude CLI not found. Server added to registry only.\nInstall Claude Code CLI and run 'claude mcp add' to activate.");
|
|
167
|
-
} catch (error) {
|
|
168
|
-
throw new Error(`Failed to add server: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async function add_server_from_json(scope, cli_available) {
|
|
172
|
-
const json_input = await text({
|
|
173
|
-
message: "Paste JSON configuration:",
|
|
174
|
-
placeholder: "{ \"name\": \"mcp-sqlite-tools\", \"command\": \"npx\", \"args\": [\"-y\", \"mcp-sqlite-tools\"] }",
|
|
175
|
-
validate: (value) => {
|
|
176
|
-
if (!value || value.trim().length === 0) return "JSON configuration is required";
|
|
177
|
-
let jsonString = value.trim();
|
|
178
|
-
if (!jsonString.startsWith("{")) jsonString = `{${jsonString}}`;
|
|
179
|
-
try {
|
|
180
|
-
const parsed = JSON.parse(jsonString);
|
|
181
|
-
if (typeof parsed !== "object" || parsed === null) return "JSON must be an object";
|
|
182
|
-
if (!parsed.command) return "Server configuration must include a \"command\" field";
|
|
183
|
-
} catch {
|
|
184
|
-
return "Invalid JSON format";
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
if (typeof json_input === "symbol") return;
|
|
189
|
-
try {
|
|
190
|
-
let jsonString = json_input.trim();
|
|
191
|
-
if (!jsonString.startsWith("{")) jsonString = `{${jsonString}}`;
|
|
192
|
-
const server_data = JSON.parse(jsonString);
|
|
193
|
-
if (!server_data.type && server_data.command) server_data.type = "stdio";
|
|
194
|
-
if (server_data.type !== "stdio") {
|
|
195
|
-
delete server_data.command;
|
|
196
|
-
delete server_data.args;
|
|
197
|
-
}
|
|
198
|
-
if (server_data.command && !server_data.args) server_data.args = [];
|
|
199
|
-
const validated_server = validate_mcp_server(server_data);
|
|
200
|
-
const details = format_server_details(validated_server);
|
|
201
|
-
details.push(`Scope: ${get_scope_description(scope)}`);
|
|
202
|
-
note(`Server to add:\n${details.join("\n")}`);
|
|
203
|
-
const should_add = await confirm({ message: "Add this server?" });
|
|
204
|
-
if (typeof should_add === "symbol" || !should_add) return;
|
|
205
|
-
await add_server_to_registry(validated_server);
|
|
206
|
-
if (cli_available) {
|
|
207
|
-
const result = await add_mcp_via_cli(validated_server, scope);
|
|
208
|
-
if (result.success) note(`Server "${validated_server.name}" installed successfully!\nScope: ${get_scope_description(scope)}\nAlso added to mcpick registry for profile management.`);
|
|
209
|
-
else log.warn(`CLI installation failed: ${result.error}\nServer added to registry only. Use 'claude mcp add' manually.`);
|
|
210
|
-
} else log.warn("Claude CLI not found. Server added to registry only.\nInstall Claude Code CLI and run 'claude mcp add' to activate.");
|
|
211
|
-
} catch (error) {
|
|
212
|
-
throw new Error(`Failed to parse or validate JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
//#endregion
|
|
9
|
+
import { access, mkdir, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
216
14
|
//#region src/commands/backup.ts
|
|
217
15
|
const MAX_BACKUPS = 10;
|
|
218
16
|
async function backup_config() {
|
|
@@ -251,79 +49,1024 @@ async function cleanup_old_backups(prefix) {
|
|
|
251
49
|
} catch {}
|
|
252
50
|
}
|
|
253
51
|
//#endregion
|
|
52
|
+
//#region src/core/client-config.ts
|
|
53
|
+
const client_options_to_skip = new Set([
|
|
54
|
+
"name",
|
|
55
|
+
"type",
|
|
56
|
+
"command",
|
|
57
|
+
"args",
|
|
58
|
+
"url",
|
|
59
|
+
"httpUrl",
|
|
60
|
+
"serverUrl",
|
|
61
|
+
"env",
|
|
62
|
+
"headers",
|
|
63
|
+
"description",
|
|
64
|
+
"disabled",
|
|
65
|
+
"enabled",
|
|
66
|
+
"environment"
|
|
67
|
+
]);
|
|
68
|
+
async function file_exists(path) {
|
|
69
|
+
try {
|
|
70
|
+
await access(path);
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function read_json_file(path) {
|
|
77
|
+
try {
|
|
78
|
+
return parse_json_or_jsonc(await readFile(path, "utf-8"));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
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);
|
|
89
|
+
}
|
|
90
|
+
function parse_json_or_jsonc(content) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(content);
|
|
93
|
+
} catch {
|
|
94
|
+
return JSON.parse(remove_jsonc_syntax(content));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function remove_jsonc_syntax(content) {
|
|
98
|
+
let result = "";
|
|
99
|
+
let in_string = false;
|
|
100
|
+
let quote = "";
|
|
101
|
+
let escaped = false;
|
|
102
|
+
let in_line_comment = false;
|
|
103
|
+
let in_block_comment = false;
|
|
104
|
+
for (let index = 0; index < content.length; index++) {
|
|
105
|
+
const char = content[index];
|
|
106
|
+
const next = content[index + 1];
|
|
107
|
+
if (in_line_comment) {
|
|
108
|
+
if (char === "\n" || char === "\r") {
|
|
109
|
+
in_line_comment = false;
|
|
110
|
+
result += char;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (in_block_comment) {
|
|
115
|
+
if (char === "*" && next === "/") {
|
|
116
|
+
in_block_comment = false;
|
|
117
|
+
index++;
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (in_string) {
|
|
122
|
+
result += char;
|
|
123
|
+
if (escaped) escaped = false;
|
|
124
|
+
else if (char === "\\") escaped = true;
|
|
125
|
+
else if (char === quote) in_string = false;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (char === "\"" || char === "'") {
|
|
129
|
+
in_string = true;
|
|
130
|
+
quote = char;
|
|
131
|
+
result += char;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (char === "/" && next === "/") {
|
|
135
|
+
in_line_comment = true;
|
|
136
|
+
index++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (char === "/" && next === "*") {
|
|
140
|
+
in_block_comment = true;
|
|
141
|
+
index++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
result += char;
|
|
145
|
+
}
|
|
146
|
+
return remove_trailing_commas(result);
|
|
147
|
+
}
|
|
148
|
+
function remove_trailing_commas(content) {
|
|
149
|
+
let result = "";
|
|
150
|
+
let in_string = false;
|
|
151
|
+
let quote = "";
|
|
152
|
+
let escaped = false;
|
|
153
|
+
for (let index = 0; index < content.length; index++) {
|
|
154
|
+
const char = content[index];
|
|
155
|
+
if (in_string) {
|
|
156
|
+
result += char;
|
|
157
|
+
if (escaped) escaped = false;
|
|
158
|
+
else if (char === "\\") escaped = true;
|
|
159
|
+
else if (char === quote) in_string = false;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (char === "\"") {
|
|
163
|
+
in_string = true;
|
|
164
|
+
quote = char;
|
|
165
|
+
result += char;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (char === ",") {
|
|
169
|
+
let cursor = index + 1;
|
|
170
|
+
while (/\s/.test(content[cursor] ?? "")) cursor++;
|
|
171
|
+
if (content[cursor] === "}" || content[cursor] === "]") continue;
|
|
172
|
+
}
|
|
173
|
+
result += char;
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
function string_record(value) {
|
|
178
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
179
|
+
const result = {};
|
|
180
|
+
for (const [key, item] of Object.entries(value)) if (typeof item === "string") result[key] = item;
|
|
181
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
182
|
+
}
|
|
183
|
+
function string_array(value) {
|
|
184
|
+
if (!Array.isArray(value)) return void 0;
|
|
185
|
+
const values = value.filter((item) => typeof item === "string");
|
|
186
|
+
return values.length > 0 ? values : void 0;
|
|
187
|
+
}
|
|
188
|
+
function infer_transport(config) {
|
|
189
|
+
if (config.type === "http" || config.type === "remote" || config.httpUrl || config.serverUrl) return "http";
|
|
190
|
+
if (config.type === "sse") return "sse";
|
|
191
|
+
if (config.url && !config.command) return "http";
|
|
192
|
+
return "stdio";
|
|
193
|
+
}
|
|
194
|
+
function normalize_mcp_server(name, config) {
|
|
195
|
+
const transport = infer_transport(config);
|
|
196
|
+
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;
|
|
199
|
+
const command_array = string_array(config.command);
|
|
200
|
+
const command = typeof config.command === "string" ? config.command : command_array?.[0];
|
|
201
|
+
const args = string_array(config.args) ?? command_array?.slice(1);
|
|
202
|
+
const env = string_record(config.env) ?? string_record(config.environment);
|
|
203
|
+
const disabled = typeof config.disabled === "boolean" ? config.disabled : typeof config.enabled === "boolean" ? !config.enabled : void 0;
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
transport,
|
|
207
|
+
...command ? { command } : {},
|
|
208
|
+
...args && args.length > 0 ? { args } : {},
|
|
209
|
+
...url ? { url } : {},
|
|
210
|
+
...env ? { env } : {},
|
|
211
|
+
...string_record(config.headers) ? { headers: string_record(config.headers) } : {},
|
|
212
|
+
...typeof config.description === "string" ? { description: config.description } : {},
|
|
213
|
+
...typeof disabled === "boolean" ? { disabled } : {},
|
|
214
|
+
...Object.keys(clientOptions).length > 0 ? { clientOptions } : {}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function get_server_record(data, key) {
|
|
218
|
+
const servers = data?.[key];
|
|
219
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return {};
|
|
220
|
+
return Object.fromEntries(Object.entries(servers).filter((entry) => {
|
|
221
|
+
const [, value] = entry;
|
|
222
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
function read_server_map(data, key) {
|
|
226
|
+
return Object.entries(get_server_record(data, key)).map(([name, config]) => normalize_mcp_server(name, config));
|
|
227
|
+
}
|
|
228
|
+
function set_server_enabled(config, enabled, mode) {
|
|
229
|
+
if (mode === "enabled" || "enabled" in config) {
|
|
230
|
+
config.enabled = enabled;
|
|
231
|
+
delete config.disabled;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
config.disabled = !enabled;
|
|
235
|
+
}
|
|
236
|
+
function create_json_adapter(options) {
|
|
237
|
+
return {
|
|
238
|
+
id: options.id,
|
|
239
|
+
label: options.label,
|
|
240
|
+
locations: options.locations,
|
|
241
|
+
async read(scope) {
|
|
242
|
+
const locations = scope ? options.locations().filter((location) => location.scope === scope) : options.locations();
|
|
243
|
+
const result = [];
|
|
244
|
+
for (const location of locations) for (const server of await this.readLocation(location)) result.push(server);
|
|
245
|
+
return result;
|
|
246
|
+
},
|
|
247
|
+
async readLocation(location) {
|
|
248
|
+
return read_server_map(await read_json_file(location.path), options.serverKey);
|
|
249
|
+
},
|
|
250
|
+
async writeEnabled(location, enabledNames) {
|
|
251
|
+
const data = await read_json_file(location.path) ?? {};
|
|
252
|
+
const servers = get_server_record(data, options.serverKey);
|
|
253
|
+
const enabled = new Set(enabledNames);
|
|
254
|
+
for (const [name, config] of Object.entries(servers)) set_server_enabled(config, enabled.has(name), options.disabledMode ?? "disabled");
|
|
255
|
+
data[options.serverKey] = servers;
|
|
256
|
+
await write_json_file(location.path, data);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function project_path(path) {
|
|
261
|
+
return join(process.cwd(), path);
|
|
262
|
+
}
|
|
263
|
+
const client_adapters = [
|
|
264
|
+
{
|
|
265
|
+
id: "claude-code",
|
|
266
|
+
label: "Claude Code",
|
|
267
|
+
locations: () => [
|
|
268
|
+
{
|
|
269
|
+
scope: "local",
|
|
270
|
+
path: get_claude_config_path(),
|
|
271
|
+
description: "~/.claude.json projects[cwd].mcpServers"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
scope: "project",
|
|
275
|
+
path: project_path(".mcp.json"),
|
|
276
|
+
description: ".mcp.json mcpServers"
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
scope: "user",
|
|
280
|
+
path: get_claude_config_path(),
|
|
281
|
+
description: "~/.claude.json mcpServers"
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
async read(scope) {
|
|
285
|
+
const locations = scope ? this.locations().filter((location) => location.scope === scope) : this.locations();
|
|
286
|
+
const result = [];
|
|
287
|
+
for (const location of locations) result.push(...await this.readLocation(location));
|
|
288
|
+
return result;
|
|
289
|
+
},
|
|
290
|
+
async readLocation(location) {
|
|
291
|
+
if (location.scope === "project") return read_server_map(await read_json_file(location.path), "mcpServers");
|
|
292
|
+
const data = await read_json_file(get_claude_config_path());
|
|
293
|
+
if (location.scope === "user") return read_server_map(data, "mcpServers");
|
|
294
|
+
const projects = data?.projects;
|
|
295
|
+
if (!projects || typeof projects !== "object" || Array.isArray(projects)) return [];
|
|
296
|
+
const project_config = projects[process.cwd()];
|
|
297
|
+
if (!project_config || typeof project_config !== "object" || Array.isArray(project_config)) return [];
|
|
298
|
+
return read_server_map(project_config, "mcpServers");
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
create_json_adapter({
|
|
302
|
+
id: "gemini-cli",
|
|
303
|
+
label: "Gemini CLI",
|
|
304
|
+
serverKey: "mcpServers",
|
|
305
|
+
locations: () => [{
|
|
306
|
+
scope: "project",
|
|
307
|
+
path: project_path(".gemini/settings.json"),
|
|
308
|
+
description: ".gemini/settings.json mcpServers"
|
|
309
|
+
}, {
|
|
310
|
+
scope: "user",
|
|
311
|
+
path: join(homedir(), ".gemini/settings.json"),
|
|
312
|
+
description: "~/.gemini/settings.json mcpServers"
|
|
313
|
+
}]
|
|
314
|
+
}),
|
|
315
|
+
create_json_adapter({
|
|
316
|
+
id: "vscode",
|
|
317
|
+
label: "VS Code / GitHub Copilot",
|
|
318
|
+
serverKey: "servers",
|
|
319
|
+
locations: () => [{
|
|
320
|
+
scope: "project",
|
|
321
|
+
path: project_path(".vscode/mcp.json"),
|
|
322
|
+
description: ".vscode/mcp.json servers"
|
|
323
|
+
}]
|
|
324
|
+
}),
|
|
325
|
+
create_json_adapter({
|
|
326
|
+
id: "cursor",
|
|
327
|
+
label: "Cursor",
|
|
328
|
+
serverKey: "mcpServers",
|
|
329
|
+
locations: () => [{
|
|
330
|
+
scope: "project",
|
|
331
|
+
path: project_path(".cursor/mcp.json"),
|
|
332
|
+
description: ".cursor/mcp.json mcpServers"
|
|
333
|
+
}, {
|
|
334
|
+
scope: "user",
|
|
335
|
+
path: join(homedir(), ".cursor/mcp.json"),
|
|
336
|
+
description: "~/.cursor/mcp.json mcpServers"
|
|
337
|
+
}]
|
|
338
|
+
}),
|
|
339
|
+
create_json_adapter({
|
|
340
|
+
id: "windsurf",
|
|
341
|
+
label: "Windsurf",
|
|
342
|
+
serverKey: "mcpServers",
|
|
343
|
+
locations: () => [{
|
|
344
|
+
scope: "user",
|
|
345
|
+
path: join(homedir(), ".codeium/windsurf/mcp_config.json"),
|
|
346
|
+
description: "~/.codeium/windsurf/mcp_config.json mcpServers"
|
|
347
|
+
}]
|
|
348
|
+
}),
|
|
349
|
+
create_json_adapter({
|
|
350
|
+
id: "opencode",
|
|
351
|
+
label: "OpenCode",
|
|
352
|
+
serverKey: "mcp",
|
|
353
|
+
disabledMode: "enabled",
|
|
354
|
+
locations: () => [{
|
|
355
|
+
scope: "project",
|
|
356
|
+
path: project_path("opencode.json"),
|
|
357
|
+
description: "opencode.json mcp"
|
|
358
|
+
}, {
|
|
359
|
+
scope: "user",
|
|
360
|
+
path: join(homedir(), ".config/opencode/opencode.json"),
|
|
361
|
+
description: "~/.config/opencode/opencode.json mcp"
|
|
362
|
+
}]
|
|
363
|
+
}),
|
|
364
|
+
create_json_adapter({
|
|
365
|
+
id: "pi",
|
|
366
|
+
label: "Pi MCP Adapter",
|
|
367
|
+
serverKey: "mcpServers",
|
|
368
|
+
locations: () => [
|
|
369
|
+
{
|
|
370
|
+
scope: "user",
|
|
371
|
+
path: join(homedir(), ".config/mcp/mcp.json"),
|
|
372
|
+
description: "~/.config/mcp/mcp.json shared MCP config"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
scope: "user",
|
|
376
|
+
path: join(homedir(), ".pi/agent/mcp.json"),
|
|
377
|
+
description: "~/.pi/agent/mcp.json Pi global override"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
scope: "project",
|
|
381
|
+
path: project_path(".mcp.json"),
|
|
382
|
+
description: ".mcp.json shared project MCP config"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
scope: "project",
|
|
386
|
+
path: project_path(".pi/mcp.json"),
|
|
387
|
+
description: ".pi/mcp.json Pi project override"
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
})
|
|
391
|
+
];
|
|
392
|
+
function get_client_adapter(id) {
|
|
393
|
+
return client_adapters.find((adapter) => adapter.id === id) ?? null;
|
|
394
|
+
}
|
|
395
|
+
async function list_client_locations() {
|
|
396
|
+
return await Promise.all(client_adapters.flatMap((adapter) => adapter.locations().map(async (location) => ({
|
|
397
|
+
client: adapter.id,
|
|
398
|
+
label: adapter.label,
|
|
399
|
+
...location,
|
|
400
|
+
exists: await file_exists(location.path)
|
|
401
|
+
}))));
|
|
402
|
+
}
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/core/registry.ts
|
|
405
|
+
async function read_server_registry() {
|
|
406
|
+
const registry_path = get_server_registry_path();
|
|
407
|
+
try {
|
|
408
|
+
await access(registry_path);
|
|
409
|
+
const registry_content = await readFile(registry_path, "utf-8");
|
|
410
|
+
return validate_server_registry(JSON.parse(registry_content));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
413
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
414
|
+
const default_registry = { servers: [] };
|
|
415
|
+
await write_server_registry(default_registry);
|
|
416
|
+
return default_registry;
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function write_server_registry(registry) {
|
|
422
|
+
const registry_path = get_server_registry_path();
|
|
423
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
424
|
+
await writeFile(registry_path, JSON.stringify(registry, null, 2), "utf-8");
|
|
425
|
+
}
|
|
426
|
+
async function add_server_to_registry(server) {
|
|
427
|
+
const registry = await read_server_registry();
|
|
428
|
+
const existing_index = registry.servers.findIndex((s) => s.name === server.name);
|
|
429
|
+
if (existing_index >= 0) registry.servers[existing_index] = server;
|
|
430
|
+
else registry.servers.push(server);
|
|
431
|
+
await write_server_registry(registry);
|
|
432
|
+
}
|
|
433
|
+
async function get_all_available_servers() {
|
|
434
|
+
const { get_enabled_servers, read_claude_config } = await import("./config-BhX4eAgg.js").then((n) => n.t);
|
|
435
|
+
const registry = await read_server_registry();
|
|
436
|
+
const config_servers = get_enabled_servers(await read_claude_config());
|
|
437
|
+
const config_by_name = new Map(config_servers.map((s) => [s.name, s]));
|
|
438
|
+
const known_names = /* @__PURE__ */ new Set();
|
|
439
|
+
let registry_updated = false;
|
|
440
|
+
for (let i = 0; i < registry.servers.length; i++) {
|
|
441
|
+
const name = registry.servers[i].name;
|
|
442
|
+
known_names.add(name);
|
|
443
|
+
const config_server = config_by_name.get(name);
|
|
444
|
+
if (config_server) {
|
|
445
|
+
registry.servers[i] = config_server;
|
|
446
|
+
registry_updated = true;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
for (const server of config_servers) if (!known_names.has(server.name)) {
|
|
450
|
+
registry.servers.push(server);
|
|
451
|
+
registry_updated = true;
|
|
452
|
+
}
|
|
453
|
+
if (registry_updated) await write_server_registry(registry);
|
|
454
|
+
return registry.servers;
|
|
455
|
+
}
|
|
456
|
+
async function sync_servers_to_registry(servers) {
|
|
457
|
+
const registry = await read_server_registry();
|
|
458
|
+
servers.forEach((server) => {
|
|
459
|
+
const existing_index = registry.servers.findIndex((s) => s.name === server.name);
|
|
460
|
+
if (existing_index >= 0) registry.servers[existing_index] = server;
|
|
461
|
+
else registry.servers.push(server);
|
|
462
|
+
});
|
|
463
|
+
await write_server_registry(registry);
|
|
464
|
+
}
|
|
465
|
+
function parse_backups(prefix, pattern) {
|
|
466
|
+
return async () => {
|
|
467
|
+
const backups_dir = get_backups_dir();
|
|
468
|
+
try {
|
|
469
|
+
await access(backups_dir);
|
|
470
|
+
return (await readdir(backups_dir)).filter((file) => file.startsWith(prefix) && file.endsWith(".json")).map((file) => {
|
|
471
|
+
const timestamp_match = file.match(pattern);
|
|
472
|
+
if (!timestamp_match) return null;
|
|
473
|
+
const [, year, month, day, hour, minute, second] = timestamp_match;
|
|
474
|
+
return {
|
|
475
|
+
filename: file,
|
|
476
|
+
timestamp: new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second)),
|
|
477
|
+
path: join(backups_dir, file)
|
|
478
|
+
};
|
|
479
|
+
}).filter((backup) => backup !== null).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const list_backups = parse_backups("mcp-servers-", /mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
487
|
+
const list_plugin_backups = parse_backups("plugins-", /plugins-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/utils/claude-cli.ts
|
|
490
|
+
const exec_file_async$1 = promisify(execFile);
|
|
491
|
+
/**
|
|
492
|
+
* Check if Claude CLI is available
|
|
493
|
+
*/
|
|
494
|
+
async function check_claude_cli() {
|
|
495
|
+
try {
|
|
496
|
+
await exec_file_async$1("claude", ["--version"]);
|
|
497
|
+
return true;
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Validate environment variable key.
|
|
504
|
+
* Must start with letter or underscore, contain only alphanumeric and underscores.
|
|
505
|
+
*/
|
|
506
|
+
function is_valid_env_key(key) {
|
|
507
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Build args array for claude mcp add command.
|
|
511
|
+
* Returns raw args — no shell escaping needed since we use execFile.
|
|
512
|
+
*/
|
|
513
|
+
function build_add_args(server, scope) {
|
|
514
|
+
const args = ["mcp", "add"];
|
|
515
|
+
args.push(server.name);
|
|
516
|
+
const transport = server.type || "stdio";
|
|
517
|
+
if (transport !== "stdio") args.push("--transport", transport);
|
|
518
|
+
args.push("--scope", scope);
|
|
519
|
+
if (transport === "stdio") {
|
|
520
|
+
if (server.env) {
|
|
521
|
+
for (const [key, value] of Object.entries(server.env)) if (is_valid_env_key(key)) args.push("-e", `${key}=${value}`);
|
|
522
|
+
}
|
|
523
|
+
if ("command" in server && server.command) {
|
|
524
|
+
args.push("--");
|
|
525
|
+
args.push(server.command);
|
|
526
|
+
if (server.args && server.args.length > 0) args.push(...server.args);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
if ("url" in server && server.url) args.push(server.url);
|
|
530
|
+
if ("headers" in server && server.headers) for (const [key, value] of Object.entries(server.headers)) args.push("-H", `${key}: ${value}`);
|
|
531
|
+
}
|
|
532
|
+
return args;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Run a claude CLI command using execFile (no shell).
|
|
536
|
+
* This avoids all shell escaping issues on every platform.
|
|
537
|
+
*/
|
|
538
|
+
async function run_claude(args) {
|
|
539
|
+
const result = await exec_file_async$1("claude", args);
|
|
540
|
+
return {
|
|
541
|
+
stdout: redact_text(result.stdout),
|
|
542
|
+
stderr: redact_text(result.stderr)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function get_redacted_error_message(error) {
|
|
546
|
+
return redact_text(error instanceof Error ? error.message : "Unknown error");
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Add an MCP server using Claude CLI
|
|
550
|
+
*/
|
|
551
|
+
async function add_mcp_via_cli(server, scope) {
|
|
552
|
+
if (!await check_claude_cli()) return {
|
|
553
|
+
success: false,
|
|
554
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
555
|
+
};
|
|
556
|
+
try {
|
|
557
|
+
await run_claude(build_add_args(server, scope));
|
|
558
|
+
return { success: true };
|
|
559
|
+
} catch (error) {
|
|
560
|
+
return {
|
|
561
|
+
success: false,
|
|
562
|
+
error: `Failed to add server via CLI: ${get_redacted_error_message(error)}`
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Remove an MCP server using Claude CLI
|
|
568
|
+
*/
|
|
569
|
+
async function remove_mcp_via_cli(name) {
|
|
570
|
+
if (!await check_claude_cli()) return {
|
|
571
|
+
success: false,
|
|
572
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
573
|
+
};
|
|
574
|
+
try {
|
|
575
|
+
await run_claude([
|
|
576
|
+
"mcp",
|
|
577
|
+
"remove",
|
|
578
|
+
name
|
|
579
|
+
]);
|
|
580
|
+
return { success: true };
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: `Failed to remove server via CLI: ${get_redacted_error_message(error)}`
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Install a plugin via Claude CLI
|
|
590
|
+
*/
|
|
591
|
+
async function install_plugin_via_cli(key, scope = "user") {
|
|
592
|
+
if (!await check_claude_cli()) return {
|
|
593
|
+
success: false,
|
|
594
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
595
|
+
};
|
|
596
|
+
try {
|
|
597
|
+
await run_claude([
|
|
598
|
+
"plugin",
|
|
599
|
+
"install",
|
|
600
|
+
key,
|
|
601
|
+
"--scope",
|
|
602
|
+
scope
|
|
603
|
+
]);
|
|
604
|
+
return { success: true };
|
|
605
|
+
} catch (error) {
|
|
606
|
+
return {
|
|
607
|
+
success: false,
|
|
608
|
+
error: `Failed to install plugin: ${get_redacted_error_message(error)}`
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Uninstall a plugin via Claude CLI
|
|
614
|
+
*/
|
|
615
|
+
async function uninstall_plugin_via_cli(key, scope = "user") {
|
|
616
|
+
if (!await check_claude_cli()) return {
|
|
617
|
+
success: false,
|
|
618
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
619
|
+
};
|
|
620
|
+
try {
|
|
621
|
+
await run_claude([
|
|
622
|
+
"plugin",
|
|
623
|
+
"uninstall",
|
|
624
|
+
key,
|
|
625
|
+
"--scope",
|
|
626
|
+
scope
|
|
627
|
+
]);
|
|
628
|
+
return { success: true };
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
error: `Failed to uninstall plugin: ${get_redacted_error_message(error)}`
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Update a plugin via Claude CLI
|
|
638
|
+
*/
|
|
639
|
+
async function update_plugin_via_cli(key, scope = "user") {
|
|
640
|
+
if (!await check_claude_cli()) return {
|
|
641
|
+
success: false,
|
|
642
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
643
|
+
};
|
|
644
|
+
try {
|
|
645
|
+
await run_claude([
|
|
646
|
+
"plugin",
|
|
647
|
+
"update",
|
|
648
|
+
key,
|
|
649
|
+
"--scope",
|
|
650
|
+
scope
|
|
651
|
+
]);
|
|
652
|
+
return { success: true };
|
|
653
|
+
} catch (error) {
|
|
654
|
+
return {
|
|
655
|
+
success: false,
|
|
656
|
+
error: `Failed to update plugin: ${get_redacted_error_message(error)}`
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Extract GitHub owner/repo from various source formats.
|
|
662
|
+
* Returns null if not a recognizable GitHub reference.
|
|
663
|
+
*/
|
|
664
|
+
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)?$/);
|
|
671
|
+
if (ssh_match) return {
|
|
672
|
+
owner: ssh_match[1],
|
|
673
|
+
repo: ssh_match[2]
|
|
674
|
+
};
|
|
675
|
+
const shorthand_match = source.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
676
|
+
if (shorthand_match) return {
|
|
677
|
+
owner: shorthand_match[1],
|
|
678
|
+
repo: shorthand_match[2]
|
|
679
|
+
};
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
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) {
|
|
687
|
+
try {
|
|
688
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
689
|
+
method: "GET",
|
|
690
|
+
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}'.`;
|
|
696
|
+
} catch {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async function marketplace_add_via_cli(source) {
|
|
701
|
+
if (!await check_claude_cli()) return {
|
|
702
|
+
success: false,
|
|
703
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
704
|
+
};
|
|
705
|
+
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);
|
|
709
|
+
if (validation_error) return {
|
|
710
|
+
success: false,
|
|
711
|
+
error: validation_error
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
await run_claude([
|
|
716
|
+
"plugin",
|
|
717
|
+
"marketplace",
|
|
718
|
+
"add",
|
|
719
|
+
source
|
|
720
|
+
]);
|
|
721
|
+
return { success: true };
|
|
722
|
+
} catch (error) {
|
|
723
|
+
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 {
|
|
729
|
+
success: false,
|
|
730
|
+
error: `Repository '${source}' not found. Check the name and your access permissions.`
|
|
731
|
+
};
|
|
732
|
+
return {
|
|
733
|
+
success: false,
|
|
734
|
+
error: `Failed to add marketplace: ${message}`
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Remove a marketplace via Claude CLI
|
|
740
|
+
*/
|
|
741
|
+
async function marketplace_remove_via_cli(name) {
|
|
742
|
+
if (!await check_claude_cli()) return {
|
|
743
|
+
success: false,
|
|
744
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
745
|
+
};
|
|
746
|
+
try {
|
|
747
|
+
await run_claude([
|
|
748
|
+
"plugin",
|
|
749
|
+
"marketplace",
|
|
750
|
+
"remove",
|
|
751
|
+
name
|
|
752
|
+
]);
|
|
753
|
+
return { success: true };
|
|
754
|
+
} catch (error) {
|
|
755
|
+
return {
|
|
756
|
+
success: false,
|
|
757
|
+
error: `Failed to remove marketplace: ${get_redacted_error_message(error)}`
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Update marketplace(s) via Claude CLI
|
|
763
|
+
*/
|
|
764
|
+
async function marketplace_update_via_cli(name) {
|
|
765
|
+
if (!await check_claude_cli()) return {
|
|
766
|
+
success: false,
|
|
767
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
768
|
+
};
|
|
769
|
+
try {
|
|
770
|
+
const args = [
|
|
771
|
+
"plugin",
|
|
772
|
+
"marketplace",
|
|
773
|
+
"update"
|
|
774
|
+
];
|
|
775
|
+
if (name) args.push(name);
|
|
776
|
+
await run_claude(args);
|
|
777
|
+
return { success: true };
|
|
778
|
+
} catch (error) {
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
error: `Failed to update marketplace: ${get_redacted_error_message(error)}`
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* List marketplaces via Claude CLI
|
|
787
|
+
*/
|
|
788
|
+
async function marketplace_list_via_cli() {
|
|
789
|
+
if (!await check_claude_cli()) return {
|
|
790
|
+
success: false,
|
|
791
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
792
|
+
};
|
|
793
|
+
try {
|
|
794
|
+
const { stdout } = await run_claude([
|
|
795
|
+
"plugin",
|
|
796
|
+
"marketplace",
|
|
797
|
+
"list"
|
|
798
|
+
]);
|
|
799
|
+
return {
|
|
800
|
+
success: true,
|
|
801
|
+
stdout: stdout.trim()
|
|
802
|
+
};
|
|
803
|
+
} catch (error) {
|
|
804
|
+
return {
|
|
805
|
+
success: false,
|
|
806
|
+
error: `Failed to list marketplaces: ${get_redacted_error_message(error)}`
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Get the scope description for display
|
|
812
|
+
*/
|
|
813
|
+
function get_scope_description(scope) {
|
|
814
|
+
switch (scope) {
|
|
815
|
+
case "local": return "This project only (default)";
|
|
816
|
+
case "project": return "Shared via .mcp.json (version controlled)";
|
|
817
|
+
case "user": return "Global - all projects";
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Validate a plugin or marketplace manifest via Claude CLI
|
|
822
|
+
*/
|
|
823
|
+
async function validate_plugin_via_cli(path) {
|
|
824
|
+
if (!await check_claude_cli()) return {
|
|
825
|
+
success: false,
|
|
826
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
827
|
+
};
|
|
828
|
+
try {
|
|
829
|
+
const { stdout } = await run_claude([
|
|
830
|
+
"plugin",
|
|
831
|
+
"validate",
|
|
832
|
+
path
|
|
833
|
+
]);
|
|
834
|
+
return {
|
|
835
|
+
success: true,
|
|
836
|
+
stdout: stdout.trim()
|
|
837
|
+
};
|
|
838
|
+
} catch (error) {
|
|
839
|
+
return {
|
|
840
|
+
success: false,
|
|
841
|
+
error: `Validation failed: ${get_redacted_error_message(error)}`
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get details about an MCP server via Claude CLI
|
|
847
|
+
*/
|
|
848
|
+
async function mcp_get_via_cli(name) {
|
|
849
|
+
if (!await check_claude_cli()) return {
|
|
850
|
+
success: false,
|
|
851
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
852
|
+
};
|
|
853
|
+
try {
|
|
854
|
+
const { stdout } = await run_claude([
|
|
855
|
+
"mcp",
|
|
856
|
+
"get",
|
|
857
|
+
name
|
|
858
|
+
]);
|
|
859
|
+
return {
|
|
860
|
+
success: true,
|
|
861
|
+
stdout: stdout.trim()
|
|
862
|
+
};
|
|
863
|
+
} catch (error) {
|
|
864
|
+
return {
|
|
865
|
+
success: false,
|
|
866
|
+
error: `Failed to get server details: ${get_redacted_error_message(error)}`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Add an MCP server from raw JSON via Claude CLI
|
|
872
|
+
*/
|
|
873
|
+
async function mcp_add_json_via_cli(name, json, scope = "local") {
|
|
874
|
+
if (!await check_claude_cli()) return {
|
|
875
|
+
success: false,
|
|
876
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
877
|
+
};
|
|
878
|
+
try {
|
|
879
|
+
await run_claude([
|
|
880
|
+
"mcp",
|
|
881
|
+
"add-json",
|
|
882
|
+
name,
|
|
883
|
+
json,
|
|
884
|
+
"--scope",
|
|
885
|
+
scope
|
|
886
|
+
]);
|
|
887
|
+
return { success: true };
|
|
888
|
+
} catch (error) {
|
|
889
|
+
return {
|
|
890
|
+
success: false,
|
|
891
|
+
error: `Failed to add server from JSON: ${get_redacted_error_message(error)}`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Reset project-scoped MCP server choices via Claude CLI
|
|
897
|
+
*/
|
|
898
|
+
async function mcp_reset_project_choices_via_cli() {
|
|
899
|
+
if (!await check_claude_cli()) return {
|
|
900
|
+
success: false,
|
|
901
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
902
|
+
};
|
|
903
|
+
try {
|
|
904
|
+
await run_claude(["mcp", "reset-project-choices"]);
|
|
905
|
+
return { success: true };
|
|
906
|
+
} catch (error) {
|
|
907
|
+
return {
|
|
908
|
+
success: false,
|
|
909
|
+
error: `Failed to reset project choices: ${get_redacted_error_message(error)}`
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Get scope options for select prompt
|
|
915
|
+
*/
|
|
916
|
+
function get_scope_options() {
|
|
917
|
+
return [
|
|
918
|
+
{
|
|
919
|
+
value: "local",
|
|
920
|
+
label: "Local",
|
|
921
|
+
hint: "This project only (default)"
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
value: "project",
|
|
925
|
+
label: "Project",
|
|
926
|
+
hint: "Shared via .mcp.json (git)"
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
value: "user",
|
|
930
|
+
label: "User (Global)",
|
|
931
|
+
hint: "Available in all projects"
|
|
932
|
+
}
|
|
933
|
+
];
|
|
934
|
+
}
|
|
935
|
+
//#endregion
|
|
254
936
|
//#region src/commands/edit-config.ts
|
|
255
937
|
async function edit_config() {
|
|
256
938
|
try {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
939
|
+
const client_id = await select({
|
|
940
|
+
message: "Which MCP client do you want to edit?",
|
|
941
|
+
options: client_adapters.map((adapter) => ({
|
|
942
|
+
value: adapter.id,
|
|
943
|
+
label: adapter.label
|
|
944
|
+
})),
|
|
945
|
+
initialValue: "claude-code"
|
|
262
946
|
});
|
|
263
|
-
if (typeof
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
if (current_servers.length > 0) {
|
|
269
|
-
await sync_servers_to_registry(current_servers);
|
|
270
|
-
all_servers = current_servers;
|
|
271
|
-
note(`Imported ${current_servers.length} servers from your .claude.json file into registry.`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
if (all_servers.length === 0) {
|
|
275
|
-
note("No MCP servers found in .claude.json or registry. Add servers first.");
|
|
947
|
+
if (typeof client_id === "symbol") return;
|
|
948
|
+
const adapter = client_adapters.find((candidate) => candidate.id === client_id);
|
|
949
|
+
if (!adapter) return;
|
|
950
|
+
if (adapter.id === "claude-code") {
|
|
951
|
+
await edit_claude_config();
|
|
276
952
|
return;
|
|
277
953
|
}
|
|
278
|
-
|
|
279
|
-
|
|
954
|
+
await edit_client_config(adapter);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
throw new Error(`Failed to edit configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async function edit_client_config(adapter) {
|
|
960
|
+
if (!adapter.writeEnabled) {
|
|
961
|
+
note(`${adapter.label} support is read-only for now.`);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const location = await select_config_location(adapter);
|
|
965
|
+
if (!location) return;
|
|
966
|
+
const servers = await adapter.readLocation(location);
|
|
967
|
+
if (servers.length === 0) {
|
|
968
|
+
note(`No MCP servers found at ${location.path}.`);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const selected_names = await multiselect({
|
|
972
|
+
message: `Toggle MCP servers for ${adapter.label}:`,
|
|
973
|
+
options: servers.map((server) => ({
|
|
974
|
+
value: server.name,
|
|
975
|
+
label: server.name,
|
|
976
|
+
hint: server_hint(server)
|
|
977
|
+
})),
|
|
978
|
+
initialValues: servers.filter((server) => server.disabled !== true).map((server) => server.name),
|
|
979
|
+
required: false
|
|
980
|
+
});
|
|
981
|
+
if (typeof selected_names === "symbol") return;
|
|
982
|
+
await adapter.writeEnabled(location, selected_names);
|
|
983
|
+
note(`Configuration updated!\nClient: ${adapter.label}\nConfig: ${location.path}\nEnabled servers: ${selected_names.length}`);
|
|
984
|
+
}
|
|
985
|
+
async function select_config_location(adapter) {
|
|
986
|
+
const locations = adapter.locations();
|
|
987
|
+
if (locations.length === 1) return locations[0];
|
|
988
|
+
const location_path = await select({
|
|
989
|
+
message: `Which ${adapter.label} configuration do you want to edit?`,
|
|
990
|
+
options: locations.map((location) => ({
|
|
991
|
+
value: location.path,
|
|
992
|
+
label: `${location.scope} — ${location.description}`,
|
|
993
|
+
hint: location.path
|
|
994
|
+
}))
|
|
995
|
+
});
|
|
996
|
+
if (typeof location_path === "symbol") return null;
|
|
997
|
+
return locations.find((location) => location.path === location_path) ?? null;
|
|
998
|
+
}
|
|
999
|
+
function server_hint(server) {
|
|
1000
|
+
return [
|
|
1001
|
+
server.disabled === true ? "off" : "on",
|
|
1002
|
+
server.command ? [server.command, ...server.args ?? []].join(" ") : server.url ? redact_url(server.url) : server.transport,
|
|
1003
|
+
server.description
|
|
1004
|
+
].filter(Boolean).join(" · ");
|
|
1005
|
+
}
|
|
1006
|
+
async function edit_claude_config() {
|
|
1007
|
+
const cli_available = await check_claude_cli();
|
|
1008
|
+
const scope = await select({
|
|
1009
|
+
message: "Which Claude Code configuration do you want to edit?",
|
|
1010
|
+
options: get_scope_options(),
|
|
1011
|
+
initialValue: "local"
|
|
1012
|
+
});
|
|
1013
|
+
if (typeof scope === "symbol") return;
|
|
1014
|
+
const current_config = await read_claude_config();
|
|
1015
|
+
let all_servers = await get_all_available_servers();
|
|
1016
|
+
if (all_servers.length === 0 && current_config.mcpServers) {
|
|
1017
|
+
const current_servers = get_enabled_servers(current_config);
|
|
1018
|
+
if (current_servers.length > 0) {
|
|
1019
|
+
await sync_servers_to_registry(current_servers);
|
|
1020
|
+
all_servers = current_servers;
|
|
1021
|
+
note(`Imported ${current_servers.length} servers from your .claude.json file into registry.`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (all_servers.length === 0) {
|
|
1025
|
+
note("No MCP servers found in .claude.json or registry. Add servers with the CLI first.");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const currently_enabled = await get_enabled_servers_for_scope(scope);
|
|
1029
|
+
const selected_server_names = await multiselect({
|
|
1030
|
+
message: `Select MCP servers for ${get_scope_description(scope)}:`,
|
|
1031
|
+
options: all_servers.map((server) => ({
|
|
280
1032
|
value: server.name,
|
|
281
1033
|
label: server.name,
|
|
282
1034
|
hint: server.description || ""
|
|
283
|
-
}))
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const server = all_servers.find((s) => s.name === name);
|
|
299
|
-
if (server) {
|
|
300
|
-
const result = await add_mcp_via_cli(server, scope);
|
|
301
|
-
if (result.success) success_count++;
|
|
302
|
-
else {
|
|
303
|
-
error_count++;
|
|
304
|
-
log.warn(`Failed to add ${name}: ${result.error}`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
for (const name of servers_to_remove) {
|
|
309
|
-
const result = await remove_mcp_via_cli(name);
|
|
310
|
-
if (result.success) success_count++;
|
|
311
|
-
else {
|
|
1035
|
+
})),
|
|
1036
|
+
initialValues: currently_enabled,
|
|
1037
|
+
required: false
|
|
1038
|
+
});
|
|
1039
|
+
if (typeof selected_server_names === "symbol") return;
|
|
1040
|
+
const selected_servers = all_servers.filter((server) => selected_server_names.includes(server.name));
|
|
1041
|
+
const servers_to_add = selected_server_names.filter((name) => !currently_enabled.includes(name));
|
|
1042
|
+
const servers_to_remove = currently_enabled.filter((name) => !selected_server_names.includes(name));
|
|
1043
|
+
if (cli_available && (scope === "local" || scope === "project")) {
|
|
1044
|
+
let error_count = 0;
|
|
1045
|
+
for (const name of servers_to_add) {
|
|
1046
|
+
const server = all_servers.find((s) => s.name === name);
|
|
1047
|
+
if (server) {
|
|
1048
|
+
const result = await add_mcp_via_cli(server, scope);
|
|
1049
|
+
if (!result.success) {
|
|
312
1050
|
error_count++;
|
|
313
|
-
log.warn(`Failed to
|
|
1051
|
+
log.warn(`Failed to add ${name}: ${result.error}`);
|
|
314
1052
|
}
|
|
315
1053
|
}
|
|
316
|
-
await sync_servers_to_registry(selected_servers);
|
|
317
|
-
if (error_count > 0) note(`Configuration updated with ${error_count} errors.\nScope: ${get_scope_description(scope)}\nAdded: ${servers_to_add.length}, Removed: ${servers_to_remove.length}`);
|
|
318
|
-
else note(`Configuration updated!\nScope: ${get_scope_description(scope)}\nEnabled servers: ${selected_servers.length}`);
|
|
319
|
-
} else {
|
|
320
|
-
await write_claude_config(create_config_from_servers(selected_servers));
|
|
321
|
-
await sync_servers_to_registry(selected_servers);
|
|
322
|
-
if (!cli_available && scope !== "user") log.warn(`Claude CLI not available. Changes written to ~/.claude.json (user scope) instead of ${scope} scope.`);
|
|
323
|
-
note(`Configuration updated!\nEnabled servers: ${selected_servers.length}`);
|
|
324
1054
|
}
|
|
325
|
-
|
|
326
|
-
|
|
1055
|
+
for (const name of servers_to_remove) {
|
|
1056
|
+
const result = await remove_mcp_via_cli(name);
|
|
1057
|
+
if (!result.success) {
|
|
1058
|
+
error_count++;
|
|
1059
|
+
log.warn(`Failed to remove ${name}: ${result.error}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
await sync_servers_to_registry(selected_servers);
|
|
1063
|
+
if (error_count > 0) note(`Configuration updated with ${error_count} errors.\nScope: ${get_scope_description(scope)}\nAdded: ${servers_to_add.length}, Removed: ${servers_to_remove.length}`);
|
|
1064
|
+
else note(`Configuration updated!\nScope: ${get_scope_description(scope)}\nEnabled servers: ${selected_servers.length}`);
|
|
1065
|
+
} else {
|
|
1066
|
+
await write_claude_config(create_config_from_servers(selected_servers));
|
|
1067
|
+
await sync_servers_to_registry(selected_servers);
|
|
1068
|
+
if (!cli_available && scope !== "user") log.warn(`Claude CLI not available. Changes written to ~/.claude.json (user scope) instead of ${scope} scope.`);
|
|
1069
|
+
note(`Configuration updated!\nEnabled servers: ${selected_servers.length}`);
|
|
327
1070
|
}
|
|
328
1071
|
}
|
|
329
1072
|
//#endregion
|
|
@@ -604,6 +1347,171 @@ async function manage_cache() {
|
|
|
604
1347
|
}
|
|
605
1348
|
}
|
|
606
1349
|
//#endregion
|
|
1350
|
+
//#region src/core/hook-state.ts
|
|
1351
|
+
async function read_disabled_hooks() {
|
|
1352
|
+
try {
|
|
1353
|
+
const content = await readFile(get_disabled_hooks_path(), "utf-8");
|
|
1354
|
+
return JSON.parse(content);
|
|
1355
|
+
} catch {
|
|
1356
|
+
return [];
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
async function write_disabled_hooks(entries) {
|
|
1360
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
1361
|
+
await writeFile(get_disabled_hooks_path(), JSON.stringify(entries, null, " "), "utf-8");
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Remove a specific hook handler from a hooks.json file by matching the handler.
|
|
1365
|
+
* Returns true if the hook was found and removed.
|
|
1366
|
+
*/
|
|
1367
|
+
async function remove_hook_from_file(hooks_path, event, handler) {
|
|
1368
|
+
let content;
|
|
1369
|
+
try {
|
|
1370
|
+
content = await readFile(hooks_path, "utf-8");
|
|
1371
|
+
} catch {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
const hooks_data = JSON.parse(content);
|
|
1375
|
+
const hooks_obj = hooks_data.hooks || hooks_data;
|
|
1376
|
+
const matchers = hooks_obj[event];
|
|
1377
|
+
if (!matchers) return false;
|
|
1378
|
+
let removed = false;
|
|
1379
|
+
for (const m of matchers) {
|
|
1380
|
+
const idx = m.hooks?.findIndex((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt);
|
|
1381
|
+
if (idx !== void 0 && idx >= 0) {
|
|
1382
|
+
m.hooks.splice(idx, 1);
|
|
1383
|
+
removed = true;
|
|
1384
|
+
if (m.hooks.length === 0) matchers.splice(matchers.indexOf(m), 1);
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (!removed) return false;
|
|
1389
|
+
if (matchers.length === 0) delete hooks_obj[event];
|
|
1390
|
+
await writeFile(hooks_path, JSON.stringify(hooks_data, null, " "), "utf-8");
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Get all hooks.json paths for a plugin (cache + marketplace source).
|
|
1395
|
+
*/
|
|
1396
|
+
function get_all_hooks_paths(plugin_key, primary_path) {
|
|
1397
|
+
const paths = [primary_path];
|
|
1398
|
+
const at_index = plugin_key.lastIndexOf("@");
|
|
1399
|
+
if (at_index > 0) {
|
|
1400
|
+
const plugin_name = plugin_key.substring(0, at_index);
|
|
1401
|
+
const marketplace_name = plugin_key.substring(at_index + 1);
|
|
1402
|
+
paths.push(join(get_marketplaces_dir(), marketplace_name, "plugins", plugin_name, "hooks", "hooks.json"));
|
|
1403
|
+
}
|
|
1404
|
+
return [...new Set(paths)];
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Disable a specific hook from a plugin.
|
|
1408
|
+
* Removes from both cache and marketplace source hooks.json files.
|
|
1409
|
+
*/
|
|
1410
|
+
async function disable_plugin_hook(entry) {
|
|
1411
|
+
if (!entry.hooks_json_path || !entry.plugin_key) throw new Error("Not a plugin hook");
|
|
1412
|
+
const disabled = await read_disabled_hooks();
|
|
1413
|
+
disabled.push({
|
|
1414
|
+
plugin_key: entry.plugin_key,
|
|
1415
|
+
hooks_json_path: entry.hooks_json_path,
|
|
1416
|
+
event: entry.event,
|
|
1417
|
+
matcher: entry.matcher,
|
|
1418
|
+
matcher_index: entry.matcher_index,
|
|
1419
|
+
hook_index: entry.hook_index,
|
|
1420
|
+
original_handler: entry.handler,
|
|
1421
|
+
disabled_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1422
|
+
});
|
|
1423
|
+
await write_disabled_hooks(disabled);
|
|
1424
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
1425
|
+
for (const hooks_path of all_paths) await remove_hook_from_file(hooks_path, entry.event, entry.handler);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Add a hook handler back into a hooks.json file.
|
|
1429
|
+
*/
|
|
1430
|
+
async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
|
|
1431
|
+
let hooks_data;
|
|
1432
|
+
try {
|
|
1433
|
+
const content = await readFile(hooks_path, "utf-8");
|
|
1434
|
+
hooks_data = JSON.parse(content);
|
|
1435
|
+
} catch {
|
|
1436
|
+
hooks_data = { hooks: {} };
|
|
1437
|
+
}
|
|
1438
|
+
const hooks_obj = hooks_data.hooks || (hooks_data.hooks = {});
|
|
1439
|
+
if (!hooks_obj[event]) hooks_obj[event] = [];
|
|
1440
|
+
const matchers = hooks_obj[event];
|
|
1441
|
+
let matcher = matchers.find((m) => (m.matcher || void 0) === matcher_pattern);
|
|
1442
|
+
if (!matcher) {
|
|
1443
|
+
matcher = { hooks: [] };
|
|
1444
|
+
if (matcher_pattern) matcher.matcher = matcher_pattern;
|
|
1445
|
+
matchers.push(matcher);
|
|
1446
|
+
}
|
|
1447
|
+
if (matcher.hooks.some((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt)) return;
|
|
1448
|
+
matcher.hooks.push(handler);
|
|
1449
|
+
await writeFile(hooks_path, JSON.stringify(hooks_data, null, " "), "utf-8");
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Re-enable a previously disabled plugin hook.
|
|
1453
|
+
* Restores to both cache and marketplace source hooks.json files.
|
|
1454
|
+
*/
|
|
1455
|
+
async function enable_plugin_hook(disabled_entry) {
|
|
1456
|
+
const all_paths = get_all_hooks_paths(disabled_entry.plugin_key, disabled_entry.hooks_json_path);
|
|
1457
|
+
for (const hooks_path of all_paths) await add_hook_to_file(hooks_path, disabled_entry.event, disabled_entry.matcher, disabled_entry.original_handler);
|
|
1458
|
+
await write_disabled_hooks((await read_disabled_hooks()).filter((d) => !(d.plugin_key === disabled_entry.plugin_key && d.event === disabled_entry.event && d.disabled_at === disabled_entry.disabled_at)));
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Check if any previously disabled hooks have been restored (e.g. by marketplace update).
|
|
1462
|
+
* Returns entries that were re-added and need to be re-disabled.
|
|
1463
|
+
*/
|
|
1464
|
+
async function check_restored_hooks() {
|
|
1465
|
+
const disabled = await read_disabled_hooks();
|
|
1466
|
+
if (disabled.length === 0) return [];
|
|
1467
|
+
const restored = [];
|
|
1468
|
+
for (const entry of disabled) {
|
|
1469
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
1470
|
+
let found = false;
|
|
1471
|
+
for (const hooks_path of all_paths) {
|
|
1472
|
+
let hooks_data;
|
|
1473
|
+
try {
|
|
1474
|
+
const content = await readFile(hooks_path, "utf-8");
|
|
1475
|
+
hooks_data = JSON.parse(content);
|
|
1476
|
+
} catch {
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const matchers = (hooks_data.hooks || hooks_data)[entry.event];
|
|
1480
|
+
if (!matchers) continue;
|
|
1481
|
+
for (const m of matchers) {
|
|
1482
|
+
if ((m.matcher || void 0) !== entry.matcher) continue;
|
|
1483
|
+
if (m.hooks?.some((h) => h.type === entry.original_handler.type && (h.command === entry.original_handler.command || h.url === entry.original_handler.url || h.prompt === entry.original_handler.prompt))) {
|
|
1484
|
+
found = true;
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (found) break;
|
|
1489
|
+
}
|
|
1490
|
+
if (found) restored.push(entry);
|
|
1491
|
+
}
|
|
1492
|
+
return restored;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Re-disable hooks that were restored by a marketplace update.
|
|
1496
|
+
*/
|
|
1497
|
+
async function redisable_restored_hooks(restored) {
|
|
1498
|
+
let success = 0;
|
|
1499
|
+
let failed = 0;
|
|
1500
|
+
for (const entry of restored) try {
|
|
1501
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
1502
|
+
let any_removed = false;
|
|
1503
|
+
for (const hooks_path of all_paths) if (await remove_hook_from_file(hooks_path, entry.event, entry.original_handler)) any_removed = true;
|
|
1504
|
+
if (any_removed) success++;
|
|
1505
|
+
else failed++;
|
|
1506
|
+
} catch {
|
|
1507
|
+
failed++;
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
success,
|
|
1511
|
+
failed
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
//#endregion
|
|
607
1515
|
//#region src/commands/manage-hooks.ts
|
|
608
1516
|
function format_hook(entry) {
|
|
609
1517
|
const detail = entry.handler.command || entry.handler.url || entry.handler.prompt || "(unknown)";
|
|
@@ -902,6 +1810,166 @@ async function manage_marketplace() {
|
|
|
902
1810
|
}
|
|
903
1811
|
}
|
|
904
1812
|
//#endregion
|
|
1813
|
+
//#region src/utils/skills-cli.ts
|
|
1814
|
+
const exec_file_async = promisify(execFile);
|
|
1815
|
+
function split_cli_list(value) {
|
|
1816
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1817
|
+
}
|
|
1818
|
+
async function run_skills_cli(args) {
|
|
1819
|
+
try {
|
|
1820
|
+
const result = await exec_file_async("npx", [
|
|
1821
|
+
"-y",
|
|
1822
|
+
"skills@latest",
|
|
1823
|
+
...args
|
|
1824
|
+
], { env: {
|
|
1825
|
+
...process.env,
|
|
1826
|
+
CI: "1",
|
|
1827
|
+
NO_COLOR: "1",
|
|
1828
|
+
FORCE_COLOR: "0",
|
|
1829
|
+
TERM: "dumb"
|
|
1830
|
+
} });
|
|
1831
|
+
return {
|
|
1832
|
+
success: true,
|
|
1833
|
+
stdout: redact_text(result.stdout.trim()),
|
|
1834
|
+
stderr: redact_text(result.stderr.trim())
|
|
1835
|
+
};
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
const err = error;
|
|
1838
|
+
return {
|
|
1839
|
+
success: false,
|
|
1840
|
+
stdout: err.stdout ? redact_text(err.stdout.trim()) : void 0,
|
|
1841
|
+
stderr: err.stderr ? redact_text(err.stderr.trim()) : void 0,
|
|
1842
|
+
error: redact_text(err.message)
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
//#endregion
|
|
1847
|
+
//#region src/commands/manage-skills.ts
|
|
1848
|
+
const SKILL_AGENTS = [
|
|
1849
|
+
{
|
|
1850
|
+
value: "claude-code",
|
|
1851
|
+
label: "Claude Code"
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
value: "pi",
|
|
1855
|
+
label: "Pi"
|
|
1856
|
+
},
|
|
1857
|
+
{
|
|
1858
|
+
value: "opencode",
|
|
1859
|
+
label: "OpenCode"
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
value: "codex",
|
|
1863
|
+
label: "Codex"
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
value: "cursor",
|
|
1867
|
+
label: "Cursor"
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
value: "windsurf",
|
|
1871
|
+
label: "Windsurf"
|
|
1872
|
+
}
|
|
1873
|
+
];
|
|
1874
|
+
async function manage_skills() {
|
|
1875
|
+
const action = await select({
|
|
1876
|
+
message: "Portable skills",
|
|
1877
|
+
options: [
|
|
1878
|
+
{
|
|
1879
|
+
value: "list",
|
|
1880
|
+
label: "List installed skills"
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
value: "available",
|
|
1884
|
+
label: "List skills available from source"
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
value: "install",
|
|
1888
|
+
label: "Install skills"
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
value: "update",
|
|
1892
|
+
label: "Update skills"
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
value: "back",
|
|
1896
|
+
label: "Back"
|
|
1897
|
+
}
|
|
1898
|
+
]
|
|
1899
|
+
});
|
|
1900
|
+
if (typeof action === "symbol" || action === "back") return;
|
|
1901
|
+
if (action === "list") {
|
|
1902
|
+
const agent = await select_agent();
|
|
1903
|
+
if (!agent) return;
|
|
1904
|
+
await show_result(await run_skills_cli([
|
|
1905
|
+
"list",
|
|
1906
|
+
"--agent",
|
|
1907
|
+
agent
|
|
1908
|
+
]));
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (action === "available") {
|
|
1912
|
+
const source = await prompt_source();
|
|
1913
|
+
if (!source) return;
|
|
1914
|
+
await show_result(await run_skills_cli([
|
|
1915
|
+
"add",
|
|
1916
|
+
source,
|
|
1917
|
+
"--list"
|
|
1918
|
+
]));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (action === "install") {
|
|
1922
|
+
const source = await prompt_source();
|
|
1923
|
+
if (!source) return;
|
|
1924
|
+
const agent = await select_agent();
|
|
1925
|
+
if (!agent) return;
|
|
1926
|
+
const skill = await text({
|
|
1927
|
+
message: "Skill name or * for all skills:",
|
|
1928
|
+
placeholder: "svelte-runes",
|
|
1929
|
+
defaultValue: "*"
|
|
1930
|
+
});
|
|
1931
|
+
if (typeof skill === "symbol") return;
|
|
1932
|
+
await show_result(await run_skills_cli([
|
|
1933
|
+
"add",
|
|
1934
|
+
source,
|
|
1935
|
+
"--agent",
|
|
1936
|
+
agent,
|
|
1937
|
+
"--skill",
|
|
1938
|
+
skill,
|
|
1939
|
+
"--yes"
|
|
1940
|
+
]));
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (action === "update") await show_result(await run_skills_cli(["update", "--yes"]));
|
|
1944
|
+
}
|
|
1945
|
+
async function select_agent() {
|
|
1946
|
+
const agent = await select({
|
|
1947
|
+
message: "Which agent/client?",
|
|
1948
|
+
options: [...SKILL_AGENTS, {
|
|
1949
|
+
value: "*",
|
|
1950
|
+
label: "All supported agents"
|
|
1951
|
+
}],
|
|
1952
|
+
initialValue: "pi"
|
|
1953
|
+
});
|
|
1954
|
+
return typeof agent === "symbol" ? null : agent;
|
|
1955
|
+
}
|
|
1956
|
+
async function prompt_source() {
|
|
1957
|
+
const source = await text({
|
|
1958
|
+
message: "Skills source:",
|
|
1959
|
+
placeholder: "spences10/skills",
|
|
1960
|
+
defaultValue: "spences10/skills"
|
|
1961
|
+
});
|
|
1962
|
+
return typeof source === "symbol" ? null : source;
|
|
1963
|
+
}
|
|
1964
|
+
async function show_result(result) {
|
|
1965
|
+
if (result.success) {
|
|
1966
|
+
if (result.stdout) log.info(result.stdout);
|
|
1967
|
+
note("Done.");
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
log.error(result.stderr || result.error || "skills CLI failed");
|
|
1971
|
+
}
|
|
1972
|
+
//#endregion
|
|
905
1973
|
//#region src/commands/restore.ts
|
|
906
1974
|
async function restore_config() {
|
|
907
1975
|
try {
|
|
@@ -984,6 +2052,68 @@ function format_time_ago(date) {
|
|
|
984
2052
|
else return "just now";
|
|
985
2053
|
}
|
|
986
2054
|
//#endregion
|
|
2055
|
+
//#region src/core/profile.ts
|
|
2056
|
+
async function load_profile(name) {
|
|
2057
|
+
const profile_path = get_profile_path(name);
|
|
2058
|
+
try {
|
|
2059
|
+
await access(profile_path);
|
|
2060
|
+
const content = await readFile(profile_path, "utf-8");
|
|
2061
|
+
const parsed = JSON.parse(content);
|
|
2062
|
+
let config;
|
|
2063
|
+
if (parsed.mcpServers) config = validate_claude_config(parsed);
|
|
2064
|
+
else if (!parsed.enabledPlugins) config = validate_claude_config({ mcpServers: parsed });
|
|
2065
|
+
else config = validate_claude_config({ mcpServers: parsed.mcpServers || {} });
|
|
2066
|
+
return {
|
|
2067
|
+
config,
|
|
2068
|
+
enabledPlugins: parsed.enabledPlugins
|
|
2069
|
+
};
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new Error(`Profile '${name}' not found at ${profile_path}`);
|
|
2072
|
+
throw error;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
async function list_profiles() {
|
|
2076
|
+
const profiles_dir = get_profiles_dir();
|
|
2077
|
+
try {
|
|
2078
|
+
await access(profiles_dir);
|
|
2079
|
+
const json_files = (await readdir(profiles_dir)).filter((f) => f.endsWith(".json"));
|
|
2080
|
+
const profiles = [];
|
|
2081
|
+
for (const file of json_files) try {
|
|
2082
|
+
const path = get_profile_path(file);
|
|
2083
|
+
const content = await readFile(path, "utf-8");
|
|
2084
|
+
const parsed = JSON.parse(content);
|
|
2085
|
+
const servers = parsed.mcpServers || parsed;
|
|
2086
|
+
const plugins = parsed.enabledPlugins || {};
|
|
2087
|
+
profiles.push({
|
|
2088
|
+
name: file.replace(".json", ""),
|
|
2089
|
+
path,
|
|
2090
|
+
serverCount: Object.keys(servers).length,
|
|
2091
|
+
pluginCount: Object.keys(plugins).length
|
|
2092
|
+
});
|
|
2093
|
+
} catch {}
|
|
2094
|
+
return profiles;
|
|
2095
|
+
} catch {
|
|
2096
|
+
return [];
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
async function save_profile(name) {
|
|
2100
|
+
const config = await read_claude_config();
|
|
2101
|
+
const settings = await read_claude_settings();
|
|
2102
|
+
const servers = config.mcpServers || {};
|
|
2103
|
+
const plugins = settings.enabledPlugins || {};
|
|
2104
|
+
const server_count = Object.keys(servers).length;
|
|
2105
|
+
const plugin_count = Object.keys(plugins).length;
|
|
2106
|
+
if (server_count === 0 && plugin_count === 0) throw new Error("No MCP servers or plugins configured to save");
|
|
2107
|
+
await ensure_directory_exists(get_profiles_dir());
|
|
2108
|
+
const profile_data = { mcpServers: servers };
|
|
2109
|
+
if (plugin_count > 0) profile_data.enabledPlugins = plugins;
|
|
2110
|
+
await writeFile(get_profile_path(name), JSON.stringify(profile_data, null, 2), "utf-8");
|
|
2111
|
+
return {
|
|
2112
|
+
serverCount: server_count,
|
|
2113
|
+
pluginCount: plugin_count
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
//#endregion
|
|
987
2117
|
//#region src/index.ts
|
|
988
2118
|
function parse_args() {
|
|
989
2119
|
const args = process.argv.slice(2);
|
|
@@ -1082,6 +2212,65 @@ async function handle_save_profile() {
|
|
|
1082
2212
|
if (counts.pluginCount > 0) parts.push(`${counts.pluginCount} plugins`);
|
|
1083
2213
|
log.success(`Profile '${name}' saved (${parts.join(", ")})`);
|
|
1084
2214
|
}
|
|
2215
|
+
async function handle_client_tools() {
|
|
2216
|
+
const client_id = await select({
|
|
2217
|
+
message: "Which client?",
|
|
2218
|
+
options: client_adapters.map((adapter) => ({
|
|
2219
|
+
value: adapter.id,
|
|
2220
|
+
label: adapter.label
|
|
2221
|
+
})),
|
|
2222
|
+
initialValue: "claude-code"
|
|
2223
|
+
});
|
|
2224
|
+
if (isCancel(client_id)) return;
|
|
2225
|
+
if (client_id !== "claude-code") {
|
|
2226
|
+
note(`${client_adapters.find((adapter) => adapter.id === client_id)?.label ?? client_id} currently has MCP server toggling only.\nUse “Enable / Disable MCP servers” from the main menu.`);
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const action = await select({
|
|
2230
|
+
message: "Claude Code tools",
|
|
2231
|
+
options: [
|
|
2232
|
+
{
|
|
2233
|
+
value: "plugins",
|
|
2234
|
+
label: "Plugins",
|
|
2235
|
+
hint: "Claude Code plugin enable/install/update"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
value: "marketplaces",
|
|
2239
|
+
label: "Marketplaces",
|
|
2240
|
+
hint: "Claude Code plugin marketplaces"
|
|
2241
|
+
},
|
|
2242
|
+
{
|
|
2243
|
+
value: "hooks",
|
|
2244
|
+
label: "Hooks",
|
|
2245
|
+
hint: "Claude Code settings/plugin hooks"
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
value: "cache",
|
|
2249
|
+
label: "Plugin cache",
|
|
2250
|
+
hint: "Claude Code plugin cache maintenance"
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
value: "back",
|
|
2254
|
+
label: "Back"
|
|
2255
|
+
}
|
|
2256
|
+
]
|
|
2257
|
+
});
|
|
2258
|
+
if (isCancel(action) || action === "back") return;
|
|
2259
|
+
switch (action) {
|
|
2260
|
+
case "plugins":
|
|
2261
|
+
await edit_plugins();
|
|
2262
|
+
break;
|
|
2263
|
+
case "marketplaces":
|
|
2264
|
+
await manage_marketplace();
|
|
2265
|
+
break;
|
|
2266
|
+
case "hooks":
|
|
2267
|
+
await manage_hooks();
|
|
2268
|
+
break;
|
|
2269
|
+
case "cache":
|
|
2270
|
+
await manage_cache();
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
1085
2274
|
async function main() {
|
|
1086
2275
|
const args = parse_args();
|
|
1087
2276
|
if (args.listProfiles) {
|
|
@@ -1096,8 +2285,8 @@ async function main() {
|
|
|
1096
2285
|
await apply_profile(args.profile);
|
|
1097
2286
|
return;
|
|
1098
2287
|
}
|
|
1099
|
-
intro("MCPick -
|
|
1100
|
-
log.info("
|
|
2288
|
+
intro("MCPick - MCP Configuration Manager");
|
|
2289
|
+
log.info("Primary flow: choose a client, then toggle its MCP servers. Use CLI commands for adding/editing server definitions.");
|
|
1101
2290
|
while (true) try {
|
|
1102
2291
|
const action = await select({
|
|
1103
2292
|
message: "What would you like to do?",
|
|
@@ -1105,53 +2294,38 @@ async function main() {
|
|
|
1105
2294
|
{
|
|
1106
2295
|
value: "edit-config",
|
|
1107
2296
|
label: "Enable / Disable MCP servers",
|
|
1108
|
-
hint: "
|
|
2297
|
+
hint: "Choose client, then toggle servers"
|
|
1109
2298
|
},
|
|
1110
2299
|
{
|
|
1111
|
-
value: "
|
|
1112
|
-
label: "
|
|
1113
|
-
hint: "
|
|
2300
|
+
value: "skills",
|
|
2301
|
+
label: "Skills",
|
|
2302
|
+
hint: "Install/list portable SKILL.md packs via skills CLI"
|
|
1114
2303
|
},
|
|
1115
2304
|
{
|
|
1116
|
-
value: "
|
|
1117
|
-
label: "
|
|
1118
|
-
hint: "
|
|
2305
|
+
value: "client-tools",
|
|
2306
|
+
label: "Client-specific tools",
|
|
2307
|
+
hint: "Plugins, hooks, marketplaces, cache where supported"
|
|
1119
2308
|
},
|
|
1120
2309
|
{
|
|
1121
|
-
value: "
|
|
1122
|
-
label: "
|
|
1123
|
-
hint: "
|
|
2310
|
+
value: "load-profile",
|
|
2311
|
+
label: "Load profile",
|
|
2312
|
+
hint: "Apply a saved profile"
|
|
1124
2313
|
},
|
|
1125
2314
|
{
|
|
1126
|
-
value: "
|
|
1127
|
-
label: "
|
|
1128
|
-
hint: "
|
|
2315
|
+
value: "save-profile",
|
|
2316
|
+
label: "Save profile",
|
|
2317
|
+
hint: "Save current config as profile"
|
|
1129
2318
|
},
|
|
1130
2319
|
{
|
|
1131
2320
|
value: "backup",
|
|
1132
2321
|
label: "Backup config",
|
|
1133
2322
|
hint: "Create a timestamped backup"
|
|
1134
2323
|
},
|
|
1135
|
-
{
|
|
1136
|
-
value: "add-server",
|
|
1137
|
-
label: "Add MCP server",
|
|
1138
|
-
hint: "Register a new MCP server"
|
|
1139
|
-
},
|
|
1140
2324
|
{
|
|
1141
2325
|
value: "restore",
|
|
1142
2326
|
label: "Restore from backup",
|
|
1143
2327
|
hint: "Restore from a previous backup"
|
|
1144
2328
|
},
|
|
1145
|
-
{
|
|
1146
|
-
value: "load-profile",
|
|
1147
|
-
label: "Load profile",
|
|
1148
|
-
hint: "Apply a saved profile"
|
|
1149
|
-
},
|
|
1150
|
-
{
|
|
1151
|
-
value: "save-profile",
|
|
1152
|
-
label: "Save profile",
|
|
1153
|
-
hint: "Save current config as profile"
|
|
1154
|
-
},
|
|
1155
2329
|
{
|
|
1156
2330
|
value: "exit",
|
|
1157
2331
|
label: "Exit",
|
|
@@ -1167,24 +2341,15 @@ async function main() {
|
|
|
1167
2341
|
case "edit-config":
|
|
1168
2342
|
await edit_config();
|
|
1169
2343
|
break;
|
|
1170
|
-
case "
|
|
1171
|
-
await
|
|
2344
|
+
case "skills":
|
|
2345
|
+
await manage_skills();
|
|
1172
2346
|
break;
|
|
1173
|
-
case "
|
|
1174
|
-
await
|
|
1175
|
-
break;
|
|
1176
|
-
case "manage-hooks":
|
|
1177
|
-
await manage_hooks();
|
|
1178
|
-
break;
|
|
1179
|
-
case "manage-cache":
|
|
1180
|
-
await manage_cache();
|
|
2347
|
+
case "client-tools":
|
|
2348
|
+
await handle_client_tools();
|
|
1181
2349
|
break;
|
|
1182
2350
|
case "backup":
|
|
1183
2351
|
await backup_config();
|
|
1184
2352
|
break;
|
|
1185
|
-
case "add-server":
|
|
1186
|
-
await add_server();
|
|
1187
|
-
break;
|
|
1188
2353
|
case "restore":
|
|
1189
2354
|
await restore_config();
|
|
1190
2355
|
break;
|
|
@@ -1218,6 +2383,7 @@ async function main() {
|
|
|
1218
2383
|
}
|
|
1219
2384
|
}
|
|
1220
2385
|
const SUBCOMMANDS = new Set([
|
|
2386
|
+
"clients",
|
|
1221
2387
|
"list",
|
|
1222
2388
|
"enable",
|
|
1223
2389
|
"disable",
|
|
@@ -1231,6 +2397,7 @@ const SUBCOMMANDS = new Set([
|
|
|
1231
2397
|
"backup",
|
|
1232
2398
|
"restore",
|
|
1233
2399
|
"profile",
|
|
2400
|
+
"skills",
|
|
1234
2401
|
"plugins",
|
|
1235
2402
|
"cache",
|
|
1236
2403
|
"dev",
|
|
@@ -1240,12 +2407,12 @@ const SUBCOMMANDS = new Set([
|
|
|
1240
2407
|
const arg = process.argv[2];
|
|
1241
2408
|
if (arg && SUBCOMMANDS.has(arg) || arg === "--help" || arg === "-h" || !process.stdout.isTTY) {
|
|
1242
2409
|
if (!arg && !process.stdout.isTTY) process.argv.push("--help");
|
|
1243
|
-
import("./cli-
|
|
2410
|
+
import("./cli-avr5R1LO.js").then((m) => m.run());
|
|
1244
2411
|
} else main().catch((error) => {
|
|
1245
2412
|
console.error("Fatal error:", error);
|
|
1246
2413
|
process.exit(1);
|
|
1247
2414
|
});
|
|
1248
2415
|
//#endregion
|
|
1249
|
-
export {};
|
|
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 };
|
|
1250
2417
|
|
|
1251
2418
|
//# sourceMappingURL=index.js.map
|