claude-git-hooks 2.4.0 → 2.5.0

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.
@@ -0,0 +1,314 @@
1
+ /**
2
+ * File: interactive-ui.js
3
+ * Purpose: Interactive UI helpers for command-line prompts
4
+ *
5
+ * Features:
6
+ * - PR preview with colored output
7
+ * - Confirmation prompts
8
+ * - Field editing
9
+ * - Keyboard navigation
10
+ */
11
+
12
+ import readline from 'readline';
13
+ import logger from './logger.js';
14
+
15
+ /**
16
+ * Show PR preview in formatted box
17
+ * Why: Give user visual preview before creating PR
18
+ *
19
+ * @param {Object} prData - PR data to preview
20
+ * @param {string} prData.title - PR title
21
+ * @param {string} prData.body - PR description
22
+ * @param {string} prData.base - Base branch
23
+ * @param {string} prData.head - Head branch
24
+ * @param {Array<string>} prData.labels - Labels
25
+ * @param {Array<string>} prData.reviewers - Reviewers
26
+ */
27
+ export const showPRPreview = (prData) => {
28
+ const { title, body, base, head, labels = [], reviewers = [] } = prData;
29
+
30
+ console.log('');
31
+ console.log('┌───────────────────────────────────────────────────────────────┐');
32
+ console.log(' PR Preview');
33
+ console.log('├───────────────────────────────────────────────────────────────┤');
34
+ console.log(` Title: ${truncate(title, 55)}`);
35
+ console.log(` From: ${truncate(head, 55)}`);
36
+ console.log(` To: ${truncate(base, 55)}`);
37
+
38
+ if (labels.length > 0) {
39
+ console.log(` Labels: ${truncate(labels.join(', '), 53)}`);
40
+ }
41
+
42
+ if (reviewers.length > 0) {
43
+ console.log(` Reviewers: ${truncate(reviewers.join(', '), 50)}`);
44
+ }
45
+
46
+ console.log('├───────────────────────────────────────────────────────────────┤');
47
+ console.log(' Description:');
48
+
49
+ // Show first 5 lines of body
50
+ const bodyLines = body.split('\n').slice(0, 5);
51
+ bodyLines.forEach(line => {
52
+ console.log(` ${truncate(line, 61)}`);
53
+ });
54
+
55
+ if (body.split('\n').length > 5) {
56
+ console.log(' ... (truncated)');
57
+ }
58
+
59
+ console.log('└───────────────────────────────────────────────────────────────┘');
60
+ console.log('');
61
+ };
62
+
63
+ /**
64
+ * Truncate string to fit in box
65
+ * @private
66
+ */
67
+ const truncate = (str, maxLen) => {
68
+ const padded = str.padEnd(maxLen, ' ');
69
+ return padded.length > maxLen ? padded.substring(0, maxLen - 3) + '...' : padded;
70
+ };
71
+
72
+ /**
73
+ * Prompt user for confirmation with custom message
74
+ * Why: Get yes/no confirmation before destructive operations
75
+ *
76
+ * @param {string} message - Question to ask
77
+ * @param {boolean} defaultValue - Default if user presses Enter (default: true)
78
+ * @returns {Promise<boolean>} - True if confirmed, false otherwise
79
+ */
80
+ export const promptConfirmation = async (message, defaultValue = true) => {
81
+ const rl = readline.createInterface({
82
+ input: process.stdin,
83
+ output: process.stdout
84
+ });
85
+
86
+ return new Promise((resolve) => {
87
+ const defaultText = defaultValue ? 'Y/n' : 'y/N';
88
+ const promptMessage = `${message} (${defaultText}): `;
89
+
90
+ rl.question(promptMessage, (answer) => {
91
+ rl.close();
92
+
93
+ const trimmed = answer.trim().toLowerCase();
94
+
95
+ // If empty, use default
96
+ if (!trimmed) {
97
+ logger.debug('interactive-ui - promptConfirmation', 'Using default', { defaultValue });
98
+ resolve(defaultValue);
99
+ return;
100
+ }
101
+
102
+ // Check yes/no
103
+ const isYes = ['y', 'yes', 'si', 's'].includes(trimmed);
104
+ const isNo = ['n', 'no'].includes(trimmed);
105
+
106
+ if (isYes) {
107
+ logger.debug('interactive-ui - promptConfirmation', 'User confirmed');
108
+ resolve(true);
109
+ } else if (isNo) {
110
+ logger.debug('interactive-ui - promptConfirmation', 'User declined');
111
+ resolve(false);
112
+ } else {
113
+ // Invalid input, use default
114
+ logger.debug('interactive-ui - promptConfirmation', 'Invalid input, using default', {
115
+ answer: trimmed,
116
+ defaultValue
117
+ });
118
+ resolve(defaultValue);
119
+ }
120
+ });
121
+ });
122
+ };
123
+
124
+ /**
125
+ * Prompt user to edit a field value
126
+ * Why: Allow user to modify PR fields before creation
127
+ *
128
+ * @param {string} fieldName - Name of field (e.g., "Title", "Description")
129
+ * @param {string} currentValue - Current value
130
+ * @returns {Promise<string>} - Updated value (or current if user skips)
131
+ */
132
+ export const promptEditField = async (fieldName, currentValue) => {
133
+ const rl = readline.createInterface({
134
+ input: process.stdin,
135
+ output: process.stdout
136
+ });
137
+
138
+ return new Promise((resolve) => {
139
+ console.log(`\nCurrent ${fieldName}:`);
140
+ console.log(currentValue);
141
+ console.log('');
142
+
143
+ const promptMessage = `New ${fieldName} (press Enter to keep current): `;
144
+
145
+ rl.question(promptMessage, (answer) => {
146
+ rl.close();
147
+
148
+ const trimmed = answer.trim();
149
+
150
+ if (!trimmed) {
151
+ logger.debug('interactive-ui - promptEditField', 'Keeping current value', { fieldName });
152
+ resolve(currentValue);
153
+ } else {
154
+ logger.debug('interactive-ui - promptEditField', 'Updated value', { fieldName });
155
+ resolve(trimmed);
156
+ }
157
+ });
158
+ });
159
+ };
160
+
161
+ /**
162
+ * Show menu with options and get user choice
163
+ * Why: Present multiple options to user
164
+ *
165
+ * @param {string} message - Menu message
166
+ * @param {Array<{key: string, label: string}>} options - Menu options
167
+ * @param {string} defaultKey - Default option key if user presses Enter
168
+ * @returns {Promise<string>} - Selected option key
169
+ */
170
+ export const promptMenu = async (message, options, defaultKey = null) => {
171
+ const rl = readline.createInterface({
172
+ input: process.stdin,
173
+ output: process.stdout
174
+ });
175
+
176
+ return new Promise((resolve) => {
177
+ console.log('');
178
+ console.log(message);
179
+ console.log('');
180
+
181
+ // Display options
182
+ options.forEach(opt => {
183
+ const isDefault = opt.key === defaultKey;
184
+ const marker = isDefault ? '→' : ' ';
185
+ console.log(` ${marker} [${opt.key}] ${opt.label}`);
186
+ });
187
+
188
+ console.log('');
189
+
190
+ const defaultText = defaultKey ? ` (default: ${defaultKey})` : '';
191
+ const promptMessage = `Choose an option${defaultText}: `;
192
+
193
+ rl.question(promptMessage, (answer) => {
194
+ rl.close();
195
+
196
+ const trimmed = answer.trim().toLowerCase();
197
+
198
+ // If empty, use default
199
+ if (!trimmed && defaultKey) {
200
+ logger.debug('interactive-ui - promptMenu', 'Using default', { defaultKey });
201
+ resolve(defaultKey);
202
+ return;
203
+ }
204
+
205
+ // Check if valid option
206
+ const selectedOption = options.find(opt => opt.key.toLowerCase() === trimmed);
207
+
208
+ if (selectedOption) {
209
+ logger.debug('interactive-ui - promptMenu', 'Option selected', { key: selectedOption.key });
210
+ resolve(selectedOption.key);
211
+ } else {
212
+ // Invalid, use default or first option
213
+ const fallback = defaultKey || options[0]?.key;
214
+ logger.debug('interactive-ui - promptMenu', 'Invalid option, using fallback', {
215
+ answer: trimmed,
216
+ fallback
217
+ });
218
+ resolve(fallback);
219
+ }
220
+ });
221
+ });
222
+ };
223
+
224
+ /**
225
+ * Show loading spinner with message
226
+ * Why: Provide visual feedback during long operations
227
+ *
228
+ * @param {string} message - Loading message
229
+ * @returns {Function} - Stop function to clear spinner
230
+ */
231
+ export const showSpinner = (message) => {
232
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
233
+ let frameIndex = 0;
234
+ let isActive = true;
235
+
236
+ const interval = setInterval(() => {
237
+ if (!isActive) {
238
+ clearInterval(interval);
239
+ return;
240
+ }
241
+
242
+ const frame = frames[frameIndex];
243
+ process.stdout.write(`\r${frame} ${message}`);
244
+ frameIndex = (frameIndex + 1) % frames.length;
245
+ }, 80);
246
+
247
+ // Return stop function
248
+ return () => {
249
+ isActive = false;
250
+ clearInterval(interval);
251
+ process.stdout.write('\r'); // Clear line
252
+ };
253
+ };
254
+
255
+ /**
256
+ * Show success message with checkmark
257
+ * @param {string} message - Success message
258
+ */
259
+ export const showSuccess = (message) => {
260
+ console.log(`✅ ${message}`);
261
+ };
262
+
263
+ /**
264
+ * Show error message with X mark
265
+ * @param {string} message - Error message
266
+ */
267
+ export const showError = (message) => {
268
+ console.log(`❌ ${message}`);
269
+ };
270
+
271
+ /**
272
+ * Show warning message with warning sign
273
+ * @param {string} message - Warning message
274
+ */
275
+ export const showWarning = (message) => {
276
+ console.log(`⚠️ ${message}`);
277
+ };
278
+
279
+ /**
280
+ * Show info message with info icon
281
+ * @param {string} message - Info message
282
+ */
283
+ export const showInfo = (message) => {
284
+ console.log(`ℹ️ ${message}`);
285
+ };
286
+
287
+ /**
288
+ * Clear console screen
289
+ * Why: Clean slate for new UI sections
290
+ */
291
+ export const clearScreen = () => {
292
+ console.clear();
293
+ };
294
+
295
+ /**
296
+ * Wait for user to press Enter
297
+ * Why: Pause before continuing
298
+ *
299
+ * @param {string} message - Message to show (default: "Press Enter to continue")
300
+ * @returns {Promise<void>}
301
+ */
302
+ export const waitForEnter = async (message = 'Press Enter to continue') => {
303
+ const rl = readline.createInterface({
304
+ input: process.stdin,
305
+ output: process.stdout
306
+ });
307
+
308
+ return new Promise((resolve) => {
309
+ rl.question(`\n${message}... `, () => {
310
+ rl.close();
311
+ resolve();
312
+ });
313
+ });
314
+ };
@@ -0,0 +1,342 @@
1
+ /**
2
+ * File: mcp-setup.js
3
+ * Purpose: Automated GitHub MCP setup for Claude CLI
4
+ *
5
+ * Features:
6
+ * - Auto-detect existing configuration
7
+ * - Read token from Claude Desktop config
8
+ * - Interactive token prompt if needed
9
+ * - Configure Claude CLI MCP
10
+ * - Set environment variables
11
+ * - Verify configuration
12
+ */
13
+
14
+ import { execSync } from 'child_process';
15
+ import fs from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import logger from './logger.js';
19
+ import { promptConfirmation, promptEditField, showSuccess, showError, showInfo, showWarning } from './interactive-ui.js';
20
+ import { getClaudeCommand } from './claude-client.js';
21
+ import { approveGitHubMcpPermissions, executeMcpCommand } from './github-client.js';
22
+
23
+ /**
24
+ * Check if GitHub MCP is already configured in Claude CLI
25
+ * Why: Avoid duplicate configuration
26
+ *
27
+ * @returns {boolean} - True if configured
28
+ */
29
+ export const isGitHubMCPConfigured = () => {
30
+ try {
31
+ const { command, args } = getClaudeCommand();
32
+ const fullCommand = `${command} ${args.join(' ')} mcp list`;
33
+ const mcpList = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
34
+ return mcpList.toLowerCase().includes('github');
35
+ } catch (error) {
36
+ logger.debug('mcp-setup - isGitHubMCPConfigured', 'Failed to check MCP list', { error: error.message });
37
+ return false;
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Find GitHub token in Claude Desktop config
43
+ * Why: Reuse existing token instead of asking user
44
+ *
45
+ * @returns {Object|null} - { token, configPath } or null if not found
46
+ */
47
+ export const findGitHubTokenInDesktopConfig = () => {
48
+ const possibleConfigPaths = [
49
+ // Linux/macOS standard path
50
+ path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json'),
51
+ // Windows native path
52
+ path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'),
53
+ // WSL accessing Windows path - try multiple user names
54
+ '/mnt/c/Users/' + os.userInfo().username + '/AppData/Roaming/Claude/claude_desktop_config.json',
55
+ ];
56
+
57
+ // Add additional Windows paths by checking if we're in WSL
58
+ if (process.platform === 'linux' && fs.existsSync('/mnt/c')) {
59
+ // We're in WSL, try to find all Windows user directories
60
+ try {
61
+ const usersDir = '/mnt/c/Users';
62
+ if (fs.existsSync(usersDir)) {
63
+ const users = fs.readdirSync(usersDir);
64
+ for (const user of users) {
65
+ const configPath = `/mnt/c/Users/${user}/AppData/Roaming/Claude/claude_desktop_config.json`;
66
+ if (!possibleConfigPaths.includes(configPath)) {
67
+ possibleConfigPaths.push(configPath);
68
+ }
69
+ }
70
+ }
71
+ } catch (error) {
72
+ logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Failed to scan /mnt/c/Users', {
73
+ error: error.message
74
+ });
75
+ }
76
+ }
77
+
78
+ for (const configPath of possibleConfigPaths) {
79
+ try {
80
+ if (fs.existsSync(configPath)) {
81
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
82
+ const token = config.mcpServers?.github?.env?.GITHUB_PERSONAL_ACCESS_TOKEN;
83
+
84
+ if (token) {
85
+ logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Found token', { configPath });
86
+ return { token, configPath };
87
+ }
88
+ }
89
+ } catch (error) {
90
+ logger.debug('mcp-setup - findGitHubTokenInDesktopConfig', 'Failed to read config', {
91
+ configPath,
92
+ error: error.message
93
+ });
94
+ }
95
+ }
96
+
97
+ return null;
98
+ };
99
+
100
+ /**
101
+ * Add GitHub MCP to Claude CLI
102
+ * Why: Configure MCP server for PR creation
103
+ *
104
+ * @param {boolean} replace - Replace existing configuration
105
+ * @returns {Promise<boolean>} - True if successful
106
+ */
107
+ export const addGitHubMCP = async (replace = false) => {
108
+ try {
109
+ const { command, args } = getClaudeCommand();
110
+
111
+ // Remove existing if replacing
112
+ if (replace) {
113
+ try {
114
+ const removeCmd = `${command} ${args.join(' ')} mcp remove github`;
115
+ execSync(removeCmd, { stdio: 'ignore' });
116
+ logger.debug('mcp-setup - addGitHubMCP', 'Removed existing GitHub MCP');
117
+ } catch (error) {
118
+ // Ignore errors, may not have remove command
119
+ logger.debug('mcp-setup - addGitHubMCP', 'Failed to remove (may not exist)', { error: error.message });
120
+ }
121
+ }
122
+
123
+ // Add GitHub MCP
124
+ // Use -- to separate claude options from npx arguments
125
+ const mcpCommand = `${command} ${args.join(' ')} mcp add github npx -- -y @modelcontextprotocol/server-github`;
126
+ logger.debug('mcp-setup - addGitHubMCP', 'Running MCP add command', { mcpCommand });
127
+
128
+ execSync(mcpCommand, { stdio: 'inherit' });
129
+
130
+ logger.debug('mcp-setup - addGitHubMCP', 'Successfully added GitHub MCP');
131
+ return true;
132
+
133
+ } catch (error) {
134
+ logger.error('mcp-setup - addGitHubMCP', 'Failed to add GitHub MCP', error);
135
+ return false;
136
+ }
137
+ };
138
+
139
+ /**
140
+ * Set environment variable in shell RC files
141
+ * Why: Persist GitHub token across sessions
142
+ *
143
+ * @param {string} token - GitHub Personal Access Token
144
+ * @returns {boolean} - True if set in at least one file
145
+ */
146
+ export const setEnvironmentVariable = (token) => {
147
+ const envVarLine = `export GITHUB_PERSONAL_ACCESS_TOKEN="${token}"`;
148
+ const shellRcFiles = [
149
+ path.join(os.homedir(), '.bashrc'),
150
+ path.join(os.homedir(), '.bash_profile'),
151
+ path.join(os.homedir(), '.zshrc')
152
+ ];
153
+
154
+ let envVarSet = false;
155
+
156
+ for (const rcFile of shellRcFiles) {
157
+ try {
158
+ if (fs.existsSync(rcFile)) {
159
+ const content = fs.readFileSync(rcFile, 'utf8');
160
+
161
+ // Check if already present
162
+ if (content.includes('GITHUB_PERSONAL_ACCESS_TOKEN')) {
163
+ logger.debug('mcp-setup - setEnvironmentVariable', 'Already present', { rcFile });
164
+ showInfo(`Environment variable already in ${path.basename(rcFile)}`);
165
+ envVarSet = true;
166
+ continue;
167
+ }
168
+
169
+ // Add to file
170
+ fs.appendFileSync(rcFile, `\n# GitHub MCP token for Claude CLI\n${envVarLine}\n`);
171
+ showSuccess(`Added environment variable to ${path.basename(rcFile)}`);
172
+ logger.debug('mcp-setup - setEnvironmentVariable', 'Added to file', { rcFile });
173
+ envVarSet = true;
174
+ }
175
+ } catch (error) {
176
+ logger.debug('mcp-setup - setEnvironmentVariable', 'Failed to update file', {
177
+ rcFile,
178
+ error: error.message
179
+ });
180
+ }
181
+ }
182
+
183
+ if (!envVarSet) {
184
+ showWarning('Could not find shell RC file (.bashrc, .bash_profile, .zshrc)');
185
+ console.log('');
186
+ console.log('Please add this line to your shell configuration manually:');
187
+ console.log(` ${envVarLine}`);
188
+ }
189
+
190
+ // Set for current session
191
+ process.env.GITHUB_PERSONAL_ACCESS_TOKEN = token;
192
+
193
+ return envVarSet;
194
+ };
195
+
196
+ /**
197
+ * Setup GitHub MCP for Claude CLI
198
+ * Why: Automate MCP configuration to enable create-pr functionality
199
+ *
200
+ * Interactive setup process:
201
+ * 1. Check if GitHub MCP already configured
202
+ * 2. Try to read GitHub token from Claude Desktop config
203
+ * 3. If not found, prompt user for token
204
+ * 4. Run: claude mcp add github npx -y @modelcontextprotocol/server-github
205
+ * 5. Configure environment variable for token
206
+ * 6. Verify configuration
207
+ *
208
+ * @returns {Promise<void>}
209
+ */
210
+ export const setupGitHubMCP = async () => {
211
+ try {
212
+ console.log('');
213
+ showInfo('GitHub MCP Setup for Claude CLI');
214
+ console.log('');
215
+
216
+ // Step 1: Check if already configured
217
+ console.log('🔍 Checking current MCP configuration...');
218
+
219
+ let mcpList = '';
220
+ try {
221
+ const { command, args } = getClaudeCommand();
222
+ const fullCommand = `${command} ${args.join(' ')} mcp list`;
223
+ mcpList = execSync(fullCommand, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
224
+ } catch (error) {
225
+ showError('Failed to run "claude mcp list". Is Claude CLI installed?');
226
+ console.error('Make sure Claude CLI is installed: npm install -g @anthropic-ai/claude-cli');
227
+ console.error('Error:', error.message);
228
+ process.exit(1);
229
+ }
230
+
231
+ const hasGitHub = isGitHubMCPConfigured();
232
+ if (hasGitHub) {
233
+ showSuccess('GitHub MCP is already configured!');
234
+ console.log('');
235
+ console.log(mcpList);
236
+
237
+ const reconfigure = await promptConfirmation('Do you want to reconfigure it?', false);
238
+ if (!reconfigure) {
239
+ showInfo('Setup cancelled. Your existing configuration is unchanged.');
240
+ return;
241
+ }
242
+ }
243
+
244
+ // Step 2: Try to read token from Claude Desktop config
245
+ console.log('');
246
+ console.log('🔑 Looking for GitHub token...');
247
+
248
+ let githubToken = null;
249
+ const tokenInfo = findGitHubTokenInDesktopConfig();
250
+
251
+ if (tokenInfo) {
252
+ githubToken = tokenInfo.token;
253
+ showSuccess('Found GitHub token in Claude Desktop config');
254
+ console.log(` Location: ${tokenInfo.configPath}`);
255
+ }
256
+
257
+ // Step 3: If not found, prompt user
258
+ if (!githubToken) {
259
+ showWarning('No GitHub token found in Claude Desktop config');
260
+ console.log('');
261
+ console.log('You need a GitHub Personal Access Token with these permissions:');
262
+ console.log(' - repo (Full control of private repositories)');
263
+ console.log(' - read:org (Read org and team membership)');
264
+ console.log('');
265
+ console.log('Create one at: https://github.com/settings/tokens/new');
266
+ console.log('');
267
+
268
+ githubToken = await promptEditField('GitHub Personal Access Token', '');
269
+
270
+ if (!githubToken || githubToken.trim() === '') {
271
+ showError('GitHub token is required. Setup cancelled.');
272
+ process.exit(1);
273
+ }
274
+ }
275
+
276
+ // Step 4: Add GitHub MCP to Claude CLI
277
+ console.log('');
278
+ console.log('⚙️ Configuring GitHub MCP for Claude CLI...');
279
+
280
+ const added = await addGitHubMCP(hasGitHub);
281
+ if (!added) {
282
+ showError('Failed to add GitHub MCP');
283
+ process.exit(1);
284
+ }
285
+
286
+ showSuccess('GitHub MCP added to Claude CLI');
287
+
288
+ // Step 5: Configure environment variable
289
+ console.log('');
290
+ console.log('🔧 Setting up environment variable...');
291
+
292
+ setEnvironmentVariable(githubToken);
293
+
294
+ // Step 6: Approve MCP permissions
295
+ console.log('');
296
+ console.log('🔐 Approving MCP permissions...');
297
+
298
+ const permResult = await approveGitHubMcpPermissions();
299
+ if (permResult.success) {
300
+ showSuccess('MCP permissions approved');
301
+ } else {
302
+ showWarning('Some permissions may need manual approval');
303
+ console.log(' Run: claude mcp approve github --all');
304
+ }
305
+
306
+ // Step 7: Verify configuration
307
+ console.log('');
308
+ console.log('✅ Verifying configuration...');
309
+
310
+ try {
311
+ const { command, args } = getClaudeCommand();
312
+ const fullCommand = `${command} ${args.join(' ')} mcp list`;
313
+ const verifyList = execSync(fullCommand, { encoding: 'utf8' });
314
+ if (verifyList.toLowerCase().includes('github')) {
315
+ showSuccess('GitHub MCP is configured and ready!');
316
+ console.log('');
317
+ console.log(verifyList);
318
+ } else {
319
+ showWarning('Configuration completed but GitHub MCP not showing in list');
320
+ console.log('You may need to restart your terminal');
321
+ }
322
+ } catch (error) {
323
+ showWarning('Could not verify configuration');
324
+ logger.debug('mcp-setup - setupGitHubMCP', 'Verification failed', { error: error.message });
325
+ }
326
+
327
+ // Final instructions
328
+ console.log('');
329
+ showSuccess('Setup complete!');
330
+ console.log('');
331
+ console.log('Next steps:');
332
+ console.log(' 1. Restart your terminal (or run: source ~/.bashrc)');
333
+ console.log(' 2. Verify with: claude mcp list');
334
+ console.log(' 3. Try creating a PR: claude-hooks create-pr develop');
335
+ console.log('');
336
+
337
+ } catch (error) {
338
+ showError('Error during MCP setup: ' + error.message);
339
+ console.error(error);
340
+ throw error;
341
+ }
342
+ };