mcpick 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -5
- package/README.md +150 -127
- package/dist/add-LJQa2my2.js +164 -0
- package/dist/add-json-TEdYweZ5.js +95 -0
- package/dist/{backup-DSDhHI5f.js → backup-kyS5IVIr.js} +4 -4
- package/dist/{cache-D6kd7qE8.js → cache-DTfzTsEE.js} +3 -3
- package/dist/cli-By-0nYNQ.js +112 -0
- package/dist/clients-qMozizys.js +30 -0
- package/dist/{clone-DYKPEsar.js → clone-BVhYjRGO.js} +5 -6
- package/dist/{config-DijVdEFn.js → config-DzMmTJYL.js} +2 -2
- package/dist/{dev-DRJRNp7y.js → dev-Cst8WkQ-.js} +5 -5
- package/dist/disable-BaOs9lrm.js +83 -0
- package/dist/enable--3mjSmTq.js +84 -0
- package/dist/{get-Bb1eOOIZ.js → get-CjhNWyRj.js} +3 -3
- package/dist/{hooks-Bmn7pUZa.js → hooks-DFmxgD0t.js} +3 -4
- package/dist/index.js +1929 -297
- package/dist/list-D5CkCXpP.js +100 -0
- package/dist/{marketplace-DcKk5dc1.js → marketplace-C3EGyIG0.js} +4 -5
- package/dist/output-HtT5HCof.js +17 -0
- package/dist/{plugin-cache-Bby9Dxm9.js → plugin-cache-BSgB42wa.js} +34 -15
- package/dist/{plugins-Dc7DN6R_.js → plugins-Dn2mPFKm.js} +4 -5
- package/dist/{profile-CX97sMGp.js → profile-Dq3ORPil.js} +4 -5
- package/dist/redact-wBMtzbno.js +88 -0
- package/dist/{reload-CYDhkCVZ.js → reload-257iU7Z7.js} +2 -2
- package/dist/remove-26XFzkPd.js +87 -0
- package/dist/{reset-project-choices-BfRSNN3m.js → reset-project-choices-D2F04LfC.js} +3 -3
- package/dist/{restore-DdMfUljI.js → restore-BYYsoNqF.js} +4 -5
- package/dist/rollback-CPdaME91.js +55 -0
- package/dist/skills-DfWk9mpk.js +216 -0
- package/package.json +22 -8
- package/.github/copilot-instructions.md +0 -32
- package/.github/workflows/ci.yml +0 -26
- package/.vscode/settings.json +0 -5
- package/dist/add-BDyaBew0.js +0 -113
- package/dist/add-json-BjgzdeG-.js +0 -58
- package/dist/atomic-write-BqEykHp9.js +0 -26
- package/dist/claude-cli-DnmBJrjg.js +0 -445
- package/dist/cli-CsFfnWBo.js +0 -84
- package/dist/disable-xJXZfUR_.js +0 -39
- package/dist/enable-RrpcN6la.js +0 -40
- 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/remove-D1owHLhG.js +0 -31
- package/dist/settings-DEcWtzLE.js +0 -201
package/dist/index.js
CHANGED
|
@@ -1,216 +1,309 @@
|
|
|
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-BPISiJi4.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
7
|
-
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-DEcWtzLE.js";
|
|
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, a as get_claude_settings_path, c as get_disabled_hooks_path, f as get_marketplaces_dir, g as get_profile_path, i as get_claude_config_path, m as get_plugin_backup_filename, n as get_backup_filename, p as get_mcpick_dir, r as get_backups_dir, t as ensure_directory_exists, y as get_server_registry_path } from "./paths-BPISiJi4.js";
|
|
3
|
+
import { r as validate_server_registry, t as validate_claude_config } from "./validation-xMlbgGCF.js";
|
|
4
|
+
import { a as get_enabled_servers, c as write_claude_config, n as create_config_from_servers, o as get_enabled_servers_for_scope, s as read_claude_config } from "./config-DzMmTJYL.js";
|
|
5
|
+
import { a as redact_url, i as redact_text } from "./redact-wBMtzbno.js";
|
|
6
|
+
import { d as refresh_all_marketplaces, l as read_known_marketplaces, n as clear_plugin_caches, r as get_cached_plugins_info, t as clean_orphaned_versions, u as read_marketplace_manifest } from "./plugin-cache-BSgB42wa.js";
|
|
11
7
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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() {
|
|
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
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
//#region src/utils/safe-apply.ts
|
|
15
|
+
async function file_exists$1(path) {
|
|
26
16
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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() }
|
|
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 } : {}
|
|
88
57
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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();
|
|
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
|
|
136
81
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
143
205
|
});
|
|
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
206
|
}
|
|
154
207
|
}
|
|
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
208
|
}
|
|
209
|
+
const plugin_hooks = await get_all_plugin_hooks();
|
|
210
|
+
entries.push(...plugin_hooks);
|
|
211
|
+
return entries;
|
|
170
212
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Scan all installed plugins for hooks.json and return flattened hook entries.
|
|
215
|
+
* Checks both cache and marketplace source paths since Claude Code reads from both.
|
|
216
|
+
*/
|
|
217
|
+
async function get_all_plugin_hooks() {
|
|
218
|
+
const { read_installed_plugins } = await import("./plugin-cache-BSgB42wa.js").then((n) => n.s);
|
|
219
|
+
const { get_marketplaces_dir } = await import("./paths-BPISiJi4.js").then((n) => n.b);
|
|
220
|
+
const installed = await read_installed_plugins();
|
|
221
|
+
const entries = [];
|
|
222
|
+
const seen_hooks = /* @__PURE__ */ new Set();
|
|
223
|
+
for (const [plugin_key, installs] of Object.entries(installed.plugins)) {
|
|
224
|
+
if (!installs?.length) continue;
|
|
225
|
+
const install = installs[0];
|
|
226
|
+
const at_index = plugin_key.lastIndexOf("@");
|
|
227
|
+
const plugin_name = at_index > 0 ? plugin_key.substring(0, at_index) : plugin_key;
|
|
228
|
+
const marketplace_name = at_index > 0 ? plugin_key.substring(at_index + 1) : "";
|
|
229
|
+
const hooks_paths = [join(install.installPath, "hooks", "hooks.json")];
|
|
230
|
+
if (marketplace_name) hooks_paths.push(join(get_marketplaces_dir(), marketplace_name, "plugins", plugin_name, "hooks", "hooks.json"));
|
|
231
|
+
for (const hooks_path of hooks_paths) {
|
|
232
|
+
let hooks_data;
|
|
179
233
|
try {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
if (!parsed.command) return "Server configuration must include a \"command\" field";
|
|
234
|
+
const content = await readFile(hooks_path, "utf-8");
|
|
235
|
+
hooks_data = JSON.parse(content);
|
|
183
236
|
} catch {
|
|
184
|
-
|
|
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
|
+
}
|
|
185
263
|
}
|
|
186
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;
|
|
187
285
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
197
304
|
}
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
}
|
|
305
|
+
return existing;
|
|
306
|
+
});
|
|
214
307
|
}
|
|
215
308
|
//#endregion
|
|
216
309
|
//#region src/commands/backup.ts
|
|
@@ -251,79 +344,1193 @@ async function cleanup_old_backups(prefix) {
|
|
|
251
344
|
} catch {}
|
|
252
345
|
}
|
|
253
346
|
//#endregion
|
|
347
|
+
//#region src/core/client-config.ts
|
|
348
|
+
const client_options_to_skip = new Set([
|
|
349
|
+
"name",
|
|
350
|
+
"type",
|
|
351
|
+
"command",
|
|
352
|
+
"args",
|
|
353
|
+
"url",
|
|
354
|
+
"httpUrl",
|
|
355
|
+
"serverUrl",
|
|
356
|
+
"env",
|
|
357
|
+
"headers",
|
|
358
|
+
"description",
|
|
359
|
+
"disabled",
|
|
360
|
+
"enabled",
|
|
361
|
+
"environment"
|
|
362
|
+
]);
|
|
363
|
+
async function file_exists(path) {
|
|
364
|
+
try {
|
|
365
|
+
await access(path);
|
|
366
|
+
return true;
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function read_json_file(path) {
|
|
372
|
+
try {
|
|
373
|
+
return parse_json_or_jsonc(await readFile(path, "utf-8"));
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async function write_json_file(path, data) {
|
|
380
|
+
await safe_json_write(path, data, 2);
|
|
381
|
+
}
|
|
382
|
+
function parse_json_or_jsonc(content) {
|
|
383
|
+
try {
|
|
384
|
+
return JSON.parse(content);
|
|
385
|
+
} catch {
|
|
386
|
+
return JSON.parse(remove_jsonc_syntax(content));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function remove_jsonc_syntax(content) {
|
|
390
|
+
let result = "";
|
|
391
|
+
let in_string = false;
|
|
392
|
+
let quote = "";
|
|
393
|
+
let escaped = false;
|
|
394
|
+
let in_line_comment = false;
|
|
395
|
+
let in_block_comment = false;
|
|
396
|
+
for (let index = 0; index < content.length; index++) {
|
|
397
|
+
const char = content[index];
|
|
398
|
+
const next = content[index + 1];
|
|
399
|
+
if (in_line_comment) {
|
|
400
|
+
if (char === "\n" || char === "\r") {
|
|
401
|
+
in_line_comment = false;
|
|
402
|
+
result += char;
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (in_block_comment) {
|
|
407
|
+
if (char === "*" && next === "/") {
|
|
408
|
+
in_block_comment = false;
|
|
409
|
+
index++;
|
|
410
|
+
}
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (in_string) {
|
|
414
|
+
result += char;
|
|
415
|
+
if (escaped) escaped = false;
|
|
416
|
+
else if (char === "\\") escaped = true;
|
|
417
|
+
else if (char === quote) in_string = false;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (char === "\"" || char === "'") {
|
|
421
|
+
in_string = true;
|
|
422
|
+
quote = char;
|
|
423
|
+
result += char;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (char === "/" && next === "/") {
|
|
427
|
+
in_line_comment = true;
|
|
428
|
+
index++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (char === "/" && next === "*") {
|
|
432
|
+
in_block_comment = true;
|
|
433
|
+
index++;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
result += char;
|
|
437
|
+
}
|
|
438
|
+
return remove_trailing_commas(result);
|
|
439
|
+
}
|
|
440
|
+
function remove_trailing_commas(content) {
|
|
441
|
+
let result = "";
|
|
442
|
+
let in_string = false;
|
|
443
|
+
let quote = "";
|
|
444
|
+
let escaped = false;
|
|
445
|
+
for (let index = 0; index < content.length; index++) {
|
|
446
|
+
const char = content[index];
|
|
447
|
+
if (in_string) {
|
|
448
|
+
result += char;
|
|
449
|
+
if (escaped) escaped = false;
|
|
450
|
+
else if (char === "\\") escaped = true;
|
|
451
|
+
else if (char === quote) in_string = false;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (char === "\"") {
|
|
455
|
+
in_string = true;
|
|
456
|
+
quote = char;
|
|
457
|
+
result += char;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (char === ",") {
|
|
461
|
+
let cursor = index + 1;
|
|
462
|
+
while (/\s/.test(content[cursor] ?? "")) cursor++;
|
|
463
|
+
if (content[cursor] === "}" || content[cursor] === "]") continue;
|
|
464
|
+
}
|
|
465
|
+
result += char;
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
function string_record(value) {
|
|
470
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
471
|
+
const result = {};
|
|
472
|
+
for (const [key, item] of Object.entries(value)) if (typeof item === "string") result[key] = item;
|
|
473
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
474
|
+
}
|
|
475
|
+
function string_array(value) {
|
|
476
|
+
if (!Array.isArray(value)) return void 0;
|
|
477
|
+
const values = value.filter((item) => typeof item === "string");
|
|
478
|
+
return values.length > 0 ? values : void 0;
|
|
479
|
+
}
|
|
480
|
+
function infer_transport(config) {
|
|
481
|
+
if (config.type === "http" || config.type === "remote" || config.httpUrl || config.serverUrl) return "http";
|
|
482
|
+
if (config.type === "sse") return "sse";
|
|
483
|
+
if (config.url && !config.command) return "http";
|
|
484
|
+
return "stdio";
|
|
485
|
+
}
|
|
486
|
+
function normalize_mcp_server(name, config) {
|
|
487
|
+
const transport = infer_transport(config);
|
|
488
|
+
const url = typeof config.httpUrl === "string" ? config.httpUrl : typeof config.serverUrl === "string" ? config.serverUrl : typeof config.url === "string" ? config.url : void 0;
|
|
489
|
+
const client_options = {};
|
|
490
|
+
for (const [key, value] of Object.entries(config)) if (!client_options_to_skip.has(key)) client_options[key] = value;
|
|
491
|
+
const command_array = string_array(config.command);
|
|
492
|
+
const command = typeof config.command === "string" ? config.command : command_array?.[0];
|
|
493
|
+
const args = string_array(config.args) ?? command_array?.slice(1);
|
|
494
|
+
const env = string_record(config.env) ?? string_record(config.environment);
|
|
495
|
+
const disabled = typeof config.disabled === "boolean" ? config.disabled : typeof config.enabled === "boolean" ? !config.enabled : void 0;
|
|
496
|
+
return {
|
|
497
|
+
name,
|
|
498
|
+
transport,
|
|
499
|
+
...command ? { command } : {},
|
|
500
|
+
...args && args.length > 0 ? { args } : {},
|
|
501
|
+
...url ? { url } : {},
|
|
502
|
+
...env ? { env } : {},
|
|
503
|
+
...string_record(config.headers) ? { headers: string_record(config.headers) } : {},
|
|
504
|
+
...typeof config.description === "string" ? { description: config.description } : {},
|
|
505
|
+
...typeof disabled === "boolean" ? { disabled } : {},
|
|
506
|
+
...Object.keys(client_options).length > 0 ? { client_options } : {}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function get_server_record(data, key) {
|
|
510
|
+
const servers = data?.[key];
|
|
511
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return {};
|
|
512
|
+
return Object.fromEntries(Object.entries(servers).filter((entry) => {
|
|
513
|
+
const [, value] = entry;
|
|
514
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
function read_server_map(data, key) {
|
|
518
|
+
return Object.entries(get_server_record(data, key)).map(([name, config]) => normalize_mcp_server(name, config));
|
|
519
|
+
}
|
|
520
|
+
function set_server_enabled(config, enabled, mode) {
|
|
521
|
+
if (mode === "enabled" || "enabled" in config) {
|
|
522
|
+
config.enabled = enabled;
|
|
523
|
+
delete config.disabled;
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
config.disabled = !enabled;
|
|
527
|
+
}
|
|
528
|
+
function portable_to_json(server, mode) {
|
|
529
|
+
const result = { ...server.client_options };
|
|
530
|
+
if (server.transport !== "stdio") result.type = server.transport;
|
|
531
|
+
if (server.command) result.command = server.command;
|
|
532
|
+
if (server.args && server.args.length > 0) result.args = server.args;
|
|
533
|
+
if (server.url) result.url = server.url;
|
|
534
|
+
if (server.env) result.env = server.env;
|
|
535
|
+
if (server.headers) result.headers = server.headers;
|
|
536
|
+
if (server.description) result.description = server.description;
|
|
537
|
+
if (typeof server.disabled === "boolean") set_server_enabled(result, !server.disabled, mode);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
function create_json_adapter(options) {
|
|
541
|
+
return {
|
|
542
|
+
id: options.id,
|
|
543
|
+
label: options.label,
|
|
544
|
+
locations: options.locations,
|
|
545
|
+
async read(scope) {
|
|
546
|
+
const locations = scope ? options.locations().filter((location) => location.scope === scope) : options.locations();
|
|
547
|
+
const result = [];
|
|
548
|
+
for (const location of locations) for (const server of await this.readLocation(location)) result.push(server);
|
|
549
|
+
return result;
|
|
550
|
+
},
|
|
551
|
+
async readLocation(location) {
|
|
552
|
+
return read_server_map(await read_json_file(location.path), options.serverKey);
|
|
553
|
+
},
|
|
554
|
+
async writeEnabled(location, enabled_names) {
|
|
555
|
+
const data = await read_json_file(location.path) ?? {};
|
|
556
|
+
const servers = get_server_record(data, options.serverKey);
|
|
557
|
+
const enabled = new Set(enabled_names);
|
|
558
|
+
for (const [name, config] of Object.entries(servers)) set_server_enabled(config, enabled.has(name), options.disabledMode ?? "disabled");
|
|
559
|
+
data[options.serverKey] = servers;
|
|
560
|
+
await write_json_file(location.path, data);
|
|
561
|
+
},
|
|
562
|
+
async write_server(location, server) {
|
|
563
|
+
const data = await read_json_file(location.path) ?? {};
|
|
564
|
+
const servers = get_server_record(data, options.serverKey);
|
|
565
|
+
servers[server.name] = portable_to_json(server, options.disabledMode ?? "disabled");
|
|
566
|
+
data[options.serverKey] = servers;
|
|
567
|
+
await write_json_file(location.path, data);
|
|
568
|
+
},
|
|
569
|
+
async write_server_config(location, name, config) {
|
|
570
|
+
const data = await read_json_file(location.path) ?? {};
|
|
571
|
+
const servers = get_server_record(data, options.serverKey);
|
|
572
|
+
servers[name] = config;
|
|
573
|
+
data[options.serverKey] = servers;
|
|
574
|
+
await write_json_file(location.path, data);
|
|
575
|
+
},
|
|
576
|
+
async remove_server(location, name) {
|
|
577
|
+
const data = await read_json_file(location.path) ?? {};
|
|
578
|
+
const servers = get_server_record(data, options.serverKey);
|
|
579
|
+
delete servers[name];
|
|
580
|
+
data[options.serverKey] = servers;
|
|
581
|
+
await write_json_file(location.path, data);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function project_path(path) {
|
|
586
|
+
return join(process.cwd(), path);
|
|
587
|
+
}
|
|
588
|
+
const client_adapters = [
|
|
589
|
+
{
|
|
590
|
+
id: "claude-code",
|
|
591
|
+
label: "Claude Code",
|
|
592
|
+
locations: () => [
|
|
593
|
+
{
|
|
594
|
+
scope: "local",
|
|
595
|
+
path: get_claude_config_path(),
|
|
596
|
+
description: "~/.claude.json projects[cwd].mcpServers"
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
scope: "project",
|
|
600
|
+
path: project_path(".mcp.json"),
|
|
601
|
+
description: ".mcp.json mcpServers"
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
scope: "user",
|
|
605
|
+
path: get_claude_config_path(),
|
|
606
|
+
description: "~/.claude.json mcpServers"
|
|
607
|
+
}
|
|
608
|
+
],
|
|
609
|
+
async read(scope) {
|
|
610
|
+
const locations = scope ? this.locations().filter((location) => location.scope === scope) : this.locations();
|
|
611
|
+
const result = [];
|
|
612
|
+
for (const location of locations) result.push(...await this.readLocation(location));
|
|
613
|
+
return result;
|
|
614
|
+
},
|
|
615
|
+
async readLocation(location) {
|
|
616
|
+
if (location.scope === "project") return read_server_map(await read_json_file(location.path), "mcpServers");
|
|
617
|
+
const data = await read_json_file(get_claude_config_path());
|
|
618
|
+
if (location.scope === "user") return read_server_map(data, "mcpServers");
|
|
619
|
+
const projects = data?.projects;
|
|
620
|
+
if (!projects || typeof projects !== "object" || Array.isArray(projects)) return [];
|
|
621
|
+
const project_config = projects[process.cwd()];
|
|
622
|
+
if (!project_config || typeof project_config !== "object" || Array.isArray(project_config)) return [];
|
|
623
|
+
return read_server_map(project_config, "mcpServers");
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
create_json_adapter({
|
|
627
|
+
id: "gemini-cli",
|
|
628
|
+
label: "Gemini CLI",
|
|
629
|
+
serverKey: "mcpServers",
|
|
630
|
+
locations: () => [{
|
|
631
|
+
scope: "project",
|
|
632
|
+
path: project_path(".gemini/settings.json"),
|
|
633
|
+
description: ".gemini/settings.json mcpServers"
|
|
634
|
+
}, {
|
|
635
|
+
scope: "user",
|
|
636
|
+
path: join(homedir(), ".gemini/settings.json"),
|
|
637
|
+
description: "~/.gemini/settings.json mcpServers"
|
|
638
|
+
}]
|
|
639
|
+
}),
|
|
640
|
+
create_json_adapter({
|
|
641
|
+
id: "vscode",
|
|
642
|
+
label: "VS Code / GitHub Copilot",
|
|
643
|
+
serverKey: "servers",
|
|
644
|
+
locations: () => [{
|
|
645
|
+
scope: "project",
|
|
646
|
+
path: project_path(".vscode/mcp.json"),
|
|
647
|
+
description: ".vscode/mcp.json servers"
|
|
648
|
+
}]
|
|
649
|
+
}),
|
|
650
|
+
create_json_adapter({
|
|
651
|
+
id: "cursor",
|
|
652
|
+
label: "Cursor",
|
|
653
|
+
serverKey: "mcpServers",
|
|
654
|
+
locations: () => [{
|
|
655
|
+
scope: "project",
|
|
656
|
+
path: project_path(".cursor/mcp.json"),
|
|
657
|
+
description: ".cursor/mcp.json mcpServers"
|
|
658
|
+
}, {
|
|
659
|
+
scope: "user",
|
|
660
|
+
path: join(homedir(), ".cursor/mcp.json"),
|
|
661
|
+
description: "~/.cursor/mcp.json mcpServers"
|
|
662
|
+
}]
|
|
663
|
+
}),
|
|
664
|
+
create_json_adapter({
|
|
665
|
+
id: "windsurf",
|
|
666
|
+
label: "Windsurf",
|
|
667
|
+
serverKey: "mcpServers",
|
|
668
|
+
locations: () => [{
|
|
669
|
+
scope: "user",
|
|
670
|
+
path: join(homedir(), ".codeium/windsurf/mcp_config.json"),
|
|
671
|
+
description: "~/.codeium/windsurf/mcp_config.json mcpServers"
|
|
672
|
+
}]
|
|
673
|
+
}),
|
|
674
|
+
create_json_adapter({
|
|
675
|
+
id: "opencode",
|
|
676
|
+
label: "OpenCode",
|
|
677
|
+
serverKey: "mcp",
|
|
678
|
+
disabledMode: "enabled",
|
|
679
|
+
locations: () => [{
|
|
680
|
+
scope: "project",
|
|
681
|
+
path: project_path("opencode.json"),
|
|
682
|
+
description: "opencode.json mcp"
|
|
683
|
+
}, {
|
|
684
|
+
scope: "user",
|
|
685
|
+
path: join(homedir(), ".config/opencode/opencode.json"),
|
|
686
|
+
description: "~/.config/opencode/opencode.json mcp"
|
|
687
|
+
}]
|
|
688
|
+
}),
|
|
689
|
+
create_json_adapter({
|
|
690
|
+
id: "pi",
|
|
691
|
+
label: "Pi MCP Adapter",
|
|
692
|
+
serverKey: "mcpServers",
|
|
693
|
+
locations: () => [
|
|
694
|
+
{
|
|
695
|
+
scope: "user",
|
|
696
|
+
path: join(homedir(), ".config/mcp/mcp.json"),
|
|
697
|
+
description: "~/.config/mcp/mcp.json shared MCP config"
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
scope: "user",
|
|
701
|
+
path: join(homedir(), ".pi/agent/mcp.json"),
|
|
702
|
+
description: "~/.pi/agent/mcp.json Pi global override"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
scope: "project",
|
|
706
|
+
path: project_path(".mcp.json"),
|
|
707
|
+
description: ".mcp.json shared project MCP config"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
scope: "project",
|
|
711
|
+
path: project_path(".pi/mcp.json"),
|
|
712
|
+
description: ".pi/mcp.json Pi project override"
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
})
|
|
716
|
+
];
|
|
717
|
+
function get_client_adapter(id) {
|
|
718
|
+
return client_adapters.find((adapter) => adapter.id === id) ?? null;
|
|
719
|
+
}
|
|
720
|
+
function resolve_client_location(adapter, scope, path) {
|
|
721
|
+
let locations = adapter.locations();
|
|
722
|
+
if (path) locations = locations.filter((location) => location.path === path);
|
|
723
|
+
else if (scope) locations = locations.filter((location) => location.scope === scope);
|
|
724
|
+
if (locations.length === 1) return locations[0];
|
|
725
|
+
if (locations.length === 0) throw new Error(`No ${adapter.label} config location matches${scope ? ` scope '${scope}'` : ""}${path ? ` path '${path}'` : ""}.`);
|
|
726
|
+
throw new Error(`${adapter.label} has multiple matching config locations. Pass --location with one of: ${locations.map((location) => location.path).join(", ")}`);
|
|
727
|
+
}
|
|
728
|
+
async function add_client_server(adapter, location, server) {
|
|
729
|
+
if (!adapter.write_server) throw new Error(`${adapter.label} support cannot add servers yet.`);
|
|
730
|
+
await adapter.write_server(location, server);
|
|
731
|
+
}
|
|
732
|
+
async function add_client_server_config(adapter, location, name, config) {
|
|
733
|
+
if (!adapter.write_server_config) throw new Error(`${adapter.label} support cannot add servers yet.`);
|
|
734
|
+
await adapter.write_server_config(location, name, config);
|
|
735
|
+
}
|
|
736
|
+
async function remove_client_server(adapter, location, server_name) {
|
|
737
|
+
if (!adapter.remove_server) throw new Error(`${adapter.label} support cannot remove servers yet.`);
|
|
738
|
+
await adapter.remove_server(location, server_name);
|
|
739
|
+
}
|
|
740
|
+
async function set_client_server_enabled(adapter, location, server_name, enabled) {
|
|
741
|
+
if (!adapter.writeEnabled) throw new Error(`${adapter.label} support is read-only.`);
|
|
742
|
+
const servers = await adapter.readLocation(location);
|
|
743
|
+
const server = servers.find((candidate) => candidate.name === server_name);
|
|
744
|
+
if (!server) throw new Error(`Server '${server_name}' not found at ${location.path}.`);
|
|
745
|
+
const enabled_names = new Set(servers.filter((candidate) => candidate.disabled !== true).map((candidate) => candidate.name));
|
|
746
|
+
if (enabled) enabled_names.add(server.name);
|
|
747
|
+
else enabled_names.delete(server.name);
|
|
748
|
+
await adapter.writeEnabled(location, [...enabled_names]);
|
|
749
|
+
return enabled_names.size;
|
|
750
|
+
}
|
|
751
|
+
async function list_client_locations() {
|
|
752
|
+
return await Promise.all(client_adapters.flatMap((adapter) => adapter.locations().map(async (location) => ({
|
|
753
|
+
client: adapter.id,
|
|
754
|
+
label: adapter.label,
|
|
755
|
+
...location,
|
|
756
|
+
exists: await file_exists(location.path)
|
|
757
|
+
}))));
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/core/registry.ts
|
|
761
|
+
async function read_server_registry() {
|
|
762
|
+
const registry_path = get_server_registry_path();
|
|
763
|
+
try {
|
|
764
|
+
await access(registry_path);
|
|
765
|
+
const registry_content = await readFile(registry_path, "utf-8");
|
|
766
|
+
return validate_server_registry(JSON.parse(registry_content));
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
769
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
770
|
+
const default_registry = { servers: [] };
|
|
771
|
+
await write_server_registry(default_registry);
|
|
772
|
+
return default_registry;
|
|
773
|
+
}
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function write_server_registry(registry) {
|
|
778
|
+
const registry_path = get_server_registry_path();
|
|
779
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
780
|
+
await atomic_json_write(registry_path, () => registry);
|
|
781
|
+
}
|
|
782
|
+
async function add_server_to_registry(server) {
|
|
783
|
+
const registry = await read_server_registry();
|
|
784
|
+
const existing_index = registry.servers.findIndex((s) => s.name === server.name);
|
|
785
|
+
if (existing_index >= 0) registry.servers[existing_index] = server;
|
|
786
|
+
else registry.servers.push(server);
|
|
787
|
+
await write_server_registry(registry);
|
|
788
|
+
}
|
|
789
|
+
async function get_all_available_servers() {
|
|
790
|
+
const { get_enabled_servers, read_claude_config } = await import("./config-DzMmTJYL.js").then((n) => n.t);
|
|
791
|
+
const registry = await read_server_registry();
|
|
792
|
+
const config_servers = get_enabled_servers(await read_claude_config());
|
|
793
|
+
const config_by_name = new Map(config_servers.map((s) => [s.name, s]));
|
|
794
|
+
const known_names = /* @__PURE__ */ new Set();
|
|
795
|
+
let registry_updated = false;
|
|
796
|
+
for (let i = 0; i < registry.servers.length; i++) {
|
|
797
|
+
const name = registry.servers[i].name;
|
|
798
|
+
known_names.add(name);
|
|
799
|
+
const config_server = config_by_name.get(name);
|
|
800
|
+
if (config_server) {
|
|
801
|
+
registry.servers[i] = config_server;
|
|
802
|
+
registry_updated = true;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
for (const server of config_servers) if (!known_names.has(server.name)) {
|
|
806
|
+
registry.servers.push(server);
|
|
807
|
+
registry_updated = true;
|
|
808
|
+
}
|
|
809
|
+
if (registry_updated) await write_server_registry(registry);
|
|
810
|
+
return registry.servers;
|
|
811
|
+
}
|
|
812
|
+
async function sync_servers_to_registry(servers) {
|
|
813
|
+
const registry = await read_server_registry();
|
|
814
|
+
servers.forEach((server) => {
|
|
815
|
+
const existing_index = registry.servers.findIndex((s) => s.name === server.name);
|
|
816
|
+
if (existing_index >= 0) registry.servers[existing_index] = server;
|
|
817
|
+
else registry.servers.push(server);
|
|
818
|
+
});
|
|
819
|
+
await write_server_registry(registry);
|
|
820
|
+
}
|
|
821
|
+
function parse_backups(prefix, pattern) {
|
|
822
|
+
return async () => {
|
|
823
|
+
const backups_dir = get_backups_dir();
|
|
824
|
+
try {
|
|
825
|
+
await access(backups_dir);
|
|
826
|
+
return (await readdir(backups_dir)).filter((file) => file.startsWith(prefix) && file.endsWith(".json")).map((file) => {
|
|
827
|
+
const timestamp_match = file.match(pattern);
|
|
828
|
+
if (!timestamp_match) return null;
|
|
829
|
+
const [, year, month, day, hour, minute, second] = timestamp_match;
|
|
830
|
+
return {
|
|
831
|
+
filename: file,
|
|
832
|
+
timestamp: new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second)),
|
|
833
|
+
path: join(backups_dir, file)
|
|
834
|
+
};
|
|
835
|
+
}).filter((backup) => backup !== null).sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const list_backups = parse_backups("mcp-servers-", /mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
843
|
+
const list_plugin_backups = parse_backups("plugins-", /plugins-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
844
|
+
//#endregion
|
|
845
|
+
//#region src/utils/claude-cli.ts
|
|
846
|
+
const exec_file_async$1 = promisify(execFile);
|
|
847
|
+
/**
|
|
848
|
+
* Check if Claude CLI is available
|
|
849
|
+
*/
|
|
850
|
+
async function check_claude_cli() {
|
|
851
|
+
try {
|
|
852
|
+
await exec_file_async$1("claude", ["--version"]);
|
|
853
|
+
return true;
|
|
854
|
+
} catch {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Validate environment variable key.
|
|
860
|
+
* Must start with letter or underscore, contain only alphanumeric and underscores.
|
|
861
|
+
*/
|
|
862
|
+
function is_valid_env_key(key) {
|
|
863
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Build args array for claude mcp add command.
|
|
867
|
+
* Returns raw args — no shell escaping needed since we use execFile.
|
|
868
|
+
*/
|
|
869
|
+
function build_add_args(server, scope) {
|
|
870
|
+
const args = ["mcp", "add"];
|
|
871
|
+
args.push(server.name);
|
|
872
|
+
const transport = server.type || "stdio";
|
|
873
|
+
if (transport !== "stdio") args.push("--transport", transport);
|
|
874
|
+
args.push("--scope", scope);
|
|
875
|
+
if (transport === "stdio") {
|
|
876
|
+
if (server.env) {
|
|
877
|
+
for (const [key, value] of Object.entries(server.env)) if (is_valid_env_key(key)) args.push("-e", `${key}=${value}`);
|
|
878
|
+
}
|
|
879
|
+
if ("command" in server && server.command) {
|
|
880
|
+
args.push("--");
|
|
881
|
+
args.push(server.command);
|
|
882
|
+
if (server.args && server.args.length > 0) args.push(...server.args);
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
if ("url" in server && server.url) args.push(server.url);
|
|
886
|
+
if ("headers" in server && server.headers) for (const [key, value] of Object.entries(server.headers)) args.push("-H", `${key}: ${value}`);
|
|
887
|
+
}
|
|
888
|
+
return args;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Run a claude CLI command using execFile (no shell).
|
|
892
|
+
* This avoids all shell escaping issues on every platform.
|
|
893
|
+
*/
|
|
894
|
+
async function run_claude(args) {
|
|
895
|
+
const result = await exec_file_async$1("claude", args);
|
|
896
|
+
return {
|
|
897
|
+
stdout: redact_text(result.stdout),
|
|
898
|
+
stderr: redact_text(result.stderr)
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function get_redacted_error_message(error) {
|
|
902
|
+
return redact_text(error instanceof Error ? error.message : "Unknown error");
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Add an MCP server using Claude CLI
|
|
906
|
+
*/
|
|
907
|
+
async function add_mcp_via_cli(server, scope) {
|
|
908
|
+
if (!await check_claude_cli()) return {
|
|
909
|
+
success: false,
|
|
910
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
911
|
+
};
|
|
912
|
+
try {
|
|
913
|
+
await run_claude(build_add_args(server, scope));
|
|
914
|
+
return { success: true };
|
|
915
|
+
} catch (error) {
|
|
916
|
+
return {
|
|
917
|
+
success: false,
|
|
918
|
+
error: `Failed to add server via CLI: ${get_redacted_error_message(error)}`
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Remove an MCP server using Claude CLI
|
|
924
|
+
*/
|
|
925
|
+
function build_remove_args(name, scope) {
|
|
926
|
+
return scope ? [
|
|
927
|
+
"mcp",
|
|
928
|
+
"remove",
|
|
929
|
+
name,
|
|
930
|
+
"--scope",
|
|
931
|
+
scope
|
|
932
|
+
] : [
|
|
933
|
+
"mcp",
|
|
934
|
+
"remove",
|
|
935
|
+
name
|
|
936
|
+
];
|
|
937
|
+
}
|
|
938
|
+
async function remove_mcp_via_cli(name, scope) {
|
|
939
|
+
if (!await check_claude_cli()) return {
|
|
940
|
+
success: false,
|
|
941
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
942
|
+
};
|
|
943
|
+
try {
|
|
944
|
+
await run_claude(build_remove_args(name, scope));
|
|
945
|
+
return { success: true };
|
|
946
|
+
} catch (error) {
|
|
947
|
+
return {
|
|
948
|
+
success: false,
|
|
949
|
+
error: `Failed to remove server via CLI: ${get_redacted_error_message(error)}`
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Install a plugin via Claude CLI
|
|
955
|
+
*/
|
|
956
|
+
async function install_plugin_via_cli(key, scope = "user") {
|
|
957
|
+
if (!await check_claude_cli()) return {
|
|
958
|
+
success: false,
|
|
959
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
960
|
+
};
|
|
961
|
+
try {
|
|
962
|
+
await run_claude([
|
|
963
|
+
"plugin",
|
|
964
|
+
"install",
|
|
965
|
+
key,
|
|
966
|
+
"--scope",
|
|
967
|
+
scope
|
|
968
|
+
]);
|
|
969
|
+
return { success: true };
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return {
|
|
972
|
+
success: false,
|
|
973
|
+
error: `Failed to install plugin: ${get_redacted_error_message(error)}`
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Uninstall a plugin via Claude CLI
|
|
979
|
+
*/
|
|
980
|
+
async function uninstall_plugin_via_cli(key, scope = "user") {
|
|
981
|
+
if (!await check_claude_cli()) return {
|
|
982
|
+
success: false,
|
|
983
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
984
|
+
};
|
|
985
|
+
try {
|
|
986
|
+
await run_claude([
|
|
987
|
+
"plugin",
|
|
988
|
+
"uninstall",
|
|
989
|
+
key,
|
|
990
|
+
"--scope",
|
|
991
|
+
scope
|
|
992
|
+
]);
|
|
993
|
+
return { success: true };
|
|
994
|
+
} catch (error) {
|
|
995
|
+
return {
|
|
996
|
+
success: false,
|
|
997
|
+
error: `Failed to uninstall plugin: ${get_redacted_error_message(error)}`
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Update a plugin via Claude CLI
|
|
1003
|
+
*/
|
|
1004
|
+
async function update_plugin_via_cli(key, scope = "user") {
|
|
1005
|
+
if (!await check_claude_cli()) return {
|
|
1006
|
+
success: false,
|
|
1007
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1008
|
+
};
|
|
1009
|
+
try {
|
|
1010
|
+
await run_claude([
|
|
1011
|
+
"plugin",
|
|
1012
|
+
"update",
|
|
1013
|
+
key,
|
|
1014
|
+
"--scope",
|
|
1015
|
+
scope
|
|
1016
|
+
]);
|
|
1017
|
+
return { success: true };
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
return {
|
|
1020
|
+
success: false,
|
|
1021
|
+
error: `Failed to update plugin: ${get_redacted_error_message(error)}`
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const GITHUB_OWNER_PATTERN = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?";
|
|
1026
|
+
const GITHUB_REPO_PATTERN = "[A-Za-z0-9._-]+";
|
|
1027
|
+
/**
|
|
1028
|
+
* Extract GitHub owner/repo from various source formats.
|
|
1029
|
+
* Returns null if not a recognizable GitHub reference.
|
|
1030
|
+
*/
|
|
1031
|
+
function parse_github_repo(source) {
|
|
1032
|
+
const trimmed = source.trim();
|
|
1033
|
+
try {
|
|
1034
|
+
const url = new URL(trimmed);
|
|
1035
|
+
if (url.hostname !== "github.com") return null;
|
|
1036
|
+
const [owner, raw_repo, ...rest] = url.pathname.split("/").filter(Boolean);
|
|
1037
|
+
if (!owner || !raw_repo || rest.length > 0) return null;
|
|
1038
|
+
const repo = raw_repo.replace(/\.git$/, "");
|
|
1039
|
+
if (!is_valid_github_repo(owner, repo)) return null;
|
|
1040
|
+
return {
|
|
1041
|
+
owner,
|
|
1042
|
+
repo,
|
|
1043
|
+
kind: "https"
|
|
1044
|
+
};
|
|
1045
|
+
} catch {}
|
|
1046
|
+
const ssh_match = trimmed.match(new RegExp(`^git@github\\.com:(${GITHUB_OWNER_PATTERN})/(${GITHUB_REPO_PATTERN})(?:\\.git)?$`));
|
|
1047
|
+
if (ssh_match) return {
|
|
1048
|
+
owner: ssh_match[1],
|
|
1049
|
+
repo: ssh_match[2].replace(/\.git$/, ""),
|
|
1050
|
+
kind: "ssh"
|
|
1051
|
+
};
|
|
1052
|
+
const shorthand_match = trimmed.match(new RegExp(`^(${GITHUB_OWNER_PATTERN})/(${GITHUB_REPO_PATTERN})$`));
|
|
1053
|
+
if (shorthand_match) return {
|
|
1054
|
+
owner: shorthand_match[1],
|
|
1055
|
+
repo: shorthand_match[2],
|
|
1056
|
+
kind: "shorthand"
|
|
1057
|
+
};
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
function is_valid_github_repo(owner, repo) {
|
|
1061
|
+
return new RegExp(`^${GITHUB_OWNER_PATTERN}$`).test(owner) && new RegExp(`^${GITHUB_REPO_PATTERN}$`).test(repo);
|
|
1062
|
+
}
|
|
1063
|
+
function build_marketplace_add_args(source) {
|
|
1064
|
+
return [
|
|
1065
|
+
"plugin",
|
|
1066
|
+
"marketplace",
|
|
1067
|
+
"add",
|
|
1068
|
+
source
|
|
1069
|
+
];
|
|
1070
|
+
}
|
|
1071
|
+
function get_github_repo_id(ref) {
|
|
1072
|
+
return `${ref.owner}/${ref.repo}`;
|
|
1073
|
+
}
|
|
1074
|
+
function get_github_clone_urls(ref) {
|
|
1075
|
+
const repo_id = get_github_repo_id(ref);
|
|
1076
|
+
const https_url = `https://github.com/${repo_id}.git`;
|
|
1077
|
+
const ssh_url = `git@github.com:${repo_id}.git`;
|
|
1078
|
+
if (ref.kind === "https") return [https_url];
|
|
1079
|
+
if (ref.kind === "ssh") return [ssh_url];
|
|
1080
|
+
return [https_url, ssh_url];
|
|
1081
|
+
}
|
|
1082
|
+
function get_process_error_output(error) {
|
|
1083
|
+
const process_error = error;
|
|
1084
|
+
return redact_text([
|
|
1085
|
+
process_error.message,
|
|
1086
|
+
process_error.stderr?.toString(),
|
|
1087
|
+
process_error.stdout?.toString()
|
|
1088
|
+
].filter(Boolean).join("\n"));
|
|
1089
|
+
}
|
|
1090
|
+
async function can_access_with_git(url) {
|
|
1091
|
+
try {
|
|
1092
|
+
await exec_file_async$1("git", [
|
|
1093
|
+
"ls-remote",
|
|
1094
|
+
"--exit-code",
|
|
1095
|
+
url,
|
|
1096
|
+
"HEAD"
|
|
1097
|
+
], {
|
|
1098
|
+
env: {
|
|
1099
|
+
...process.env,
|
|
1100
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1101
|
+
GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
|
|
1102
|
+
},
|
|
1103
|
+
timeout: 15e3
|
|
1104
|
+
});
|
|
1105
|
+
return null;
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
return get_process_error_output(error);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function can_view_with_gh(repo_id) {
|
|
1111
|
+
try {
|
|
1112
|
+
await exec_file_async$1("gh", [
|
|
1113
|
+
"repo",
|
|
1114
|
+
"view",
|
|
1115
|
+
repo_id,
|
|
1116
|
+
"--json",
|
|
1117
|
+
"nameWithOwner"
|
|
1118
|
+
], { timeout: 15e3 });
|
|
1119
|
+
return true;
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
const message = get_process_error_output(error).toLowerCase();
|
|
1122
|
+
if (message.includes("could not resolve") || message.includes("not found") || message.includes("repository not found")) return false;
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
async function get_github_api_status(repo_id) {
|
|
1127
|
+
try {
|
|
1128
|
+
return (await fetch(`https://api.github.com/repos/${repo_id}`, {
|
|
1129
|
+
method: "GET",
|
|
1130
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
1131
|
+
})).status;
|
|
1132
|
+
} catch {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Validate that a GitHub repository can be accessed by git without cloning it.
|
|
1138
|
+
* Returns an error message if validation fails, null if OK.
|
|
1139
|
+
*/
|
|
1140
|
+
async function validate_github_repo(ref) {
|
|
1141
|
+
const repo_id = get_github_repo_id(ref);
|
|
1142
|
+
const git_errors = [];
|
|
1143
|
+
for (const url of get_github_clone_urls(ref)) {
|
|
1144
|
+
const git_error = await can_access_with_git(url);
|
|
1145
|
+
if (!git_error) return null;
|
|
1146
|
+
git_errors.push(git_error);
|
|
1147
|
+
}
|
|
1148
|
+
const gh_visible = await can_view_with_gh(repo_id);
|
|
1149
|
+
if (gh_visible === false) return `Repository '${repo_id}' not found or not accessible with your GitHub account.`;
|
|
1150
|
+
const api_status = await get_github_api_status(repo_id);
|
|
1151
|
+
if (api_status === 404 && gh_visible !== true) return `Repository '${repo_id}' not found or private/inaccessible. Check the name, sign in with 'gh auth login', or use an SSH URL with access.`;
|
|
1152
|
+
if (api_status === 403 && gh_visible !== true) return `Unable to validate '${repo_id}' because GitHub API access was denied or rate-limited. Git access also failed; check your credentials.`;
|
|
1153
|
+
return format_git_access_error(ref, git_errors.join("\n"));
|
|
1154
|
+
}
|
|
1155
|
+
function format_git_access_error(ref, message) {
|
|
1156
|
+
const repo_id = get_github_repo_id(ref);
|
|
1157
|
+
const lower = message.toLowerCase();
|
|
1158
|
+
if (ref.kind === "shorthand" && (lower.includes("could not read username") || lower.includes("authentication failed")) && (lower.includes("permission denied (publickey)") || lower.includes("ssh authentication"))) return `Repository '${repo_id}' exists, but Git cannot access it over HTTPS or SSH. Run 'gh auth login' and 'gh auth setup-git', or configure an SSH key with GitHub.`;
|
|
1159
|
+
if (ref.kind === "https" || lower.includes("https authentication failed") || lower.includes("could not read username") || lower.includes("authentication failed")) return `Repository '${repo_id}' exists, but HTTPS Git authentication failed. Run 'gh auth login' and 'gh auth setup-git', configure a Git credential helper, or use git@github.com:${repo_id}.git.`;
|
|
1160
|
+
if (ref.kind === "ssh" || lower.includes("permission denied (publickey)") || lower.includes("ssh authentication")) return `SSH authentication failed for '${repo_id}'. Configure an SSH key with GitHub or use https://github.com/${repo_id}.git with a configured Git credential helper.`;
|
|
1161
|
+
return `Unable to access GitHub repository '${repo_id}' with git. Check that the repository exists and that your HTTPS or SSH credentials can clone it.`;
|
|
1162
|
+
}
|
|
1163
|
+
async function marketplace_add_via_cli(source) {
|
|
1164
|
+
if (!await check_claude_cli()) return {
|
|
1165
|
+
success: false,
|
|
1166
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1167
|
+
};
|
|
1168
|
+
const gh = parse_github_repo(source);
|
|
1169
|
+
if (gh) {
|
|
1170
|
+
const validation_error = await validate_github_repo(gh);
|
|
1171
|
+
if (validation_error) return {
|
|
1172
|
+
success: false,
|
|
1173
|
+
error: validation_error
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
await run_claude(build_marketplace_add_args(source));
|
|
1178
|
+
return { success: true };
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
const message = get_redacted_error_message(error);
|
|
1181
|
+
const lower_message = message.toLowerCase();
|
|
1182
|
+
if (gh) {
|
|
1183
|
+
if (lower_message.includes("https authentication failed") || lower_message.includes("could not read username") || lower_message.includes("authentication failed")) return {
|
|
1184
|
+
success: false,
|
|
1185
|
+
error: format_git_access_error(gh, message)
|
|
1186
|
+
};
|
|
1187
|
+
if (message.includes("SSH") || message.includes("Permission denied (publickey)")) return {
|
|
1188
|
+
success: false,
|
|
1189
|
+
error: format_git_access_error(gh, message)
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
if (lower_message.includes("not found") || lower_message.includes("does not exist")) return {
|
|
1193
|
+
success: false,
|
|
1194
|
+
error: gh ? `Repository '${get_github_repo_id(gh)}' not found or not accessible with your GitHub account.` : `Repository '${source}' not found. Check the name and your access permissions.`
|
|
1195
|
+
};
|
|
1196
|
+
return {
|
|
1197
|
+
success: false,
|
|
1198
|
+
error: `Failed to add marketplace: ${message}`
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Remove a marketplace via Claude CLI
|
|
1204
|
+
*/
|
|
1205
|
+
async function marketplace_remove_via_cli(name) {
|
|
1206
|
+
if (!await check_claude_cli()) return {
|
|
1207
|
+
success: false,
|
|
1208
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1209
|
+
};
|
|
1210
|
+
try {
|
|
1211
|
+
await run_claude([
|
|
1212
|
+
"plugin",
|
|
1213
|
+
"marketplace",
|
|
1214
|
+
"remove",
|
|
1215
|
+
name
|
|
1216
|
+
]);
|
|
1217
|
+
return { success: true };
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
return {
|
|
1220
|
+
success: false,
|
|
1221
|
+
error: `Failed to remove marketplace: ${get_redacted_error_message(error)}`
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Update marketplace(s) via Claude CLI
|
|
1227
|
+
*/
|
|
1228
|
+
async function marketplace_update_via_cli(name) {
|
|
1229
|
+
if (!await check_claude_cli()) return {
|
|
1230
|
+
success: false,
|
|
1231
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1232
|
+
};
|
|
1233
|
+
try {
|
|
1234
|
+
const args = [
|
|
1235
|
+
"plugin",
|
|
1236
|
+
"marketplace",
|
|
1237
|
+
"update"
|
|
1238
|
+
];
|
|
1239
|
+
if (name) args.push(name);
|
|
1240
|
+
await run_claude(args);
|
|
1241
|
+
return { success: true };
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
return {
|
|
1244
|
+
success: false,
|
|
1245
|
+
error: `Failed to update marketplace: ${get_redacted_error_message(error)}`
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* List marketplaces via Claude CLI
|
|
1251
|
+
*/
|
|
1252
|
+
async function marketplace_list_via_cli() {
|
|
1253
|
+
if (!await check_claude_cli()) return {
|
|
1254
|
+
success: false,
|
|
1255
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1256
|
+
};
|
|
1257
|
+
try {
|
|
1258
|
+
const { stdout } = await run_claude([
|
|
1259
|
+
"plugin",
|
|
1260
|
+
"marketplace",
|
|
1261
|
+
"list"
|
|
1262
|
+
]);
|
|
1263
|
+
return {
|
|
1264
|
+
success: true,
|
|
1265
|
+
stdout: stdout.trim()
|
|
1266
|
+
};
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
return {
|
|
1269
|
+
success: false,
|
|
1270
|
+
error: `Failed to list marketplaces: ${get_redacted_error_message(error)}`
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Get the scope description for display
|
|
1276
|
+
*/
|
|
1277
|
+
function get_scope_description(scope) {
|
|
1278
|
+
switch (scope) {
|
|
1279
|
+
case "local": return "This project only (default)";
|
|
1280
|
+
case "project": return "Shared via .mcp.json (version controlled)";
|
|
1281
|
+
case "user": return "Global - all projects";
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Validate a plugin or marketplace manifest via Claude CLI
|
|
1286
|
+
*/
|
|
1287
|
+
async function validate_plugin_via_cli(path) {
|
|
1288
|
+
if (!await check_claude_cli()) return {
|
|
1289
|
+
success: false,
|
|
1290
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1291
|
+
};
|
|
1292
|
+
try {
|
|
1293
|
+
const { stdout } = await run_claude([
|
|
1294
|
+
"plugin",
|
|
1295
|
+
"validate",
|
|
1296
|
+
path
|
|
1297
|
+
]);
|
|
1298
|
+
return {
|
|
1299
|
+
success: true,
|
|
1300
|
+
stdout: stdout.trim()
|
|
1301
|
+
};
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
return {
|
|
1304
|
+
success: false,
|
|
1305
|
+
error: `Validation failed: ${get_redacted_error_message(error)}`
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Get details about an MCP server via Claude CLI
|
|
1311
|
+
*/
|
|
1312
|
+
async function mcp_get_via_cli(name) {
|
|
1313
|
+
if (!await check_claude_cli()) return {
|
|
1314
|
+
success: false,
|
|
1315
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1316
|
+
};
|
|
1317
|
+
try {
|
|
1318
|
+
const { stdout } = await run_claude([
|
|
1319
|
+
"mcp",
|
|
1320
|
+
"get",
|
|
1321
|
+
name
|
|
1322
|
+
]);
|
|
1323
|
+
return {
|
|
1324
|
+
success: true,
|
|
1325
|
+
stdout: stdout.trim()
|
|
1326
|
+
};
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
return {
|
|
1329
|
+
success: false,
|
|
1330
|
+
error: `Failed to get server details: ${get_redacted_error_message(error)}`
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Add an MCP server from raw JSON via Claude CLI
|
|
1336
|
+
*/
|
|
1337
|
+
async function mcp_add_json_via_cli(name, json, scope = "local") {
|
|
1338
|
+
if (!await check_claude_cli()) return {
|
|
1339
|
+
success: false,
|
|
1340
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1341
|
+
};
|
|
1342
|
+
try {
|
|
1343
|
+
await run_claude([
|
|
1344
|
+
"mcp",
|
|
1345
|
+
"add-json",
|
|
1346
|
+
name,
|
|
1347
|
+
json,
|
|
1348
|
+
"--scope",
|
|
1349
|
+
scope
|
|
1350
|
+
]);
|
|
1351
|
+
return { success: true };
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
return {
|
|
1354
|
+
success: false,
|
|
1355
|
+
error: `Failed to add server from JSON: ${get_redacted_error_message(error)}`
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Reset project-scoped MCP server choices via Claude CLI
|
|
1361
|
+
*/
|
|
1362
|
+
async function mcp_reset_project_choices_via_cli() {
|
|
1363
|
+
if (!await check_claude_cli()) return {
|
|
1364
|
+
success: false,
|
|
1365
|
+
error: "Claude CLI not found. Please install Claude Code CLI."
|
|
1366
|
+
};
|
|
1367
|
+
try {
|
|
1368
|
+
await run_claude(["mcp", "reset-project-choices"]);
|
|
1369
|
+
return { success: true };
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
return {
|
|
1372
|
+
success: false,
|
|
1373
|
+
error: `Failed to reset project choices: ${get_redacted_error_message(error)}`
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Get scope options for select prompt
|
|
1379
|
+
*/
|
|
1380
|
+
function get_scope_options() {
|
|
1381
|
+
return [
|
|
1382
|
+
{
|
|
1383
|
+
value: "local",
|
|
1384
|
+
label: "Local",
|
|
1385
|
+
hint: "This project only (default)"
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
value: "project",
|
|
1389
|
+
label: "Project",
|
|
1390
|
+
hint: "Shared via .mcp.json (git)"
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
value: "user",
|
|
1394
|
+
label: "User (Global)",
|
|
1395
|
+
hint: "Available in all projects"
|
|
1396
|
+
}
|
|
1397
|
+
];
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
254
1400
|
//#region src/commands/edit-config.ts
|
|
255
1401
|
async function edit_config() {
|
|
256
1402
|
try {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
1403
|
+
const client_id = await select({
|
|
1404
|
+
message: "Which MCP client do you want to edit?",
|
|
1405
|
+
options: client_adapters.map((adapter) => ({
|
|
1406
|
+
value: adapter.id,
|
|
1407
|
+
label: adapter.label
|
|
1408
|
+
})),
|
|
1409
|
+
initialValue: "claude-code"
|
|
262
1410
|
});
|
|
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.");
|
|
1411
|
+
if (typeof client_id === "symbol") return;
|
|
1412
|
+
const adapter = client_adapters.find((candidate) => candidate.id === client_id);
|
|
1413
|
+
if (!adapter) return;
|
|
1414
|
+
if (adapter.id === "claude-code") {
|
|
1415
|
+
await edit_claude_config();
|
|
276
1416
|
return;
|
|
277
1417
|
}
|
|
278
|
-
|
|
279
|
-
|
|
1418
|
+
await edit_client_config(adapter);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
throw new Error(`Failed to edit configuration: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
async function edit_client_config(adapter) {
|
|
1424
|
+
if (!adapter.writeEnabled) {
|
|
1425
|
+
note(`${adapter.label} support is read-only for now.`);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const location = await select_config_location(adapter);
|
|
1429
|
+
if (!location) return;
|
|
1430
|
+
const servers = await adapter.readLocation(location);
|
|
1431
|
+
if (servers.length === 0) {
|
|
1432
|
+
note(`No MCP servers found at ${location.path}.`);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const selected_names = await multiselect({
|
|
1436
|
+
message: `Toggle MCP servers for ${adapter.label}:`,
|
|
1437
|
+
options: servers.map((server) => ({
|
|
1438
|
+
value: server.name,
|
|
1439
|
+
label: server.name,
|
|
1440
|
+
hint: server_hint(server)
|
|
1441
|
+
})),
|
|
1442
|
+
initialValues: servers.filter((server) => server.disabled !== true).map((server) => server.name),
|
|
1443
|
+
required: false
|
|
1444
|
+
});
|
|
1445
|
+
if (typeof selected_names === "symbol") return;
|
|
1446
|
+
await adapter.writeEnabled(location, selected_names);
|
|
1447
|
+
note(`Configuration updated!\nClient: ${adapter.label}\nConfig: ${location.path}\nEnabled servers: ${selected_names.length}`);
|
|
1448
|
+
}
|
|
1449
|
+
async function select_config_location(adapter) {
|
|
1450
|
+
const locations = adapter.locations();
|
|
1451
|
+
if (locations.length === 1) return locations[0];
|
|
1452
|
+
const location_path = await select({
|
|
1453
|
+
message: `Which ${adapter.label} configuration do you want to edit?`,
|
|
1454
|
+
options: locations.map((location) => ({
|
|
1455
|
+
value: location.path,
|
|
1456
|
+
label: `${location.scope} — ${location.description}`,
|
|
1457
|
+
hint: location.path
|
|
1458
|
+
}))
|
|
1459
|
+
});
|
|
1460
|
+
if (typeof location_path === "symbol") return null;
|
|
1461
|
+
return locations.find((location) => location.path === location_path) ?? null;
|
|
1462
|
+
}
|
|
1463
|
+
function server_hint(server) {
|
|
1464
|
+
return [
|
|
1465
|
+
server.disabled === true ? "off" : "on",
|
|
1466
|
+
server.command ? [server.command, ...server.args ?? []].join(" ") : server.url ? redact_url(server.url) : server.transport,
|
|
1467
|
+
server.description
|
|
1468
|
+
].filter(Boolean).join(" · ");
|
|
1469
|
+
}
|
|
1470
|
+
async function edit_claude_config() {
|
|
1471
|
+
const cli_available = await check_claude_cli();
|
|
1472
|
+
const scope = await select({
|
|
1473
|
+
message: "Which Claude Code configuration do you want to edit?",
|
|
1474
|
+
options: get_scope_options(),
|
|
1475
|
+
initialValue: "local"
|
|
1476
|
+
});
|
|
1477
|
+
if (typeof scope === "symbol") return;
|
|
1478
|
+
const current_config = await read_claude_config();
|
|
1479
|
+
let all_servers = await get_all_available_servers();
|
|
1480
|
+
if (all_servers.length === 0 && current_config.mcpServers) {
|
|
1481
|
+
const current_servers = get_enabled_servers(current_config);
|
|
1482
|
+
if (current_servers.length > 0) {
|
|
1483
|
+
await sync_servers_to_registry(current_servers);
|
|
1484
|
+
all_servers = current_servers;
|
|
1485
|
+
note(`Imported ${current_servers.length} servers from your .claude.json file into registry.`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (all_servers.length === 0) {
|
|
1489
|
+
note("No MCP servers found in .claude.json or registry. Add servers with the CLI first.");
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const currently_enabled = await get_enabled_servers_for_scope(scope);
|
|
1493
|
+
const selected_server_names = await multiselect({
|
|
1494
|
+
message: `Select MCP servers for ${get_scope_description(scope)}:`,
|
|
1495
|
+
options: all_servers.map((server) => ({
|
|
280
1496
|
value: server.name,
|
|
281
1497
|
label: server.name,
|
|
282
1498
|
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 {
|
|
1499
|
+
})),
|
|
1500
|
+
initialValues: currently_enabled,
|
|
1501
|
+
required: false
|
|
1502
|
+
});
|
|
1503
|
+
if (typeof selected_server_names === "symbol") return;
|
|
1504
|
+
const selected_servers = all_servers.filter((server) => selected_server_names.includes(server.name));
|
|
1505
|
+
const servers_to_add = selected_server_names.filter((name) => !currently_enabled.includes(name));
|
|
1506
|
+
const servers_to_remove = currently_enabled.filter((name) => !selected_server_names.includes(name));
|
|
1507
|
+
if (cli_available && (scope === "local" || scope === "project")) {
|
|
1508
|
+
let error_count = 0;
|
|
1509
|
+
for (const name of servers_to_add) {
|
|
1510
|
+
const server = all_servers.find((s) => s.name === name);
|
|
1511
|
+
if (server) {
|
|
1512
|
+
const result = await add_mcp_via_cli(server, scope);
|
|
1513
|
+
if (!result.success) {
|
|
312
1514
|
error_count++;
|
|
313
|
-
log.warn(`Failed to
|
|
1515
|
+
log.warn(`Failed to add ${name}: ${result.error}`);
|
|
314
1516
|
}
|
|
315
1517
|
}
|
|
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
1518
|
}
|
|
325
|
-
|
|
326
|
-
|
|
1519
|
+
for (const name of servers_to_remove) {
|
|
1520
|
+
const result = await remove_mcp_via_cli(name, scope);
|
|
1521
|
+
if (!result.success) {
|
|
1522
|
+
error_count++;
|
|
1523
|
+
log.warn(`Failed to remove ${name}: ${result.error}`);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
await sync_servers_to_registry(selected_servers);
|
|
1527
|
+
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}`);
|
|
1528
|
+
else note(`Configuration updated!\nScope: ${get_scope_description(scope)}\nEnabled servers: ${selected_servers.length}`);
|
|
1529
|
+
} else {
|
|
1530
|
+
await write_claude_config(create_config_from_servers(selected_servers));
|
|
1531
|
+
await sync_servers_to_registry(selected_servers);
|
|
1532
|
+
if (!cli_available && scope !== "user") log.warn(`Claude CLI not available. Changes written to ~/.claude.json (user scope) instead of ${scope} scope.`);
|
|
1533
|
+
note(`Configuration updated!\nEnabled servers: ${selected_servers.length}`);
|
|
327
1534
|
}
|
|
328
1535
|
}
|
|
329
1536
|
//#endregion
|
|
@@ -604,6 +1811,171 @@ async function manage_cache() {
|
|
|
604
1811
|
}
|
|
605
1812
|
}
|
|
606
1813
|
//#endregion
|
|
1814
|
+
//#region src/core/hook-state.ts
|
|
1815
|
+
async function read_disabled_hooks() {
|
|
1816
|
+
try {
|
|
1817
|
+
const content = await readFile(get_disabled_hooks_path(), "utf-8");
|
|
1818
|
+
return JSON.parse(content);
|
|
1819
|
+
} catch {
|
|
1820
|
+
return [];
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
async function write_disabled_hooks(entries) {
|
|
1824
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
1825
|
+
await safe_json_write(get_disabled_hooks_path(), entries, " ");
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Remove a specific hook handler from a hooks.json file by matching the handler.
|
|
1829
|
+
* Returns true if the hook was found and removed.
|
|
1830
|
+
*/
|
|
1831
|
+
async function remove_hook_from_file(hooks_path, event, handler) {
|
|
1832
|
+
let content;
|
|
1833
|
+
try {
|
|
1834
|
+
content = await readFile(hooks_path, "utf-8");
|
|
1835
|
+
} catch {
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
const hooks_data = JSON.parse(content);
|
|
1839
|
+
const hooks_obj = hooks_data.hooks || hooks_data;
|
|
1840
|
+
const matchers = hooks_obj[event];
|
|
1841
|
+
if (!matchers) return false;
|
|
1842
|
+
let removed = false;
|
|
1843
|
+
for (const m of matchers) {
|
|
1844
|
+
const idx = m.hooks?.findIndex((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt);
|
|
1845
|
+
if (idx !== void 0 && idx >= 0) {
|
|
1846
|
+
m.hooks.splice(idx, 1);
|
|
1847
|
+
removed = true;
|
|
1848
|
+
if (m.hooks.length === 0) matchers.splice(matchers.indexOf(m), 1);
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (!removed) return false;
|
|
1853
|
+
if (matchers.length === 0) delete hooks_obj[event];
|
|
1854
|
+
await safe_json_write(hooks_path, hooks_data, " ");
|
|
1855
|
+
return true;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Get all hooks.json paths for a plugin (cache + marketplace source).
|
|
1859
|
+
*/
|
|
1860
|
+
function get_all_hooks_paths(plugin_key, primary_path) {
|
|
1861
|
+
const paths = [primary_path];
|
|
1862
|
+
const at_index = plugin_key.lastIndexOf("@");
|
|
1863
|
+
if (at_index > 0) {
|
|
1864
|
+
const plugin_name = plugin_key.substring(0, at_index);
|
|
1865
|
+
const marketplace_name = plugin_key.substring(at_index + 1);
|
|
1866
|
+
paths.push(join(get_marketplaces_dir(), marketplace_name, "plugins", plugin_name, "hooks", "hooks.json"));
|
|
1867
|
+
}
|
|
1868
|
+
return [...new Set(paths)];
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Disable a specific hook from a plugin.
|
|
1872
|
+
* Removes from both cache and marketplace source hooks.json files.
|
|
1873
|
+
*/
|
|
1874
|
+
async function disable_plugin_hook(entry) {
|
|
1875
|
+
if (!entry.hooks_json_path || !entry.plugin_key) throw new Error("Not a plugin hook");
|
|
1876
|
+
const disabled = await read_disabled_hooks();
|
|
1877
|
+
disabled.push({
|
|
1878
|
+
plugin_key: entry.plugin_key,
|
|
1879
|
+
hooks_json_path: entry.hooks_json_path,
|
|
1880
|
+
event: entry.event,
|
|
1881
|
+
matcher: entry.matcher,
|
|
1882
|
+
matcher_index: entry.matcher_index,
|
|
1883
|
+
hook_index: entry.hook_index,
|
|
1884
|
+
original_handler: entry.handler,
|
|
1885
|
+
disabled_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1886
|
+
});
|
|
1887
|
+
await write_disabled_hooks(disabled);
|
|
1888
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
1889
|
+
for (const hooks_path of all_paths) await remove_hook_from_file(hooks_path, entry.event, entry.handler);
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Add a hook handler back into a hooks.json file.
|
|
1893
|
+
*/
|
|
1894
|
+
async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
|
|
1895
|
+
let hooks_data;
|
|
1896
|
+
try {
|
|
1897
|
+
const content = await readFile(hooks_path, "utf-8");
|
|
1898
|
+
hooks_data = JSON.parse(content);
|
|
1899
|
+
} catch {
|
|
1900
|
+
hooks_data = { hooks: {} };
|
|
1901
|
+
}
|
|
1902
|
+
const hooks_obj = hooks_data.hooks || (hooks_data.hooks = {});
|
|
1903
|
+
if (!hooks_obj[event]) hooks_obj[event] = [];
|
|
1904
|
+
const matchers = hooks_obj[event];
|
|
1905
|
+
let matcher = matchers.find((m) => (m.matcher || void 0) === matcher_pattern);
|
|
1906
|
+
if (!matcher) {
|
|
1907
|
+
matcher = { hooks: [] };
|
|
1908
|
+
if (matcher_pattern) matcher.matcher = matcher_pattern;
|
|
1909
|
+
matchers.push(matcher);
|
|
1910
|
+
}
|
|
1911
|
+
if (matcher.hooks.some((h) => h.type === handler.type && h.command === handler.command && h.url === handler.url && h.prompt === handler.prompt)) return;
|
|
1912
|
+
matcher.hooks.push(handler);
|
|
1913
|
+
await safe_json_write(hooks_path, hooks_data, " ");
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Re-enable a previously disabled plugin hook.
|
|
1917
|
+
* Restores to both cache and marketplace source hooks.json files.
|
|
1918
|
+
*/
|
|
1919
|
+
async function enable_plugin_hook(disabled_entry) {
|
|
1920
|
+
const all_paths = get_all_hooks_paths(disabled_entry.plugin_key, disabled_entry.hooks_json_path);
|
|
1921
|
+
for (const hooks_path of all_paths) await add_hook_to_file(hooks_path, disabled_entry.event, disabled_entry.matcher, disabled_entry.original_handler);
|
|
1922
|
+
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)));
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Check if any previously disabled hooks have been restored (e.g. by marketplace update).
|
|
1926
|
+
* Returns entries that were re-added and need to be re-disabled.
|
|
1927
|
+
*/
|
|
1928
|
+
async function check_restored_hooks() {
|
|
1929
|
+
const disabled = await read_disabled_hooks();
|
|
1930
|
+
if (disabled.length === 0) return [];
|
|
1931
|
+
const restored = [];
|
|
1932
|
+
for (const entry of disabled) {
|
|
1933
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
1934
|
+
let found = false;
|
|
1935
|
+
for (const hooks_path of all_paths) {
|
|
1936
|
+
let hooks_data;
|
|
1937
|
+
try {
|
|
1938
|
+
const content = await readFile(hooks_path, "utf-8");
|
|
1939
|
+
hooks_data = JSON.parse(content);
|
|
1940
|
+
} catch {
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
const matchers = (hooks_data.hooks || hooks_data)[entry.event];
|
|
1944
|
+
if (!matchers) continue;
|
|
1945
|
+
for (const m of matchers) {
|
|
1946
|
+
if ((m.matcher || void 0) !== entry.matcher) continue;
|
|
1947
|
+
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))) {
|
|
1948
|
+
found = true;
|
|
1949
|
+
break;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (found) break;
|
|
1953
|
+
}
|
|
1954
|
+
if (found) restored.push(entry);
|
|
1955
|
+
}
|
|
1956
|
+
return restored;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Re-disable hooks that were restored by a marketplace update.
|
|
1960
|
+
*/
|
|
1961
|
+
async function redisable_restored_hooks(restored) {
|
|
1962
|
+
let success = 0;
|
|
1963
|
+
let failed = 0;
|
|
1964
|
+
for (const entry of restored) try {
|
|
1965
|
+
const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
|
|
1966
|
+
let any_removed = false;
|
|
1967
|
+
for (const hooks_path of all_paths) if (await remove_hook_from_file(hooks_path, entry.event, entry.original_handler)) any_removed = true;
|
|
1968
|
+
if (any_removed) success++;
|
|
1969
|
+
else failed++;
|
|
1970
|
+
} catch {
|
|
1971
|
+
failed++;
|
|
1972
|
+
}
|
|
1973
|
+
return {
|
|
1974
|
+
success,
|
|
1975
|
+
failed
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
//#endregion
|
|
607
1979
|
//#region src/commands/manage-hooks.ts
|
|
608
1980
|
function format_hook(entry) {
|
|
609
1981
|
const detail = entry.handler.command || entry.handler.url || entry.handler.prompt || "(unknown)";
|
|
@@ -902,6 +2274,166 @@ async function manage_marketplace() {
|
|
|
902
2274
|
}
|
|
903
2275
|
}
|
|
904
2276
|
//#endregion
|
|
2277
|
+
//#region src/utils/skills-cli.ts
|
|
2278
|
+
const exec_file_async = promisify(execFile);
|
|
2279
|
+
function split_cli_list(value) {
|
|
2280
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
2281
|
+
}
|
|
2282
|
+
async function run_skills_cli(args) {
|
|
2283
|
+
try {
|
|
2284
|
+
const result = await exec_file_async("npx", [
|
|
2285
|
+
"-y",
|
|
2286
|
+
"skills@latest",
|
|
2287
|
+
...args
|
|
2288
|
+
], { env: {
|
|
2289
|
+
...process.env,
|
|
2290
|
+
CI: "1",
|
|
2291
|
+
NO_COLOR: "1",
|
|
2292
|
+
FORCE_COLOR: "0",
|
|
2293
|
+
TERM: "dumb"
|
|
2294
|
+
} });
|
|
2295
|
+
return {
|
|
2296
|
+
success: true,
|
|
2297
|
+
stdout: redact_text(result.stdout.trim()),
|
|
2298
|
+
stderr: redact_text(result.stderr.trim())
|
|
2299
|
+
};
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
const err = error;
|
|
2302
|
+
return {
|
|
2303
|
+
success: false,
|
|
2304
|
+
stdout: err.stdout ? redact_text(err.stdout.trim()) : void 0,
|
|
2305
|
+
stderr: err.stderr ? redact_text(err.stderr.trim()) : void 0,
|
|
2306
|
+
error: redact_text(err.message)
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
//#endregion
|
|
2311
|
+
//#region src/commands/manage-skills.ts
|
|
2312
|
+
const SKILL_AGENTS = [
|
|
2313
|
+
{
|
|
2314
|
+
value: "claude-code",
|
|
2315
|
+
label: "Claude Code"
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
value: "pi",
|
|
2319
|
+
label: "Pi"
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
value: "opencode",
|
|
2323
|
+
label: "OpenCode"
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
value: "codex",
|
|
2327
|
+
label: "Codex"
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
value: "cursor",
|
|
2331
|
+
label: "Cursor"
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
value: "windsurf",
|
|
2335
|
+
label: "Windsurf"
|
|
2336
|
+
}
|
|
2337
|
+
];
|
|
2338
|
+
async function manage_skills() {
|
|
2339
|
+
const action = await select({
|
|
2340
|
+
message: "Portable skills",
|
|
2341
|
+
options: [
|
|
2342
|
+
{
|
|
2343
|
+
value: "list",
|
|
2344
|
+
label: "List installed skills"
|
|
2345
|
+
},
|
|
2346
|
+
{
|
|
2347
|
+
value: "available",
|
|
2348
|
+
label: "List skills available from source"
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
value: "install",
|
|
2352
|
+
label: "Install skills"
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
value: "update",
|
|
2356
|
+
label: "Update skills"
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
value: "back",
|
|
2360
|
+
label: "Back"
|
|
2361
|
+
}
|
|
2362
|
+
]
|
|
2363
|
+
});
|
|
2364
|
+
if (typeof action === "symbol" || action === "back") return;
|
|
2365
|
+
if (action === "list") {
|
|
2366
|
+
const agent = await select_agent();
|
|
2367
|
+
if (!agent) return;
|
|
2368
|
+
await show_result(await run_skills_cli([
|
|
2369
|
+
"list",
|
|
2370
|
+
"--agent",
|
|
2371
|
+
agent
|
|
2372
|
+
]));
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
if (action === "available") {
|
|
2376
|
+
const source = await prompt_source();
|
|
2377
|
+
if (!source) return;
|
|
2378
|
+
await show_result(await run_skills_cli([
|
|
2379
|
+
"add",
|
|
2380
|
+
source,
|
|
2381
|
+
"--list"
|
|
2382
|
+
]));
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
if (action === "install") {
|
|
2386
|
+
const source = await prompt_source();
|
|
2387
|
+
if (!source) return;
|
|
2388
|
+
const agent = await select_agent();
|
|
2389
|
+
if (!agent) return;
|
|
2390
|
+
const skill = await text({
|
|
2391
|
+
message: "Skill name or * for all skills:",
|
|
2392
|
+
placeholder: "svelte-runes",
|
|
2393
|
+
defaultValue: "*"
|
|
2394
|
+
});
|
|
2395
|
+
if (typeof skill === "symbol") return;
|
|
2396
|
+
await show_result(await run_skills_cli([
|
|
2397
|
+
"add",
|
|
2398
|
+
source,
|
|
2399
|
+
"--agent",
|
|
2400
|
+
agent,
|
|
2401
|
+
"--skill",
|
|
2402
|
+
skill,
|
|
2403
|
+
"--yes"
|
|
2404
|
+
]));
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (action === "update") await show_result(await run_skills_cli(["update", "--yes"]));
|
|
2408
|
+
}
|
|
2409
|
+
async function select_agent() {
|
|
2410
|
+
const agent = await select({
|
|
2411
|
+
message: "Which agent/client?",
|
|
2412
|
+
options: [...SKILL_AGENTS, {
|
|
2413
|
+
value: "*",
|
|
2414
|
+
label: "All supported agents"
|
|
2415
|
+
}],
|
|
2416
|
+
initialValue: "pi"
|
|
2417
|
+
});
|
|
2418
|
+
return typeof agent === "symbol" ? null : agent;
|
|
2419
|
+
}
|
|
2420
|
+
async function prompt_source() {
|
|
2421
|
+
const source = await text({
|
|
2422
|
+
message: "Skills source:",
|
|
2423
|
+
placeholder: "spences10/skills",
|
|
2424
|
+
defaultValue: "spences10/skills"
|
|
2425
|
+
});
|
|
2426
|
+
return typeof source === "symbol" ? null : source;
|
|
2427
|
+
}
|
|
2428
|
+
async function show_result(result) {
|
|
2429
|
+
if (result.success) {
|
|
2430
|
+
if (result.stdout) log.info(result.stdout);
|
|
2431
|
+
note("Done.");
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
log.error(result.stderr || result.error || "skills CLI failed");
|
|
2435
|
+
}
|
|
2436
|
+
//#endregion
|
|
905
2437
|
//#region src/commands/restore.ts
|
|
906
2438
|
async function restore_config() {
|
|
907
2439
|
try {
|
|
@@ -984,6 +2516,68 @@ function format_time_ago(date) {
|
|
|
984
2516
|
else return "just now";
|
|
985
2517
|
}
|
|
986
2518
|
//#endregion
|
|
2519
|
+
//#region src/core/profile.ts
|
|
2520
|
+
async function load_profile(name) {
|
|
2521
|
+
const profile_path = get_profile_path(name);
|
|
2522
|
+
try {
|
|
2523
|
+
await access(profile_path);
|
|
2524
|
+
const content = await readFile(profile_path, "utf-8");
|
|
2525
|
+
const parsed = JSON.parse(content);
|
|
2526
|
+
let config;
|
|
2527
|
+
if (parsed.mcpServers) config = validate_claude_config(parsed);
|
|
2528
|
+
else if (!parsed.enabledPlugins) config = validate_claude_config({ mcpServers: parsed });
|
|
2529
|
+
else config = validate_claude_config({ mcpServers: parsed.mcpServers || {} });
|
|
2530
|
+
return {
|
|
2531
|
+
config,
|
|
2532
|
+
enabledPlugins: parsed.enabledPlugins
|
|
2533
|
+
};
|
|
2534
|
+
} catch (error) {
|
|
2535
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new Error(`Profile '${name}' not found at ${profile_path}`);
|
|
2536
|
+
throw error;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
async function list_profiles() {
|
|
2540
|
+
const profiles_dir = get_profiles_dir();
|
|
2541
|
+
try {
|
|
2542
|
+
await access(profiles_dir);
|
|
2543
|
+
const json_files = (await readdir(profiles_dir)).filter((f) => f.endsWith(".json"));
|
|
2544
|
+
const profiles = [];
|
|
2545
|
+
for (const file of json_files) try {
|
|
2546
|
+
const path = get_profile_path(file);
|
|
2547
|
+
const content = await readFile(path, "utf-8");
|
|
2548
|
+
const parsed = JSON.parse(content);
|
|
2549
|
+
const servers = parsed.mcpServers || parsed;
|
|
2550
|
+
const plugins = parsed.enabledPlugins || {};
|
|
2551
|
+
profiles.push({
|
|
2552
|
+
name: file.replace(".json", ""),
|
|
2553
|
+
path,
|
|
2554
|
+
serverCount: Object.keys(servers).length,
|
|
2555
|
+
pluginCount: Object.keys(plugins).length
|
|
2556
|
+
});
|
|
2557
|
+
} catch {}
|
|
2558
|
+
return profiles;
|
|
2559
|
+
} catch {
|
|
2560
|
+
return [];
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
async function save_profile(name) {
|
|
2564
|
+
const config = await read_claude_config();
|
|
2565
|
+
const settings = await read_claude_settings();
|
|
2566
|
+
const servers = config.mcpServers || {};
|
|
2567
|
+
const plugins = settings.enabledPlugins || {};
|
|
2568
|
+
const server_count = Object.keys(servers).length;
|
|
2569
|
+
const plugin_count = Object.keys(plugins).length;
|
|
2570
|
+
if (server_count === 0 && plugin_count === 0) throw new Error("No MCP servers or plugins configured to save");
|
|
2571
|
+
await ensure_directory_exists(get_profiles_dir());
|
|
2572
|
+
const profile_data = { mcpServers: servers };
|
|
2573
|
+
if (plugin_count > 0) profile_data.enabledPlugins = plugins;
|
|
2574
|
+
await safe_json_write(get_profile_path(name), profile_data, 2);
|
|
2575
|
+
return {
|
|
2576
|
+
serverCount: server_count,
|
|
2577
|
+
pluginCount: plugin_count
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
//#endregion
|
|
987
2581
|
//#region src/index.ts
|
|
988
2582
|
function parse_args() {
|
|
989
2583
|
const args = process.argv.slice(2);
|
|
@@ -1082,6 +2676,65 @@ async function handle_save_profile() {
|
|
|
1082
2676
|
if (counts.pluginCount > 0) parts.push(`${counts.pluginCount} plugins`);
|
|
1083
2677
|
log.success(`Profile '${name}' saved (${parts.join(", ")})`);
|
|
1084
2678
|
}
|
|
2679
|
+
async function handle_client_tools() {
|
|
2680
|
+
const client_id = await select({
|
|
2681
|
+
message: "Which client?",
|
|
2682
|
+
options: client_adapters.map((adapter) => ({
|
|
2683
|
+
value: adapter.id,
|
|
2684
|
+
label: adapter.label
|
|
2685
|
+
})),
|
|
2686
|
+
initialValue: "claude-code"
|
|
2687
|
+
});
|
|
2688
|
+
if (isCancel(client_id)) return;
|
|
2689
|
+
if (client_id !== "claude-code") {
|
|
2690
|
+
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.`);
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
const action = await select({
|
|
2694
|
+
message: "Claude Code tools",
|
|
2695
|
+
options: [
|
|
2696
|
+
{
|
|
2697
|
+
value: "plugins",
|
|
2698
|
+
label: "Plugins",
|
|
2699
|
+
hint: "Claude Code plugin enable/install/update"
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
value: "marketplaces",
|
|
2703
|
+
label: "Marketplaces",
|
|
2704
|
+
hint: "Claude Code plugin marketplaces"
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
value: "hooks",
|
|
2708
|
+
label: "Hooks",
|
|
2709
|
+
hint: "Claude Code settings/plugin hooks"
|
|
2710
|
+
},
|
|
2711
|
+
{
|
|
2712
|
+
value: "cache",
|
|
2713
|
+
label: "Plugin cache",
|
|
2714
|
+
hint: "Claude Code plugin cache maintenance"
|
|
2715
|
+
},
|
|
2716
|
+
{
|
|
2717
|
+
value: "back",
|
|
2718
|
+
label: "Back"
|
|
2719
|
+
}
|
|
2720
|
+
]
|
|
2721
|
+
});
|
|
2722
|
+
if (isCancel(action) || action === "back") return;
|
|
2723
|
+
switch (action) {
|
|
2724
|
+
case "plugins":
|
|
2725
|
+
await edit_plugins();
|
|
2726
|
+
break;
|
|
2727
|
+
case "marketplaces":
|
|
2728
|
+
await manage_marketplace();
|
|
2729
|
+
break;
|
|
2730
|
+
case "hooks":
|
|
2731
|
+
await manage_hooks();
|
|
2732
|
+
break;
|
|
2733
|
+
case "cache":
|
|
2734
|
+
await manage_cache();
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
1085
2738
|
async function main() {
|
|
1086
2739
|
const args = parse_args();
|
|
1087
2740
|
if (args.listProfiles) {
|
|
@@ -1096,8 +2749,8 @@ async function main() {
|
|
|
1096
2749
|
await apply_profile(args.profile);
|
|
1097
2750
|
return;
|
|
1098
2751
|
}
|
|
1099
|
-
intro("MCPick -
|
|
1100
|
-
log.info("
|
|
2752
|
+
intro("MCPick - MCP Configuration Manager");
|
|
2753
|
+
log.info("Primary flow: choose a client, then toggle its MCP servers. Use CLI commands for adding/editing server definitions.");
|
|
1101
2754
|
while (true) try {
|
|
1102
2755
|
const action = await select({
|
|
1103
2756
|
message: "What would you like to do?",
|
|
@@ -1105,32 +2758,17 @@ async function main() {
|
|
|
1105
2758
|
{
|
|
1106
2759
|
value: "edit-config",
|
|
1107
2760
|
label: "Enable / Disable MCP servers",
|
|
1108
|
-
hint: "
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
value: "add-server",
|
|
1112
|
-
label: "Add MCP server",
|
|
1113
|
-
hint: "Register a new MCP server"
|
|
2761
|
+
hint: "Choose client, then toggle servers"
|
|
1114
2762
|
},
|
|
1115
2763
|
{
|
|
1116
|
-
value: "
|
|
1117
|
-
label: "
|
|
1118
|
-
hint: "
|
|
2764
|
+
value: "skills",
|
|
2765
|
+
label: "Skills",
|
|
2766
|
+
hint: "Install/list portable SKILL.md packs via skills CLI"
|
|
1119
2767
|
},
|
|
1120
2768
|
{
|
|
1121
|
-
value: "
|
|
1122
|
-
label: "
|
|
1123
|
-
hint: "
|
|
1124
|
-
},
|
|
1125
|
-
{
|
|
1126
|
-
value: "manage-hooks",
|
|
1127
|
-
label: "Manage hooks",
|
|
1128
|
-
hint: "List, add, or remove event hooks"
|
|
1129
|
-
},
|
|
1130
|
-
{
|
|
1131
|
-
value: "manage-cache",
|
|
1132
|
-
label: "Manage plugin cache",
|
|
1133
|
-
hint: "View, clear, or refresh plugin caches"
|
|
2769
|
+
value: "client-tools",
|
|
2770
|
+
label: "Client-specific tools",
|
|
2771
|
+
hint: "Plugins, hooks, marketplaces, cache where supported"
|
|
1134
2772
|
},
|
|
1135
2773
|
{
|
|
1136
2774
|
value: "load-profile",
|
|
@@ -1167,24 +2805,15 @@ async function main() {
|
|
|
1167
2805
|
case "edit-config":
|
|
1168
2806
|
await edit_config();
|
|
1169
2807
|
break;
|
|
1170
|
-
case "
|
|
1171
|
-
await
|
|
2808
|
+
case "skills":
|
|
2809
|
+
await manage_skills();
|
|
1172
2810
|
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();
|
|
2811
|
+
case "client-tools":
|
|
2812
|
+
await handle_client_tools();
|
|
1181
2813
|
break;
|
|
1182
2814
|
case "backup":
|
|
1183
2815
|
await backup_config();
|
|
1184
2816
|
break;
|
|
1185
|
-
case "add-server":
|
|
1186
|
-
await add_server();
|
|
1187
|
-
break;
|
|
1188
2817
|
case "restore":
|
|
1189
2818
|
await restore_config();
|
|
1190
2819
|
break;
|
|
@@ -1218,6 +2847,7 @@ async function main() {
|
|
|
1218
2847
|
}
|
|
1219
2848
|
}
|
|
1220
2849
|
const SUBCOMMANDS = new Set([
|
|
2850
|
+
"clients",
|
|
1221
2851
|
"list",
|
|
1222
2852
|
"enable",
|
|
1223
2853
|
"disable",
|
|
@@ -1231,21 +2861,23 @@ const SUBCOMMANDS = new Set([
|
|
|
1231
2861
|
"backup",
|
|
1232
2862
|
"restore",
|
|
1233
2863
|
"profile",
|
|
2864
|
+
"skills",
|
|
1234
2865
|
"plugins",
|
|
1235
2866
|
"cache",
|
|
1236
2867
|
"dev",
|
|
1237
2868
|
"marketplace",
|
|
1238
|
-
"reload"
|
|
2869
|
+
"reload",
|
|
2870
|
+
"rollback"
|
|
1239
2871
|
]);
|
|
1240
2872
|
const arg = process.argv[2];
|
|
1241
2873
|
if (arg && SUBCOMMANDS.has(arg) || arg === "--help" || arg === "-h" || !process.stdout.isTTY) {
|
|
1242
2874
|
if (!arg && !process.stdout.isTTY) process.argv.push("--help");
|
|
1243
|
-
import("./cli-
|
|
2875
|
+
import("./cli-By-0nYNQ.js").then((m) => m.run());
|
|
1244
2876
|
} else main().catch((error) => {
|
|
1245
2877
|
console.error("Fatal error:", error);
|
|
1246
2878
|
process.exit(1);
|
|
1247
2879
|
});
|
|
1248
2880
|
//#endregion
|
|
1249
|
-
export {};
|
|
2881
|
+
export { add_client_server as A, get_all_plugins as B, validate_plugin_via_cli as C, list_plugin_backups as D, list_backups as E, resolve_client_location as F, list_config_backups as G, remove_hook as H, set_client_server_enabled as I, restore_config_backup as K, add_hook as L, get_client_adapter as M, list_client_locations as N, read_server_registry as O, remove_client_server as P, build_enabled_plugins as R, update_plugin_via_cli as S, get_all_available_servers as T, write_claude_settings as U, read_claude_settings as V, atomic_json_write as W, mcp_add_json_via_cli as _, split_cli_list as a, remove_mcp_via_cli as b, enable_plugin_hook as c, add_mcp_via_cli as d, install_plugin_via_cli as f, marketplace_update_via_cli as g, marketplace_remove_via_cli as h, run_skills_cli as i, add_client_server_config as j, write_server_registry as k, read_disabled_hooks as l, marketplace_list_via_cli as m, load_profile as n, check_restored_hooks as o, marketplace_add_via_cli as p, save_profile as r, disable_plugin_hook as s, list_profiles as t, redisable_restored_hooks as u, mcp_get_via_cli as v, add_server_to_registry as w, uninstall_plugin_via_cli as x, mcp_reset_project_choices_via_cli as y, get_all_hooks as z };
|
|
1250
2882
|
|
|
1251
2883
|
//# sourceMappingURL=index.js.map
|