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