mcpick 0.0.16 → 0.0.18

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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.18
4
+
5
+ ### Patch Changes
6
+
7
+ - 58ff00f: Add clone command and skip redundant stdio transport flag
8
+ - 7f277da: redact env keys when listing
9
+ - b52fcbd: Show available CLI commands hint in TUI intro
10
+
11
+ ## 0.0.17
12
+
13
+ ### Patch Changes
14
+
15
+ - 54fe401: feat: plugin hook management with per-hook disable/enable
16
+ and update guard
17
+
3
18
  ## 0.0.16
4
19
 
5
20
  ### Patch Changes
@@ -0,0 +1,108 @@
1
+ import { defineCommand } from 'citty';
2
+ import { detect_server_scope, find_server_in_scope, } from '../../core/config.js';
3
+ import { add_server_to_registry } from '../../core/registry.js';
4
+ import { validate_mcp_server } from '../../core/validation.js';
5
+ import { add_mcp_via_cli } from '../../utils/claude-cli.js';
6
+ import { redact_server } from '../../utils/redact.js';
7
+ import { error, output } from '../output.js';
8
+ export default defineCommand({
9
+ meta: {
10
+ name: 'clone',
11
+ description: 'Clone an existing MCP server config with a new name, optionally overriding command/args',
12
+ },
13
+ args: {
14
+ source: {
15
+ type: 'positional',
16
+ description: 'Source server name to clone from',
17
+ required: true,
18
+ },
19
+ name: {
20
+ type: 'positional',
21
+ description: 'New server name',
22
+ required: true,
23
+ },
24
+ command: {
25
+ type: 'string',
26
+ description: 'Override command (e.g. "node" for local dev)',
27
+ },
28
+ args: {
29
+ type: 'string',
30
+ description: 'Override comma-separated arguments',
31
+ },
32
+ scope: {
33
+ type: 'string',
34
+ description: 'Scope for new server (default: same as source)',
35
+ },
36
+ json: {
37
+ type: 'boolean',
38
+ description: 'Output as JSON',
39
+ default: false,
40
+ },
41
+ },
42
+ async run({ args }) {
43
+ if (args.scope &&
44
+ !['local', 'project', 'user'].includes(args.scope)) {
45
+ error(`Invalid scope: ${args.scope}. Use local, project, or user.`);
46
+ }
47
+ // Find the source server
48
+ const scope = args.scope;
49
+ let found;
50
+ if (scope) {
51
+ found = await find_server_in_scope(args.source, scope);
52
+ }
53
+ else {
54
+ found = await detect_server_scope(args.source);
55
+ }
56
+ if (!found) {
57
+ error(`Server '${args.source}' not found${scope ? ` in ${scope} scope` : ' in any scope'}`);
58
+ }
59
+ const target_scope = args.scope || found.scope;
60
+ // Clone the config, applying overrides
61
+ const cloned = {
62
+ ...found.server,
63
+ name: args.name,
64
+ };
65
+ if (args.command) {
66
+ cloned.command = args.command;
67
+ // When overriding command, clear url/type if switching from http/sse to stdio
68
+ delete cloned.url;
69
+ if (cloned.type === 'sse' || cloned.type === 'http') {
70
+ delete cloned.type;
71
+ }
72
+ }
73
+ if (args.args) {
74
+ cloned.args = args.args.split(',');
75
+ }
76
+ // Validate
77
+ let server;
78
+ try {
79
+ server = validate_mcp_server(cloned);
80
+ }
81
+ catch (err) {
82
+ error(`Invalid cloned config: ${err instanceof Error ? err.message : 'validation failed'}`);
83
+ }
84
+ // Add to registry
85
+ await add_server_to_registry(server);
86
+ // Enable via CLI
87
+ const result = await add_mcp_via_cli(server, target_scope);
88
+ if (args.json) {
89
+ output({
90
+ cloned: server.name,
91
+ from: args.source,
92
+ scope: target_scope,
93
+ server: redact_server(server),
94
+ cli: result.success,
95
+ error: result.error,
96
+ }, true);
97
+ }
98
+ else {
99
+ if (result.success) {
100
+ console.log(`Cloned '${args.source}' → '${server.name}' (scope: ${target_scope})`);
101
+ }
102
+ else {
103
+ console.log(`Cloned '${args.source}' → '${server.name}' to registry but CLI failed: ${result.error}`);
104
+ }
105
+ }
106
+ },
107
+ });
108
+ //# sourceMappingURL=clone.js.map
@@ -1,5 +1,6 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { apply_dev_override, list_dev_overrides, restore_all_dev_overrides, restore_dev_override, } from '../../core/dev-override.js';
3
+ import { redact_server_base } from '../../utils/redact.js';
3
4
  import { error, output } from '../output.js';
4
5
  const apply = defineCommand({
5
6
  meta: {
@@ -126,7 +127,12 @@ const list = defineCommand({
126
127
  async run({ args }) {
127
128
  const overrides = await list_dev_overrides();
128
129
  if (args.json) {
129
- output(overrides, true);
130
+ const redacted = overrides.map((o) => ({
131
+ ...o,
132
+ original: redact_server_base(o.original),
133
+ dev: redact_server_base(o.dev),
134
+ }));
135
+ output(redacted, true);
130
136
  return;
131
137
  }
132
138
  if (overrides.length === 0) {
@@ -0,0 +1,314 @@
1
+ import { defineCommand } from 'citty';
2
+ import { check_restored_hooks, disable_plugin_hook, enable_plugin_hook, read_disabled_hooks, redisable_restored_hooks, } from '../../core/hook-state.js';
3
+ import { add_hook, get_all_hooks, remove_hook, } from '../../core/settings.js';
4
+ import { error, output } from '../output.js';
5
+ const list = defineCommand({
6
+ meta: {
7
+ name: 'list',
8
+ description: 'List all configured hooks (settings + plugins + disabled)',
9
+ },
10
+ args: {
11
+ scope: {
12
+ type: 'string',
13
+ description: 'Filter by source: user, project, project-local, or plugin',
14
+ },
15
+ json: {
16
+ type: 'boolean',
17
+ description: 'Output as JSON',
18
+ default: false,
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const hooks = await get_all_hooks();
23
+ const disabled = await read_disabled_hooks();
24
+ const filtered = args.scope
25
+ ? hooks.filter((h) => h.source === args.scope || h.scope === args.scope)
26
+ : hooks;
27
+ if (args.json) {
28
+ output({ active: filtered, disabled }, true);
29
+ }
30
+ else {
31
+ if (filtered.length === 0 && disabled.length === 0) {
32
+ console.log('No hooks configured.');
33
+ return;
34
+ }
35
+ if (filtered.length > 0) {
36
+ for (let i = 0; i < filtered.length; i++) {
37
+ const h = filtered[i];
38
+ const detail = h.handler.command ||
39
+ h.handler.url ||
40
+ h.handler.prompt ||
41
+ '(unknown)';
42
+ const matcher_str = h.matcher ? ` [${h.matcher}]` : '';
43
+ const source = h.source === 'plugin'
44
+ ? `plugin: ${h.plugin_key}`
45
+ : h.scope;
46
+ console.log(`${i}: [${source}] ${h.event}${matcher_str} → ${h.handler.type}: ${detail}`);
47
+ }
48
+ }
49
+ if (disabled.length > 0) {
50
+ console.log('\nDisabled:');
51
+ for (let i = 0; i < disabled.length; i++) {
52
+ const d = disabled[i];
53
+ const detail = d.original_handler.command ||
54
+ d.original_handler.url ||
55
+ d.original_handler.prompt ||
56
+ '(unknown)';
57
+ const matcher_str = d.matcher ? ` [${d.matcher}]` : '';
58
+ console.log(`${i}: [${d.plugin_key}] ${d.event}${matcher_str} → ${d.original_handler.type}: ${detail}`);
59
+ }
60
+ }
61
+ }
62
+ },
63
+ });
64
+ const disable = defineCommand({
65
+ meta: {
66
+ name: 'disable',
67
+ description: 'Disable a hook by index (use "hooks list" to see indices)',
68
+ },
69
+ args: {
70
+ index: {
71
+ type: 'positional',
72
+ description: 'Hook index from "hooks list" (0-based)',
73
+ required: true,
74
+ },
75
+ json: {
76
+ type: 'boolean',
77
+ description: 'Output as JSON',
78
+ default: false,
79
+ },
80
+ },
81
+ async run({ args }) {
82
+ const hooks = await get_all_hooks();
83
+ const idx = parseInt(args.index, 10);
84
+ if (isNaN(idx) || idx < 0 || idx >= hooks.length) {
85
+ error(`Invalid index: ${args.index}. Run "mcpick hooks list" to see available hooks (0-${hooks.length - 1}).`);
86
+ }
87
+ const entry = hooks[idx];
88
+ if (entry.source === 'plugin') {
89
+ await disable_plugin_hook(entry);
90
+ if (args.json) {
91
+ output({
92
+ disabled: true,
93
+ event: entry.event,
94
+ plugin_key: entry.plugin_key,
95
+ }, true);
96
+ }
97
+ else {
98
+ console.log(`Disabled: ${entry.event} from ${entry.plugin_key}`);
99
+ console.log('Restart Claude Code for changes to take effect.');
100
+ }
101
+ }
102
+ else {
103
+ // Settings hook — remove it
104
+ await remove_hook(entry);
105
+ if (args.json) {
106
+ output({
107
+ removed: true,
108
+ event: entry.event,
109
+ scope: entry.scope,
110
+ }, true);
111
+ }
112
+ else {
113
+ console.log(`Removed: ${entry.event} (${entry.scope})`);
114
+ }
115
+ }
116
+ },
117
+ });
118
+ const enable = defineCommand({
119
+ meta: {
120
+ name: 'enable',
121
+ description: 'Re-enable a disabled hook by index (use "hooks list" to see disabled hooks)',
122
+ },
123
+ args: {
124
+ index: {
125
+ type: 'positional',
126
+ description: 'Disabled hook index from "hooks list" (0-based)',
127
+ required: true,
128
+ },
129
+ json: {
130
+ type: 'boolean',
131
+ description: 'Output as JSON',
132
+ default: false,
133
+ },
134
+ },
135
+ async run({ args }) {
136
+ const disabled = await read_disabled_hooks();
137
+ const idx = parseInt(args.index, 10);
138
+ if (isNaN(idx) || idx < 0 || idx >= disabled.length) {
139
+ error(`Invalid index: ${args.index}. Run "mcpick hooks list" to see disabled hooks (0-${disabled.length - 1}).`);
140
+ }
141
+ const entry = disabled[idx];
142
+ await enable_plugin_hook(entry);
143
+ if (args.json) {
144
+ output({
145
+ enabled: true,
146
+ event: entry.event,
147
+ plugin_key: entry.plugin_key,
148
+ }, true);
149
+ }
150
+ else {
151
+ console.log(`Re-enabled: ${entry.event} for ${entry.plugin_key}`);
152
+ console.log('Restart Claude Code for changes to take effect.');
153
+ }
154
+ },
155
+ });
156
+ const add = defineCommand({
157
+ meta: {
158
+ name: 'add',
159
+ description: 'Add a new settings-based hook',
160
+ },
161
+ args: {
162
+ event: {
163
+ type: 'positional',
164
+ description: 'Hook event type (e.g. UserPromptSubmit, PreToolUse)',
165
+ required: true,
166
+ },
167
+ handler_type: {
168
+ type: 'positional',
169
+ description: 'Handler type: command, prompt, http, or agent',
170
+ required: true,
171
+ },
172
+ value: {
173
+ type: 'positional',
174
+ description: 'Handler value (command string, prompt text, URL, or agent prompt)',
175
+ required: true,
176
+ },
177
+ matcher: {
178
+ type: 'string',
179
+ description: 'Matcher pattern (e.g. Bash, Edit|Write) — only for tool/session events',
180
+ },
181
+ scope: {
182
+ type: 'string',
183
+ description: 'Scope: user, project, or project-local (default: user)',
184
+ default: 'user',
185
+ },
186
+ json: {
187
+ type: 'boolean',
188
+ description: 'Output as JSON',
189
+ default: false,
190
+ },
191
+ },
192
+ async run({ args }) {
193
+ const scope = args.scope;
194
+ if (!['user', 'project', 'project-local'].includes(scope)) {
195
+ error(`Invalid scope: ${scope}. Use user, project, or project-local.`);
196
+ }
197
+ const handler_type = args.handler_type;
198
+ if (!['command', 'prompt', 'http', 'agent'].includes(handler_type)) {
199
+ error(`Invalid handler type: ${handler_type}. Use command, prompt, http, or agent.`);
200
+ }
201
+ const handler = { type: handler_type };
202
+ if (handler_type === 'command')
203
+ handler.command = args.value;
204
+ else if (handler_type === 'prompt')
205
+ handler.prompt = args.value;
206
+ else if (handler_type === 'http')
207
+ handler.url = args.value;
208
+ else if (handler_type === 'agent')
209
+ handler.prompt = args.value;
210
+ await add_hook(scope, args.event, args.matcher || undefined, handler);
211
+ if (args.json) {
212
+ output({
213
+ added: true,
214
+ event: args.event,
215
+ handler_type,
216
+ scope,
217
+ matcher: args.matcher || null,
218
+ }, true);
219
+ }
220
+ else {
221
+ console.log(`Hook added: ${args.event} → ${handler_type} (${scope})`);
222
+ }
223
+ },
224
+ });
225
+ const remove = defineCommand({
226
+ meta: {
227
+ name: 'remove',
228
+ description: 'Remove a settings hook by index (use "hooks list" to see indices)',
229
+ },
230
+ args: {
231
+ index: {
232
+ type: 'positional',
233
+ description: 'Hook index from "hooks list" (0-based)',
234
+ required: true,
235
+ },
236
+ json: {
237
+ type: 'boolean',
238
+ description: 'Output as JSON',
239
+ default: false,
240
+ },
241
+ },
242
+ async run({ args }) {
243
+ const hooks = await get_all_hooks();
244
+ const settings_hooks = hooks.filter((h) => h.source !== 'plugin');
245
+ const idx = parseInt(args.index, 10);
246
+ if (isNaN(idx) || idx < 0 || idx >= settings_hooks.length) {
247
+ error(`Invalid index: ${args.index}. Use "mcpick hooks list" to see settings hooks.`);
248
+ }
249
+ const entry = settings_hooks[idx];
250
+ await remove_hook(entry);
251
+ if (args.json) {
252
+ output({
253
+ removed: true,
254
+ event: entry.event,
255
+ scope: entry.scope,
256
+ }, true);
257
+ }
258
+ else {
259
+ const detail = entry.handler.command ||
260
+ entry.handler.url ||
261
+ entry.handler.prompt ||
262
+ '(unknown)';
263
+ console.log(`Removed: [${entry.scope}] ${entry.event} → ${entry.handler.type}: ${detail}`);
264
+ }
265
+ },
266
+ });
267
+ const check = defineCommand({
268
+ meta: {
269
+ name: 'check',
270
+ description: 'Check if marketplace updates restored any disabled hooks',
271
+ },
272
+ args: {
273
+ fix: {
274
+ type: 'boolean',
275
+ description: 'Automatically re-disable restored hooks',
276
+ default: false,
277
+ },
278
+ json: {
279
+ type: 'boolean',
280
+ description: 'Output as JSON',
281
+ default: false,
282
+ },
283
+ },
284
+ async run({ args }) {
285
+ const restored = await check_restored_hooks();
286
+ if (args.json) {
287
+ output({ restored: restored.length, hooks: restored }, true);
288
+ return;
289
+ }
290
+ if (restored.length === 0) {
291
+ console.log('No disabled hooks were restored. All good.');
292
+ return;
293
+ }
294
+ console.log(`${restored.length} disabled hook(s) were restored:`);
295
+ for (const r of restored) {
296
+ console.log(` ${r.plugin_key}: ${r.event}`);
297
+ }
298
+ if (args.fix) {
299
+ const result = await redisable_restored_hooks(restored);
300
+ console.log(`Re-disabled ${result.success} hook(s).${result.failed > 0 ? ` Failed: ${result.failed}` : ''}`);
301
+ }
302
+ else {
303
+ console.log('Run with --fix to re-disable, or use "mcpick hooks disable".');
304
+ }
305
+ },
306
+ });
307
+ export default defineCommand({
308
+ meta: {
309
+ name: 'hooks',
310
+ description: 'Manage hooks (settings + plugin). Disable individual plugin hooks, add/remove settings hooks.',
311
+ },
312
+ subCommands: { list, disable, enable, add, remove, check },
313
+ });
314
+ //# sourceMappingURL=hooks.js.map
@@ -1,6 +1,7 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { get_enabled_servers_for_scope } from '../../core/config.js';
3
3
  import { get_all_available_servers } from '../../core/registry.js';
4
+ import { redact_server } from '../../utils/redact.js';
4
5
  import { error, output } from '../output.js';
5
6
  export default defineCommand({
6
7
  meta: {
@@ -38,7 +39,7 @@ export default defineCommand({
38
39
  for (const scope of scopes) {
39
40
  status[scope] = enabled_by_scope[scope].includes(server.name);
40
41
  }
41
- const { name, ...rest } = server;
42
+ const { name, ...rest } = redact_server(server);
42
43
  return { name, ...status, ...rest };
43
44
  });
44
45
  output(data, true);
@@ -1,4 +1,5 @@
1
1
  import { defineCommand } from 'citty';
2
+ import { check_restored_hooks, redisable_restored_hooks, } from '../../core/hook-state.js';
2
3
  import { read_marketplace_manifest } from '../../core/plugin-cache.js';
3
4
  import { marketplace_add_via_cli, marketplace_list_via_cli, marketplace_remove_via_cli, marketplace_update_via_cli, } from '../../utils/claude-cli.js';
4
5
  import { error, output } from '../output.js';
@@ -182,6 +183,13 @@ const update = defineCommand({
182
183
  console.log(args.name
183
184
  ? `Marketplace updated: ${args.name}`
184
185
  : 'All marketplaces updated.');
186
+ // Check if update restored any disabled hooks
187
+ const restored = await check_restored_hooks();
188
+ if (restored.length > 0) {
189
+ console.log(`\nWarning: ${restored.length} disabled hook(s) were restored by the update.`);
190
+ const redisable_result = await redisable_restored_hooks(restored);
191
+ console.log(`Re-disabled ${redisable_result.success} hook(s).${redisable_result.failed > 0 ? ` Failed: ${redisable_result.failed}` : ''}`);
192
+ }
185
193
  }
186
194
  else {
187
195
  error(result.error || 'Unknown error');
package/dist/cli/index.js CHANGED
@@ -11,12 +11,14 @@ const main = defineCommand({
11
11
  remove: () => import('./commands/remove.js').then((m) => m.default),
12
12
  add: () => import('./commands/add.js').then((m) => m.default),
13
13
  'add-json': () => import('./commands/add-json.js').then((m) => m.default),
14
+ clone: () => import('./commands/clone.js').then((m) => m.default),
14
15
  get: () => import('./commands/get.js').then((m) => m.default),
15
16
  'reset-project-choices': () => import('./commands/reset-project-choices.js').then((m) => m.default),
16
17
  backup: () => import('./commands/backup.js').then((m) => m.default),
17
18
  restore: () => import('./commands/restore.js').then((m) => m.default),
18
19
  profile: () => import('./commands/profile.js').then((m) => m.default),
19
20
  plugins: () => import('./commands/plugins.js').then((m) => m.default),
21
+ hooks: () => import('./commands/hooks.js').then((m) => m.default),
20
22
  cache: () => import('./commands/cache.js').then((m) => m.default),
21
23
  dev: () => import('./commands/dev.js').then((m) => m.default),
22
24
  marketplace: () => import('./commands/marketplace.js').then((m) => m.default),
@@ -0,0 +1,99 @@
1
+ import { isCancel, log, multiselect, note } from '@clack/prompts';
2
+ import { disable_plugin_hook, enable_plugin_hook, read_disabled_hooks, } from '../core/hook-state.js';
3
+ import { get_all_hooks } from '../core/settings.js';
4
+ function format_hook(entry) {
5
+ const detail = entry.handler.command ||
6
+ entry.handler.url ||
7
+ entry.handler.prompt ||
8
+ '(unknown)';
9
+ const truncated = detail.length > 50 ? detail.substring(0, 47) + '...' : detail;
10
+ return `${entry.event} → ${entry.handler.type}: ${truncated}`;
11
+ }
12
+ function format_source(entry) {
13
+ if (entry.source === 'plugin' && entry.plugin_key) {
14
+ return entry.plugin_key;
15
+ }
16
+ return entry.scope;
17
+ }
18
+ export async function manage_hooks() {
19
+ // Get all active hooks + disabled hooks
20
+ const active_hooks = await get_all_hooks();
21
+ const disabled = await read_disabled_hooks();
22
+ const items = [];
23
+ // Active hooks
24
+ for (let i = 0; i < active_hooks.length; i++) {
25
+ const h = active_hooks[i];
26
+ items.push({
27
+ id: `active:${i}`,
28
+ active_entry: h,
29
+ label: format_hook(h),
30
+ hint: format_source(h),
31
+ });
32
+ }
33
+ // Disabled hooks
34
+ for (let i = 0; i < disabled.length; i++) {
35
+ const d = disabled[i];
36
+ const detail = d.original_handler.command ||
37
+ d.original_handler.url ||
38
+ d.original_handler.prompt ||
39
+ '(unknown)';
40
+ const truncated = detail.length > 50 ? detail.substring(0, 47) + '...' : detail;
41
+ items.push({
42
+ id: `disabled:${i}`,
43
+ disabled_index: i,
44
+ label: `${d.event} → ${d.original_handler.type}: ${truncated}`,
45
+ hint: `${d.plugin_key} (disabled)`,
46
+ });
47
+ }
48
+ if (items.length === 0) {
49
+ note('No hooks found (settings or plugins).');
50
+ return;
51
+ }
52
+ // Currently enabled = active hooks
53
+ const currently_enabled = items
54
+ .filter((item) => item.active_entry)
55
+ .map((item) => item.id);
56
+ const selected = await multiselect({
57
+ message: 'Toggle hooks on/off:',
58
+ options: items.map((item) => ({
59
+ value: item.id,
60
+ label: item.label,
61
+ hint: item.hint,
62
+ })),
63
+ initialValues: currently_enabled,
64
+ required: false,
65
+ });
66
+ if (isCancel(selected))
67
+ return;
68
+ const selected_set = new Set(selected);
69
+ let changes = 0;
70
+ // Disable hooks that were deselected (active → disabled)
71
+ for (const item of items) {
72
+ if (!item.active_entry)
73
+ continue;
74
+ if (selected_set.has(item.id))
75
+ continue;
76
+ // Only plugin hooks can be disabled — settings hooks get removed
77
+ if (item.active_entry.source === 'plugin') {
78
+ await disable_plugin_hook(item.active_entry);
79
+ changes++;
80
+ }
81
+ }
82
+ // Enable hooks that were selected (disabled → active)
83
+ for (const item of items) {
84
+ if (item.disabled_index === undefined)
85
+ continue;
86
+ if (!selected_set.has(item.id))
87
+ continue;
88
+ await enable_plugin_hook(disabled[item.disabled_index]);
89
+ changes++;
90
+ }
91
+ if (changes > 0) {
92
+ log.success(`${changes} hook(s) updated.`);
93
+ log.info('Restart Claude Code for changes to take effect.');
94
+ }
95
+ else {
96
+ log.info('No changes.');
97
+ }
98
+ }
99
+ //# sourceMappingURL=manage-hooks.js.map
@@ -1,4 +1,5 @@
1
1
  import { confirm, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
2
+ import { check_restored_hooks, redisable_restored_hooks, } from '../core/hook-state.js';
2
3
  import { read_known_marketplaces, read_marketplace_manifest, } from '../core/plugin-cache.js';
3
4
  import { get_all_plugins, read_claude_settings, } from '../core/settings.js';
4
5
  import { install_plugin_via_cli, marketplace_add_via_cli, marketplace_remove_via_cli, marketplace_update_via_cli, uninstall_plugin_via_cli, } from '../utils/claude-cli.js';
@@ -216,6 +217,23 @@ async function handle_update() {
216
217
  log.error(result.error || 'Unknown error');
217
218
  }
218
219
  }
220
+ // Check if update restored any disabled hooks
221
+ const restored = await check_restored_hooks();
222
+ if (restored.length > 0) {
223
+ log.warn(`${restored.length} disabled hook(s) were restored by the update.`);
224
+ const should_redisable = await confirm({
225
+ message: 'Re-disable these hooks?',
226
+ });
227
+ if (!isCancel(should_redisable) && should_redisable) {
228
+ const redisable_result = await redisable_restored_hooks(restored);
229
+ if (redisable_result.success > 0) {
230
+ log.success(`Re-disabled ${redisable_result.success} hook(s).`);
231
+ }
232
+ if (redisable_result.failed > 0) {
233
+ log.error(`Failed to re-disable ${redisable_result.failed} hook(s).`);
234
+ }
235
+ }
236
+ }
219
237
  }
220
238
  export async function manage_marketplace() {
221
239
  while (true) {
@@ -42,6 +42,60 @@ export function create_config_from_servers(selected_servers) {
42
42
  });
43
43
  return { mcpServers: mcp_servers };
44
44
  }
45
+ /**
46
+ * Find a server's full config in a specific scope.
47
+ */
48
+ export async function find_server_in_scope(name, scope) {
49
+ if (scope === 'user' || scope === 'local') {
50
+ const config_path = get_claude_config_path();
51
+ try {
52
+ await access(config_path);
53
+ const content = await readFile(config_path, 'utf-8');
54
+ const parsed = JSON.parse(content);
55
+ if (scope === 'user') {
56
+ const server = parsed.mcpServers?.[name];
57
+ if (server)
58
+ return { server, scope: 'user' };
59
+ }
60
+ else {
61
+ const cwd = get_current_project_path();
62
+ const server = parsed.projects?.[cwd]?.mcpServers?.[name];
63
+ if (server)
64
+ return { server, scope: 'local' };
65
+ }
66
+ }
67
+ catch {
68
+ // File doesn't exist
69
+ }
70
+ }
71
+ else if (scope === 'project') {
72
+ const mcp_path = get_project_mcp_json_path();
73
+ try {
74
+ await access(mcp_path);
75
+ const content = await readFile(mcp_path, 'utf-8');
76
+ const parsed = JSON.parse(content);
77
+ const server = parsed.mcpServers?.[name];
78
+ if (server)
79
+ return { server, scope: 'project' };
80
+ }
81
+ catch {
82
+ // File doesn't exist
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Auto-detect which scope a server lives in.
89
+ * Searches local → project → user.
90
+ */
91
+ export async function detect_server_scope(name) {
92
+ for (const scope of ['local', 'project', 'user']) {
93
+ const result = await find_server_in_scope(name, scope);
94
+ if (result)
95
+ return result;
96
+ }
97
+ return null;
98
+ }
45
99
  /**
46
100
  * Read full Claude config including projects section
47
101
  */
@@ -1,6 +1,7 @@
1
- import { access, readFile } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import { atomic_json_write } from '../utils/atomic-write.js';
3
3
  import { get_claude_config_path, get_current_project_path, get_dev_overrides_path, get_project_mcp_json_path, } from '../utils/paths.js';
4
+ import { detect_server_scope, find_server_in_scope, } from './config.js';
4
5
  const EMPTY_OVERRIDES = {
5
6
  version: 1,
6
7
  overrides: {},
@@ -17,62 +18,6 @@ export async function read_dev_overrides() {
17
18
  async function write_dev_overrides(data) {
18
19
  await atomic_json_write(get_dev_overrides_path(), () => data);
19
20
  }
20
- /**
21
- * Read full config for a given scope and return the server entry if found.
22
- * Returns { config, server, scope } or null.
23
- */
24
- async function find_server_in_scope(name, scope) {
25
- if (scope === 'user' || scope === 'local') {
26
- const config_path = get_claude_config_path();
27
- try {
28
- await access(config_path);
29
- const content = await readFile(config_path, 'utf-8');
30
- const parsed = JSON.parse(content);
31
- if (scope === 'user') {
32
- const server = parsed.mcpServers?.[name];
33
- if (server)
34
- return { server, scope: 'user' };
35
- }
36
- else {
37
- // local scope: projects[cwd].mcpServers
38
- const cwd = get_current_project_path();
39
- const server = parsed.projects?.[cwd]?.mcpServers?.[name];
40
- if (server)
41
- return { server, scope: 'local' };
42
- }
43
- }
44
- catch {
45
- // File doesn't exist
46
- }
47
- }
48
- else if (scope === 'project') {
49
- const mcp_path = get_project_mcp_json_path();
50
- try {
51
- await access(mcp_path);
52
- const content = await readFile(mcp_path, 'utf-8');
53
- const parsed = JSON.parse(content);
54
- const server = parsed.mcpServers?.[name];
55
- if (server)
56
- return { server, scope: 'project' };
57
- }
58
- catch {
59
- // File doesn't exist
60
- }
61
- }
62
- return null;
63
- }
64
- /**
65
- * Auto-detect which scope a server lives in.
66
- * Searches local → project → user.
67
- */
68
- async function detect_server_scope(name) {
69
- for (const scope of ['local', 'project', 'user']) {
70
- const result = await find_server_in_scope(name, scope);
71
- if (result)
72
- return result;
73
- }
74
- return null;
75
- }
76
21
  /**
77
22
  * Write a server config into the appropriate scope config file.
78
23
  */
@@ -0,0 +1,220 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { ensure_directory_exists, get_disabled_hooks_path, get_marketplaces_dir, get_mcpick_dir, } from '../utils/paths.js';
4
+ export async function read_disabled_hooks() {
5
+ try {
6
+ const content = await readFile(get_disabled_hooks_path(), 'utf-8');
7
+ return JSON.parse(content);
8
+ }
9
+ catch {
10
+ return [];
11
+ }
12
+ }
13
+ async function write_disabled_hooks(entries) {
14
+ await ensure_directory_exists(get_mcpick_dir());
15
+ await writeFile(get_disabled_hooks_path(), JSON.stringify(entries, null, '\t'), 'utf-8');
16
+ }
17
+ /**
18
+ * Remove a specific hook handler from a hooks.json file by matching the handler.
19
+ * Returns true if the hook was found and removed.
20
+ */
21
+ async function remove_hook_from_file(hooks_path, event, handler) {
22
+ let content;
23
+ try {
24
+ content = await readFile(hooks_path, 'utf-8');
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ const hooks_data = JSON.parse(content);
30
+ const hooks_obj = (hooks_data.hooks || hooks_data);
31
+ const matchers = hooks_obj[event];
32
+ if (!matchers)
33
+ return false;
34
+ let removed = false;
35
+ for (const m of matchers) {
36
+ const idx = m.hooks?.findIndex((h) => h.type === handler.type &&
37
+ h.command === handler.command &&
38
+ h.url === handler.url &&
39
+ h.prompt === handler.prompt);
40
+ if (idx !== undefined && idx >= 0) {
41
+ m.hooks.splice(idx, 1);
42
+ removed = true;
43
+ if (m.hooks.length === 0) {
44
+ matchers.splice(matchers.indexOf(m), 1);
45
+ }
46
+ break;
47
+ }
48
+ }
49
+ if (!removed)
50
+ return false;
51
+ if (matchers.length === 0) {
52
+ delete hooks_obj[event];
53
+ }
54
+ await writeFile(hooks_path, JSON.stringify(hooks_data, null, '\t'), 'utf-8');
55
+ return true;
56
+ }
57
+ /**
58
+ * Get all hooks.json paths for a plugin (cache + marketplace source).
59
+ */
60
+ function get_all_hooks_paths(plugin_key, primary_path) {
61
+ const paths = [primary_path];
62
+ const at_index = plugin_key.lastIndexOf('@');
63
+ if (at_index > 0) {
64
+ const plugin_name = plugin_key.substring(0, at_index);
65
+ const marketplace_name = plugin_key.substring(at_index + 1);
66
+ paths.push(join(get_marketplaces_dir(), marketplace_name, 'plugins', plugin_name, 'hooks', 'hooks.json'));
67
+ }
68
+ return [...new Set(paths)]; // deduplicate
69
+ }
70
+ /**
71
+ * Disable a specific hook from a plugin.
72
+ * Removes from both cache and marketplace source hooks.json files.
73
+ */
74
+ export async function disable_plugin_hook(entry) {
75
+ if (!entry.hooks_json_path || !entry.plugin_key) {
76
+ throw new Error('Not a plugin hook');
77
+ }
78
+ // Save to disabled state
79
+ const disabled = await read_disabled_hooks();
80
+ disabled.push({
81
+ plugin_key: entry.plugin_key,
82
+ hooks_json_path: entry.hooks_json_path,
83
+ event: entry.event,
84
+ matcher: entry.matcher,
85
+ matcher_index: entry.matcher_index,
86
+ hook_index: entry.hook_index,
87
+ original_handler: entry.handler,
88
+ disabled_at: new Date().toISOString(),
89
+ });
90
+ await write_disabled_hooks(disabled);
91
+ // Remove from all hooks.json files (cache + marketplace source)
92
+ const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
93
+ for (const hooks_path of all_paths) {
94
+ await remove_hook_from_file(hooks_path, entry.event, entry.handler);
95
+ }
96
+ }
97
+ /**
98
+ * Add a hook handler back into a hooks.json file.
99
+ */
100
+ async function add_hook_to_file(hooks_path, event, matcher_pattern, handler) {
101
+ let hooks_data;
102
+ try {
103
+ const content = await readFile(hooks_path, 'utf-8');
104
+ hooks_data = JSON.parse(content);
105
+ }
106
+ catch {
107
+ hooks_data = { hooks: {} };
108
+ }
109
+ const hooks_obj = (hooks_data.hooks ||
110
+ (hooks_data.hooks = {}));
111
+ if (!hooks_obj[event])
112
+ hooks_obj[event] = [];
113
+ const matchers = hooks_obj[event];
114
+ let matcher = matchers.find((m) => (m.matcher || undefined) === matcher_pattern);
115
+ if (!matcher) {
116
+ matcher = { hooks: [] };
117
+ if (matcher_pattern)
118
+ matcher.matcher = matcher_pattern;
119
+ matchers.push(matcher);
120
+ }
121
+ // Only add if not already present (avoids duplicates)
122
+ const already_exists = matcher.hooks.some((h) => h.type === handler.type &&
123
+ h.command === handler.command &&
124
+ h.url === handler.url &&
125
+ h.prompt === handler.prompt);
126
+ if (already_exists)
127
+ return;
128
+ matcher.hooks.push(handler);
129
+ await writeFile(hooks_path, JSON.stringify(hooks_data, null, '\t'), 'utf-8');
130
+ }
131
+ /**
132
+ * Re-enable a previously disabled plugin hook.
133
+ * Restores to both cache and marketplace source hooks.json files.
134
+ */
135
+ export async function enable_plugin_hook(disabled_entry) {
136
+ const all_paths = get_all_hooks_paths(disabled_entry.plugin_key, disabled_entry.hooks_json_path);
137
+ for (const hooks_path of all_paths) {
138
+ await add_hook_to_file(hooks_path, disabled_entry.event, disabled_entry.matcher, disabled_entry.original_handler);
139
+ }
140
+ // Remove from disabled state
141
+ const disabled = await read_disabled_hooks();
142
+ const updated = disabled.filter((d) => !(d.plugin_key === disabled_entry.plugin_key &&
143
+ d.event === disabled_entry.event &&
144
+ d.disabled_at === disabled_entry.disabled_at));
145
+ await write_disabled_hooks(updated);
146
+ }
147
+ /**
148
+ * Check if any previously disabled hooks have been restored (e.g. by marketplace update).
149
+ * Returns entries that were re-added and need to be re-disabled.
150
+ */
151
+ export async function check_restored_hooks() {
152
+ const disabled = await read_disabled_hooks();
153
+ if (disabled.length === 0)
154
+ return [];
155
+ const restored = [];
156
+ for (const entry of disabled) {
157
+ const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
158
+ let found = false;
159
+ for (const hooks_path of all_paths) {
160
+ let hooks_data;
161
+ try {
162
+ const content = await readFile(hooks_path, 'utf-8');
163
+ hooks_data = JSON.parse(content);
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ const hooks_obj = (hooks_data.hooks || hooks_data);
169
+ const matchers = hooks_obj[entry.event];
170
+ if (!matchers)
171
+ continue;
172
+ for (const m of matchers) {
173
+ if ((m.matcher || undefined) !== entry.matcher)
174
+ continue;
175
+ const has_match = m.hooks?.some((h) => h.type === entry.original_handler.type &&
176
+ (h.command === entry.original_handler.command ||
177
+ h.url === entry.original_handler.url ||
178
+ h.prompt === entry.original_handler.prompt));
179
+ if (has_match) {
180
+ found = true;
181
+ break;
182
+ }
183
+ }
184
+ if (found)
185
+ break;
186
+ }
187
+ if (found)
188
+ restored.push(entry);
189
+ }
190
+ return restored;
191
+ }
192
+ /**
193
+ * Re-disable hooks that were restored by a marketplace update.
194
+ */
195
+ export async function redisable_restored_hooks(restored) {
196
+ let success = 0;
197
+ let failed = 0;
198
+ for (const entry of restored) {
199
+ try {
200
+ const all_paths = get_all_hooks_paths(entry.plugin_key, entry.hooks_json_path);
201
+ let any_removed = false;
202
+ for (const hooks_path of all_paths) {
203
+ const removed = await remove_hook_from_file(hooks_path, entry.event, entry.original_handler);
204
+ if (removed)
205
+ any_removed = true;
206
+ }
207
+ if (any_removed) {
208
+ success++;
209
+ }
210
+ else {
211
+ failed++;
212
+ }
213
+ }
214
+ catch {
215
+ failed++;
216
+ }
217
+ }
218
+ return { success, failed };
219
+ }
220
+ //# sourceMappingURL=hook-state.js.map
@@ -1,4 +1,5 @@
1
1
  import { access, readFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
2
3
  import { atomic_json_write } from '../utils/atomic-write.js';
3
4
  import { get_claude_settings_path } from '../utils/paths.js';
4
5
  export async function read_claude_settings() {
@@ -49,4 +50,194 @@ export function build_enabled_plugins(plugins) {
49
50
  }
50
51
  return result;
51
52
  }
53
+ async function read_settings_file(path) {
54
+ try {
55
+ await access(path);
56
+ const content = await readFile(path, 'utf-8');
57
+ return JSON.parse(content);
58
+ }
59
+ catch {
60
+ return {};
61
+ }
62
+ }
63
+ function get_settings_paths() {
64
+ const home = process.env.HOME || process.env.USERPROFILE || '';
65
+ return [
66
+ {
67
+ scope: 'user',
68
+ path: resolve(home, '.claude', 'settings.json'),
69
+ },
70
+ {
71
+ scope: 'project',
72
+ path: resolve(process.cwd(), '.claude', 'settings.json'),
73
+ },
74
+ {
75
+ scope: 'project-local',
76
+ path: resolve(process.cwd(), '.claude', 'settings.local.json'),
77
+ },
78
+ ];
79
+ }
80
+ /**
81
+ * Read all hooks across all scopes (settings + plugins), flattened for display.
82
+ */
83
+ export async function get_all_hooks() {
84
+ const entries = [];
85
+ // Settings-based hooks
86
+ for (const { scope, path } of get_settings_paths()) {
87
+ const data = await read_settings_file(path);
88
+ const hooks = data.hooks;
89
+ if (!hooks)
90
+ continue;
91
+ for (const [event, matchers] of Object.entries(hooks)) {
92
+ if (!Array.isArray(matchers))
93
+ continue;
94
+ for (let mi = 0; mi < matchers.length; mi++) {
95
+ const m = matchers[mi];
96
+ if (!m.hooks?.length)
97
+ continue;
98
+ for (let hi = 0; hi < m.hooks.length; hi++) {
99
+ entries.push({
100
+ event: event,
101
+ matcher: m.matcher,
102
+ handler: m.hooks[hi],
103
+ scope,
104
+ source: scope,
105
+ matcher_index: mi,
106
+ hook_index: hi,
107
+ });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ // Plugin-based hooks
113
+ const plugin_hooks = await get_all_plugin_hooks();
114
+ entries.push(...plugin_hooks);
115
+ return entries;
116
+ }
117
+ /**
118
+ * Scan all installed plugins for hooks.json and return flattened hook entries.
119
+ * Checks both cache and marketplace source paths since Claude Code reads from both.
120
+ */
121
+ export async function get_all_plugin_hooks() {
122
+ const { read_installed_plugins } = await import('./plugin-cache.js');
123
+ const { get_marketplaces_dir } = await import('../utils/paths.js');
124
+ const installed = await read_installed_plugins();
125
+ const entries = [];
126
+ const seen_hooks = new Set();
127
+ for (const [plugin_key, installs] of Object.entries(installed.plugins)) {
128
+ if (!installs?.length)
129
+ continue;
130
+ const install = installs[0];
131
+ const at_index = plugin_key.lastIndexOf('@');
132
+ const plugin_name = at_index > 0 ? plugin_key.substring(0, at_index) : plugin_key;
133
+ const marketplace_name = at_index > 0 ? plugin_key.substring(at_index + 1) : '';
134
+ // Collect all hooks.json paths for this plugin (cache + marketplace source)
135
+ const hooks_paths = [
136
+ join(install.installPath, 'hooks', 'hooks.json'),
137
+ ];
138
+ // Also check marketplace source path
139
+ if (marketplace_name) {
140
+ hooks_paths.push(join(get_marketplaces_dir(), marketplace_name, 'plugins', plugin_name, 'hooks', 'hooks.json'));
141
+ }
142
+ for (const hooks_path of hooks_paths) {
143
+ let hooks_data;
144
+ try {
145
+ const content = await readFile(hooks_path, 'utf-8');
146
+ hooks_data = JSON.parse(content);
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ const hooks = (hooks_data.hooks || hooks_data);
152
+ for (const [event, matchers] of Object.entries(hooks)) {
153
+ if (!Array.isArray(matchers))
154
+ continue;
155
+ for (let mi = 0; mi < matchers.length; mi++) {
156
+ const m = matchers[mi];
157
+ if (!m.hooks?.length)
158
+ continue;
159
+ for (let hi = 0; hi < m.hooks.length; hi++) {
160
+ // Deduplicate: same plugin + event + handler type + command
161
+ const h = m.hooks[hi];
162
+ const dedup_key = `${plugin_key}:${event}:${h.type}:${h.command || h.url || h.prompt}`;
163
+ if (seen_hooks.has(dedup_key))
164
+ continue;
165
+ seen_hooks.add(dedup_key);
166
+ entries.push({
167
+ event: event,
168
+ matcher: m.matcher,
169
+ handler: h,
170
+ scope: 'user',
171
+ source: 'plugin',
172
+ matcher_index: mi,
173
+ hook_index: hi,
174
+ plugin_key,
175
+ hooks_json_path: hooks_path,
176
+ });
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ return entries;
183
+ }
184
+ /**
185
+ * Remove a specific hook entry by scope/event/indices.
186
+ */
187
+ export async function remove_hook(entry) {
188
+ const scope_path = get_settings_paths().find((s) => s.scope === entry.scope);
189
+ if (!scope_path)
190
+ throw new Error(`Unknown scope: ${entry.scope}`);
191
+ await atomic_json_write(scope_path.path, (existing) => {
192
+ const hooks = existing.hooks;
193
+ if (!hooks)
194
+ return existing;
195
+ const matchers = hooks[entry.event];
196
+ if (!matchers?.[entry.matcher_index])
197
+ return existing;
198
+ const matcher = matchers[entry.matcher_index];
199
+ matcher.hooks.splice(entry.hook_index, 1);
200
+ // Clean up empty matchers
201
+ if (matcher.hooks.length === 0) {
202
+ matchers.splice(entry.matcher_index, 1);
203
+ }
204
+ // Clean up empty events
205
+ if (matchers.length === 0) {
206
+ delete hooks[entry.event];
207
+ }
208
+ // Clean up empty hooks object
209
+ if (Object.keys(hooks).length === 0) {
210
+ delete existing.hooks;
211
+ }
212
+ return existing;
213
+ });
214
+ }
215
+ /**
216
+ * Add a hook to a specific scope.
217
+ */
218
+ export async function add_hook(scope, event, matcher, handler) {
219
+ const scope_path = get_settings_paths().find((s) => s.scope === scope);
220
+ if (!scope_path)
221
+ throw new Error(`Unknown scope: ${scope}`);
222
+ await atomic_json_write(scope_path.path, (existing) => {
223
+ if (!existing.hooks)
224
+ existing.hooks = {};
225
+ const hooks = existing.hooks;
226
+ if (!hooks[event])
227
+ hooks[event] = [];
228
+ const matchers = hooks[event];
229
+ // Find existing matcher group or create new
230
+ const existing_matcher = matchers.find((m) => (m.matcher || undefined) === matcher);
231
+ if (existing_matcher) {
232
+ existing_matcher.hooks.push(handler);
233
+ }
234
+ else {
235
+ const new_matcher = { hooks: [handler] };
236
+ if (matcher)
237
+ new_matcher.matcher = matcher;
238
+ matchers.push(new_matcher);
239
+ }
240
+ return existing;
241
+ });
242
+ }
52
243
  //# sourceMappingURL=settings.js.map
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { backup_config } from './commands/backup.js';
5
5
  import { edit_config } from './commands/edit-config.js';
6
6
  import { edit_plugins } from './commands/edit-plugins.js';
7
7
  import { manage_cache } from './commands/manage-cache.js';
8
+ import { manage_hooks } from './commands/manage-hooks.js';
8
9
  import { manage_marketplace } from './commands/manage-marketplace.js';
9
10
  import { restore_config } from './commands/restore.js';
10
11
  import { write_claude_config } from './core/config.js';
@@ -164,6 +165,7 @@ async function main() {
164
165
  return;
165
166
  }
166
167
  intro('MCPick - MCP Server Configuration Manager');
168
+ log.info('CLI: mcpick <command> --help | Commands: list, add, clone, enable, disable, remove, get, dev, profile, plugins, hooks, backup, restore');
167
169
  while (true) {
168
170
  try {
169
171
  const action = await select({
@@ -184,6 +186,11 @@ async function main() {
184
186
  label: 'Manage marketplaces',
185
187
  hint: 'Add, remove, or update plugin marketplaces',
186
188
  },
189
+ {
190
+ value: 'manage-hooks',
191
+ label: 'Manage hooks',
192
+ hint: 'List, add, or remove event hooks',
193
+ },
187
194
  {
188
195
  value: 'manage-cache',
189
196
  label: 'Manage plugin cache',
@@ -235,6 +242,9 @@ async function main() {
235
242
  case 'manage-marketplace':
236
243
  await manage_marketplace();
237
244
  break;
245
+ case 'manage-hooks':
246
+ await manage_hooks();
247
+ break;
238
248
  case 'manage-cache':
239
249
  await manage_cache();
240
250
  break;
@@ -286,8 +296,10 @@ const SUBCOMMANDS = new Set([
286
296
  'remove',
287
297
  'add',
288
298
  'add-json',
299
+ 'clone',
289
300
  'get',
290
301
  'reset-project-choices',
302
+ 'hooks',
291
303
  'backup',
292
304
  'restore',
293
305
  'profile',
@@ -34,9 +34,11 @@ function build_add_command(server, scope) {
34
34
  const parts = ['claude', 'mcp', 'add'];
35
35
  // Server name
36
36
  parts.push(shell_escape(server.name));
37
- // Transport type
37
+ // Transport type (only specify if non-default)
38
38
  const transport = server.type || 'stdio';
39
- parts.push('--transport', transport);
39
+ if (transport !== 'stdio') {
40
+ parts.push('--transport', transport);
41
+ }
40
42
  // Scope
41
43
  parts.push('--scope', scope);
42
44
  // Handle different transport types
@@ -105,6 +105,9 @@ export function get_plugin_cache_dir() {
105
105
  export function get_marketplaces_dir() {
106
106
  return join(get_plugins_dir(), 'marketplaces');
107
107
  }
108
+ export function get_disabled_hooks_path() {
109
+ return join(get_mcpick_dir(), 'disabled-hooks.json');
110
+ }
108
111
  export function get_marketplace_manifest_path(name) {
109
112
  return join(get_marketplaces_dir(), name, '.claude-plugin', 'marketplace.json');
110
113
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Redact sensitive values (env, headers) from a server config.
3
+ * Shows key names but replaces values with "***".
4
+ */
5
+ export function redact_server_base(server) {
6
+ const redacted = { ...server };
7
+ if ('env' in redacted && redacted.env) {
8
+ redacted.env = redact_record(redacted.env);
9
+ }
10
+ if ('headers' in redacted && redacted.headers) {
11
+ redacted.headers = redact_record(redacted.headers);
12
+ }
13
+ return redacted;
14
+ }
15
+ export function redact_server(server) {
16
+ return {
17
+ ...redact_server_base(server),
18
+ name: server.name,
19
+ };
20
+ }
21
+ function redact_record(record) {
22
+ const redacted = {};
23
+ for (const key of Object.keys(record)) {
24
+ redacted[key] = '***';
25
+ }
26
+ return redacted;
27
+ }
28
+ //# sourceMappingURL=redact.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Dynamic MCP server and plugin configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",