octocode-mcp 2.3.4 → 2.3.6

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.
Files changed (3) hide show
  1. package/README.md +3 -6
  2. package/build/index.js +2368 -1746
  3. package/package.json +2 -2
package/build/index.js CHANGED
@@ -8,83 +8,12 @@ import NodeCache from 'node-cache';
8
8
  import crypto from 'crypto';
9
9
  import z from 'zod';
10
10
 
11
- const PROMPT_SYSTEM_PROMPT = `Expert code research assistant for GitHub/NPM ecosystems (public/private). Use powerful semantic search for efficient code discovery.
12
-
13
- CORE TOOLS:
14
- GitHub: Code, repos, issues, PRs, commits - supports boolean logic (AND/OR/NOT) and exact phrases
15
- NPM: Package search (fuzzy only, no boolean) + metadata (repo URLs, exports, dependencies)
16
- API Status: Check connectivity + user's GitHub organizations for private access
17
-
18
- SEARCH STRATEGIES:
19
- GitHub Code/Issues/PRs/Commits:
20
- OR (broad): "useState OR setState" - finds alternatives
21
- AND (precise): "react AND hooks" - requires both terms
22
- NOT (filter): "auth NOT test" - excludes noise
23
- Quotes (exact): "useEffect cleanup" - literal phrases
24
- Combine with filters: language, path, owner for focus
25
-
26
- NPM Search:
27
- Space-separated keywords only: "react state management"
28
- No boolean operators supported
29
- Use npm_view_package for direct package metadata → GitHub repo URL
30
-
31
- OPTIMIZATION:
32
- Check user's GitHub orgs for private repo access
33
- npm_view_package gives repo URL instantly - avoid GitHub repo search
34
- Use targeted searches, avoid redundant tool calls
35
- Combine tools strategically: NPM → GitHub file content
36
- Discovery: comprehensive multi-tool approach
37
- Direct queries: quick targeted approach
38
-
39
- CLI Help using CLI (use when needed to check failures)
40
- gh <command> --help
41
- npm <command> --help
42
-
43
- Always provide code snippets and documentation references.`;
44
-
45
- function createResult(data, isError = false, suggestions) {
46
- const text = isError
47
- ? `${data}${''}`
48
- : JSON.stringify(data, null, 2);
49
- return {
50
- content: [{ type: 'text', text }],
51
- isError,
52
- };
53
- }
54
- // LEGACY SUPPORT - Remove these once all tools are updated
55
- function createSuccessResult$1(data) {
56
- return createResult(data, false);
57
- }
58
- function createErrorResult$1(message, error) {
59
- return createResult(`${message}: ${error.message}`, true);
60
- }
61
- // ENHANCED PARSING UTILITY
62
- function parseJsonResponse(responseText, fallback = null) {
63
- try {
64
- const data = JSON.parse(responseText);
65
- return { data, parsed: true };
66
- }
67
- catch {
68
- return { data: (fallback || responseText), parsed: false };
69
- }
70
- }
71
- /**
72
- * Determines if a string needs quoting for GitHub search
73
- */
74
- //TODO: move to util.ts
75
- function needsQuoting(str) {
76
- return (str.includes(' ') ||
77
- str.includes('"') ||
78
- str.includes('\t') ||
79
- str.includes('\n') ||
80
- str.includes('\r') ||
81
- /[<>(){}[\]\\|&;]/.test(str));
82
- }
83
-
84
11
  const VERSION = 'v1';
85
12
  const cache = new NodeCache({
86
13
  stdTTL: 86400, // 24 hour cache
87
14
  checkperiod: 3600, // Check for expired keys every 1 hour
15
+ maxKeys: 1000, // Limit cache to 1000 entries to prevent unbounded growth
16
+ deleteOnExpire: true, // Automatically delete expired keys
88
17
  });
89
18
  function generateCacheKey(prefix, params) {
90
19
  const paramString = JSON.stringify(params, Object.keys(params).sort());
@@ -105,6 +34,9 @@ async function withCache(cacheKey, operation) {
105
34
  }
106
35
  return result;
107
36
  }
37
+ function clearAllCache() {
38
+ cache.flushAll();
39
+ }
108
40
 
109
41
  const safeExecAsync = promisify(exec);
110
42
  // Allowed command prefixes - this prevents shell injection by restricting to safe commands
@@ -124,10 +56,9 @@ function createSuccessResult(data) {
124
56
  };
125
57
  }
126
58
  function createErrorResult(message, error) {
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
127
60
  return {
128
- content: [
129
- { type: 'text', text: `${message}: ${error.message}` },
130
- ],
61
+ content: [{ type: 'text', text: `${message}: ${errorMessage}` }],
131
62
  isError: true,
132
63
  };
133
64
  }
@@ -138,14 +69,16 @@ function isValidGhCommand(command) {
138
69
  return ALLOWED_GH_COMMANDS.includes(command);
139
70
  }
140
71
  /**
141
- * Get platform-specific shell configuration with PowerShell support
72
+ * Get platform-specific shell configuration with improved shell detection
142
73
  */
143
74
  function getShellConfig(preferredWindowsShell) {
144
75
  const isWindows = platform() === 'win32';
145
76
  if (!isWindows) {
77
+ // Use user's actual shell instead of hardcoded /bin/sh to avoid alias/function conflicts
78
+ const userShell = process.env.SHELL || '/bin/sh';
146
79
  return {
147
- shell: '/bin/sh',
148
- shellEnv: '/bin/sh',
80
+ shell: userShell,
81
+ shellEnv: userShell,
149
82
  type: 'unix',
150
83
  };
151
84
  }
@@ -165,15 +98,31 @@ function getShellConfig(preferredWindowsShell) {
165
98
  };
166
99
  }
167
100
  /**
168
- * Escape shell arguments to prevent shell injection and handle special characters
169
- * Cross-platform compatible escaping for Windows CMD, PowerShell, and Unix shells
101
+ * Escape shell arguments with improved GitHub CLI query handling
170
102
  */
171
- function escapeShellArg(arg, shellType) {
103
+ function escapeShellArg(arg, shellType, isGitHubQuery // Flag to indicate if this is the main GitHub search query argument
104
+ ) {
172
105
  // Auto-detect shell type if not provided
173
106
  if (!shellType) {
174
107
  const isWindows = platform() === 'win32';
175
108
  shellType = isWindows ? 'cmd' : 'unix';
176
109
  }
110
+ // Special handling for GitHub search queries
111
+ if (isGitHubQuery) {
112
+ // If the argument already contains quotes, preserve them
113
+ if (arg.includes('"')) {
114
+ // For Unix-like shells, wrap the entire argument in single quotes
115
+ if (shellType === 'unix') {
116
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
117
+ }
118
+ // For Windows CMD
119
+ if (shellType === 'cmd') {
120
+ return `"${arg.replace(/"/g, '""')}"`;
121
+ }
122
+ // For PowerShell
123
+ return `'${arg.replace(/'/g, "''")}'`;
124
+ }
125
+ }
177
126
  switch (shellType) {
178
127
  case 'powershell':
179
128
  return escapePowerShellArg(arg);
@@ -181,7 +130,7 @@ function escapeShellArg(arg, shellType) {
181
130
  return escapeWindowsCmdArg(arg);
182
131
  case 'unix':
183
132
  default:
184
- return escapeUnixShellArg(arg);
133
+ return escapeUnixShellArg(arg, isGitHubQuery);
185
134
  }
186
135
  }
187
136
  /**
@@ -208,11 +157,20 @@ function escapeWindowsCmdArg(arg) {
208
157
  return arg;
209
158
  }
210
159
  /**
211
- * Escape arguments for Unix shells (/bin/sh, bash, etc.)
160
+ * Escape arguments for Unix shells with special handling for GitHub CLI queries
212
161
  */
213
- function escapeUnixShellArg(arg) {
214
- // Unix shell escaping
215
- if (/[^\w\-._/:=@]/.test(arg)) {
162
+ function escapeUnixShellArg(arg, isGitHubQuery) {
163
+ // If it's a GitHub search query with special characters or spaces
164
+ if (isGitHubQuery && (arg.includes(' ') || /[:"']/g.test(arg))) {
165
+ // Preserve existing quotes if present
166
+ if (arg.includes('"')) {
167
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
168
+ }
169
+ // Add double quotes for terms that need them
170
+ return `"${arg}"`;
171
+ }
172
+ // Standard Unix shell escaping for other arguments
173
+ if (/[^a-zA-Z0-9\-_./=@:]/.test(arg)) {
216
174
  return `'${arg.replace(/'/g, "'\"'\"'")}'`;
217
175
  }
218
176
  return arg;
@@ -243,8 +201,7 @@ async function executeNpmCommand(command, args = [], options = {}) {
243
201
  return executeNpmCommand();
244
202
  }
245
203
  /**
246
- * Execute GitHub CLI commands safely by validating against allowed commands
247
- * Security: Only executes commands that start with "gh {ALLOWED_COMMAND}"
204
+ * Execute GitHub CLI commands safely with improved boolean query handling
248
205
  */
249
206
  async function executeGitHubCommand(command, args = [], options = {}) {
250
207
  // Security check: only allow registered commands
@@ -254,7 +211,22 @@ async function executeGitHubCommand(command, args = [], options = {}) {
254
211
  // Get shell configuration
255
212
  const shellConfig = getShellConfig(options.windowsShell);
256
213
  // Build command with validated prefix and properly escaped arguments
257
- const escapedArgs = args.map(arg => escapeShellArg(arg, shellConfig.type));
214
+ // For GitHub search commands, qualifiers like "language:typescript" should not be escaped
215
+ // Only the main query term (if it contains spaces) needs escaping
216
+ const escapedArgs = args.map((arg, index) => {
217
+ const isMainQueryArgument = command === 'search' && index === 1;
218
+ const isGitHubQualifier = command === 'search' &&
219
+ index > 1 &&
220
+ (arg.includes(':') || arg.startsWith('(')) &&
221
+ !arg.startsWith('--');
222
+ // Don't escape GitHub search qualifiers - they need to be passed as-is
223
+ // This includes qualifiers like "language:typescript", "user:microsoft", "org:microsoft"
224
+ // and complex expressions like "(user:microsoft OR org:microsoft)"
225
+ if (isGitHubQualifier) {
226
+ return arg;
227
+ }
228
+ return escapeShellArg(arg, shellConfig.type, isMainQueryArgument);
229
+ });
258
230
  const fullCommand = `gh ${command} ${escapedArgs.join(' ')}`;
259
231
  const executeGhCommand = () => executeCommand(fullCommand, 'github', options, shellConfig);
260
232
  if (options.cache) {
@@ -268,8 +240,7 @@ async function executeGitHubCommand(command, args = [], options = {}) {
268
240
  return executeGhCommand();
269
241
  }
270
242
  /**
271
- * Execute shell commands with timeout and error handling
272
- * Security: Should only be called with pre-validated command prefixes
243
+ * Execute shell commands with improved environment handling and error detection
273
244
  */
274
245
  async function executeCommand(fullCommand, type, options = {}, shellConfig) {
275
246
  try {
@@ -282,29 +253,50 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
282
253
  env: {
283
254
  ...process.env,
284
255
  ...options.env,
285
- // Ensure clean shell environment - cross-platform compatible
256
+ // More conservative shell environment handling
286
257
  SHELL: config.shellEnv,
287
258
  PATH: process.env.PATH,
259
+ // Only disable problematic shell features, not all of them
260
+ ...(config.type === 'unix' && {
261
+ // Only disable the most problematic shell configurations
262
+ BASH_ENV: '', // Prevent auto-sourcing of problematic configs
263
+ }),
288
264
  },
289
265
  encoding: 'utf-8',
290
- shell: config.shell, // Use platform-appropriate shell
266
+ shell: config.shell,
291
267
  };
292
268
  const { stdout, stderr } = await safeExecAsync(fullCommand, execOptions);
293
- // Handle different warning patterns for npm vs gh
269
+ // Improved error detection that ignores shell configuration conflicts
294
270
  const shouldTreatAsError = type === 'npm'
295
- ? stderr && !stderr.includes('npm WARN')
271
+ ? stderr &&
272
+ !stderr.includes('npm WARN') &&
273
+ !stderr.includes('npm notice')
296
274
  : stderr &&
297
275
  !stderr.includes('Warning:') &&
298
276
  !stderr.includes('notice:') &&
299
- !stderr.includes('No such file or directory') && // Ignore shell-related errors
277
+ // Ignore shell configuration conflicts - common in development environments
278
+ !stderr.includes('No such file or directory') &&
279
+ !stderr.includes('head: illegal option') &&
280
+ !stderr.includes('head: |: No such file or directory') &&
281
+ !stderr.includes('head: cat: No such file or directory') &&
282
+ !/^head:\s+/.test(stderr) && // Ignore all head command errors (shell conflicts)
283
+ !/^\s*head:\s+/.test(stderr) && // Ignore head errors with leading whitespace
300
284
  stderr.trim() !== '';
301
285
  if (shouldTreatAsError) {
302
286
  const errorType = type === 'npm' ? 'NPM command error' : 'GitHub CLI command error';
303
287
  return createErrorResult(errorType, new Error(stderr));
304
288
  }
289
+ // Try to parse stdout as JSON, fallback to string if not possible
290
+ let parsedResult = stdout;
291
+ try {
292
+ parsedResult = JSON.parse(stdout);
293
+ }
294
+ catch {
295
+ // Not JSON, keep as string
296
+ }
305
297
  return createSuccessResult({
306
298
  command: fullCommand,
307
- result: stdout,
299
+ result: parsedResult,
308
300
  timestamp: new Date().toISOString(),
309
301
  type,
310
302
  platform: platform(),
@@ -321,17 +313,303 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
321
313
  }
322
314
  }
323
315
 
324
- const TOOL_NAME$9 = 'api_status_check';
325
- const DESCRIPTION$9 = `Gets the list of user github organizations (in case the tool needs to use them in "owner" fields in github search) and checks users gh cli and npm cli login status.
326
- Use when user asks on specific implementaon in his organization (e.g. - I work at Wix, or search Wix code about X) or when cli is failing.`;
316
+ var LogLevel;
317
+ (function (LogLevel) {
318
+ LogLevel["ERROR"] = "ERROR";
319
+ LogLevel["WARN"] = "WARN";
320
+ LogLevel["INFO"] = "INFO";
321
+ LogLevel["DEBUG"] = "DEBUG";
322
+ })(LogLevel || (LogLevel = {}));
323
+ class Logger {
324
+ appName;
325
+ timestamp;
326
+ constructor(appName = 'octocode-mcp', timestamp = true) {
327
+ this.appName = appName;
328
+ this.timestamp = timestamp;
329
+ }
330
+ formatMessage(level, message, ...args) {
331
+ const timestamp = this.timestamp ? `[${new Date().toISOString()}] ` : '';
332
+ const prefix = `${timestamp}[${this.appName}] [${level}]`;
333
+ const formattedArgs = args.length > 0
334
+ ? ' ' +
335
+ args
336
+ .map(arg => {
337
+ if (arg instanceof Error) {
338
+ return `${arg.message}\n${arg.stack}`;
339
+ }
340
+ return typeof arg === 'object'
341
+ ? JSON.stringify(arg, null, 2)
342
+ : String(arg);
343
+ })
344
+ .join(' ')
345
+ : '';
346
+ return `${prefix} ${message}${formattedArgs}`;
347
+ }
348
+ error(message, ...args) {
349
+ // eslint-disable-next-line no-console
350
+ console.error(this.formatMessage(LogLevel.ERROR, message, ...args));
351
+ }
352
+ warn(message, ...args) {
353
+ // eslint-disable-next-line no-console
354
+ console.warn(this.formatMessage(LogLevel.WARN, message, ...args));
355
+ }
356
+ info(message, ...args) {
357
+ // eslint-disable-next-line no-console
358
+ console.log(this.formatMessage(LogLevel.INFO, message, ...args));
359
+ }
360
+ debug(message, ...args) {
361
+ if (process.env.DEBUG === 'true' ||
362
+ process.env.NODE_ENV === 'development') {
363
+ // eslint-disable-next-line no-console
364
+ console.log(this.formatMessage(LogLevel.DEBUG, message, ...args));
365
+ }
366
+ }
367
+ }
368
+ // Singleton instance
369
+ const logger = new Logger();
370
+
371
+ function createResult(options) {
372
+ const { data, error } = options;
373
+ if (error) {
374
+ const errorMessage = typeof error === 'string'
375
+ ? error
376
+ : error.message || 'Unknown error';
377
+ const errorResponse = errorMessage;
378
+ return {
379
+ content: [{ type: 'text', text: errorResponse }],
380
+ isError: true,
381
+ };
382
+ }
383
+ try {
384
+ return {
385
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
386
+ isError: false,
387
+ };
388
+ }
389
+ catch (jsonError) {
390
+ logger.error('JSON serialization failed:', jsonError);
391
+ return {
392
+ content: [
393
+ {
394
+ type: 'text',
395
+ text: `JSON serialization failed: ${jsonError instanceof Error ? jsonError.message : 'Unknown error'}`,
396
+ },
397
+ ],
398
+ isError: true,
399
+ };
400
+ }
401
+ }
402
+ /**
403
+ * Convert ISO timestamp to DDMMYYYY format
404
+ */
405
+ function toDDMMYYYY(timestamp) {
406
+ const date = new Date(timestamp);
407
+ const day = date.getDate().toString().padStart(2, '0');
408
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
409
+ const year = date.getFullYear();
410
+ return `${day}/${month}/${year}`;
411
+ }
412
+ /**
413
+ * Convert repository URL to owner/repo format
414
+ */
415
+ function simplifyRepoUrl(url) {
416
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
417
+ return match ? match[1] : url;
418
+ }
419
+ /**
420
+ * Extract first line of commit message
421
+ */
422
+ function getCommitTitle(message) {
423
+ return message.split('\n')[0].trim();
424
+ }
425
+ /**
426
+ * Convert bytes to human readable format
427
+ */
428
+ function humanizeBytes(bytes) {
429
+ if (bytes === 0)
430
+ return '0 B';
431
+ const k = 1024;
432
+ const sizes = ['B', 'KB', 'MB', 'GB'];
433
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
434
+ return `${Math.round(bytes / Math.pow(k, i))} ${sizes[i]}`;
435
+ }
436
+ /**
437
+ * Simplify GitHub URL to relative path
438
+ */
439
+ function simplifyGitHubUrl(url) {
440
+ const match = url.match(/github\.com\/[^/]+\/[^/]+\/(?:blob|commit)\/[^/]+\/(.+)$/);
441
+ return match ? match[1] : url;
442
+ }
443
+ /**
444
+ * Clean and optimize text match context
445
+ */
446
+ function optimizeTextMatch(fragment, maxLength = 100) {
447
+ // Remove excessive whitespace and normalize
448
+ const cleaned = fragment.replace(/\s+/g, ' ').trim();
449
+ if (cleaned.length <= maxLength) {
450
+ return cleaned;
451
+ }
452
+ // Try to cut at word boundary
453
+ const truncated = cleaned.substring(0, maxLength);
454
+ const lastSpace = truncated.lastIndexOf(' ');
455
+ if (lastSpace > maxLength * 0.7) {
456
+ return truncated.substring(0, lastSpace) + '…';
457
+ }
458
+ return truncated + '…';
459
+ }
460
+
461
+ /**
462
+ * Constants for error messages and suggestions across MCP tools
463
+ * Centralized management of error handling for better consistency
464
+ */
465
+ const ERROR_MESSAGES = {
466
+ // Authentication errors
467
+ AUTHENTICATION_REQUIRED: 'GitHub authentication required - run api_status_check tool',
468
+ // Rate limit errors
469
+ RATE_LIMIT_EXCEEDED: 'Rate limit exceeded - use specific filters (owner, language) or wait',
470
+ RATE_LIMIT_SIMPLE: 'Rate limit exceeded - wait or add filters',
471
+ // No results errors
472
+ NO_RESULTS_FOUND: 'No results found - try simpler query or different filters',
473
+ NO_REPOSITORIES_FOUND: 'No repositories found - try simpler query or different filters',
474
+ NO_COMMITS_FOUND: 'No commits found - try simpler query or different filters',
475
+ NO_ISSUES_FOUND: 'No issues found - try simpler query or different filters',
476
+ NO_PULL_REQUESTS_FOUND: 'No pull requests found - try simpler query or different filters',
477
+ NO_PACKAGES_FOUND: 'No packages found',
478
+ // Connection/network errors
479
+ SEARCH_FAILED: 'Search failed - check connection or simplify query',
480
+ REPOSITORY_SEARCH_FAILED: 'Repository search failed - check connection or query',
481
+ COMMIT_SEARCH_FAILED: 'Commit search failed',
482
+ ISSUE_SEARCH_FAILED: 'Issue search failed - check auth or simplify query',
483
+ PR_SEARCH_FAILED: 'PR search failed - check access and query syntax',
484
+ PACKAGE_SEARCH_FAILED: 'Package search failed - try different keywords',
485
+ // GitHub CLI errors
486
+ CLI_INVALID_RESPONSE: 'GitHub CLI invalid response - check "gh version" and update',
487
+ // Timeout errors
488
+ SEARCH_TIMEOUT: 'Search timed out - add filters (language, owner) or use specific terms',
489
+ // Query validation errors
490
+ QUERY_TOO_LONG: 'Query too long (max 256 chars) - simplify search terms',
491
+ QUERY_REQUIRED: 'Query required - provide search keywords',
492
+ EMPTY_QUERY: 'Empty query - try "useState", "authentication", or language:python',
493
+ QUERY_TOO_LONG_1000: 'Query too long (max 1000 chars) - use key terms like "error handling"',
494
+ REPO_OR_OWNER_NOT_FOUND: 'Repository/owner not found - check spelling, visibility, and permissions',
495
+ // Query syntax errors
496
+ INVALID_QUERY_SYNTAX: 'Invalid syntax - Boolean operators not supported, use quotes for phrases',
497
+ // Size/format validation errors
498
+ INVALID_SIZE_FORMAT: 'Invalid size format - use >N, <N, or N..M without quotes',
499
+ INVALID_SEARCH_SCOPE: 'Invalid scope - use "file" for content, "path" for filenames',
500
+ // API Status check errors
501
+ API_STATUS_CHECK_FAILED: 'API Status Check Failed',
502
+ };
503
+ const SUGGESTIONS = {
504
+ // Code search suggestions
505
+ CODE_SEARCH_NO_RESULTS: `No results found. Try this progression:
506
+ 1. Remove ALL filters and search with just one keyword
507
+ 2. Break into several queries with minimal query and filters
508
+ 3. Use api_status_check to verify access to private repos`,
509
+ // Repository search suggestions
510
+ REPO_SEARCH_PRIMARY_FILTER: 'Start with simple query (1-2 words), then add filters based on results',
511
+ // General search suggestions
512
+ SIMPLIFY_QUERY: `Try this search progression:
513
+ 1. Single keyword with NO filters
514
+ 2. Add ONE filter at a time
515
+ 3. Try synonyms and related terms
516
+ 4. Search broader categories`,
517
+ // Issue/PR search suggestions
518
+ PROVIDE_KEYWORDS: 'Start with simple keywords, then refine based on results',
519
+ PROVIDE_PR_KEYWORDS: 'Begin with basic terms, analyze results, then add filters',
520
+ // Package search suggestions
521
+ DIFFERENT_KEYWORDS: `Try multiple approaches:
522
+ 1. Single functional terms: "auth", "react"
523
+ 2. Break compound words: "authlib" → "auth"
524
+ 3. Search by use case: "user login" vs "authentication"
525
+ 4. Try category terms: "framework", "tool", "library"`,
526
+ };
527
+ // Helper function to get error message with context-specific suggestions
528
+ function getErrorWithSuggestion(options) {
529
+ const { baseError, suggestion } = options;
530
+ const errors = Array.isArray(baseError) ? baseError : [baseError];
531
+ const suggestions = Array.isArray(suggestion)
532
+ ? suggestion
533
+ : suggestion
534
+ ? [suggestion]
535
+ : [];
536
+ let result = errors.join('\n');
537
+ if (suggestions.length > 0) {
538
+ result += '\n\nSuggestion: ' + suggestions.join('\n');
539
+ }
540
+ return result;
541
+ }
542
+ // Common error handling patterns
543
+ function createAuthenticationError() {
544
+ return ERROR_MESSAGES.AUTHENTICATION_REQUIRED;
545
+ }
546
+ function createRateLimitError(detailed = true) {
547
+ return detailed
548
+ ? ERROR_MESSAGES.RATE_LIMIT_EXCEEDED
549
+ : ERROR_MESSAGES.RATE_LIMIT_SIMPLE;
550
+ }
551
+ function createNoResultsError(type = 'code') {
552
+ switch (type) {
553
+ case 'repositories':
554
+ return ERROR_MESSAGES.NO_REPOSITORIES_FOUND;
555
+ case 'commits':
556
+ return ERROR_MESSAGES.NO_COMMITS_FOUND;
557
+ case 'issues':
558
+ return ERROR_MESSAGES.NO_ISSUES_FOUND;
559
+ case 'pull_requests':
560
+ return ERROR_MESSAGES.NO_PULL_REQUESTS_FOUND;
561
+ case 'packages':
562
+ return ERROR_MESSAGES.NO_PACKAGES_FOUND;
563
+ default:
564
+ return ERROR_MESSAGES.NO_RESULTS_FOUND;
565
+ }
566
+ }
567
+ function createSearchFailedError(type = 'code') {
568
+ switch (type) {
569
+ case 'repositories':
570
+ return ERROR_MESSAGES.REPOSITORY_SEARCH_FAILED;
571
+ case 'commits':
572
+ return ERROR_MESSAGES.COMMIT_SEARCH_FAILED;
573
+ case 'issues':
574
+ return ERROR_MESSAGES.ISSUE_SEARCH_FAILED;
575
+ case 'pull_requests':
576
+ return ERROR_MESSAGES.PR_SEARCH_FAILED;
577
+ case 'packages':
578
+ return ERROR_MESSAGES.PACKAGE_SEARCH_FAILED;
579
+ default:
580
+ return ERROR_MESSAGES.SEARCH_FAILED;
581
+ }
582
+ }
583
+
584
+ const API_STATUS_CHECK_TOOL_NAME = 'apiStatusCheck';
585
+ const DESCRIPTION$9 = `Check GitHub and NPM authentication status. Returns connected status and GitHub organizations for accessing private repositories. No parameters required.`;
586
+ // Helper function to parse execution results with proper typing
587
+ function parseExecResult(result) {
588
+ if (!result.isError && result.content?.[0]?.text) {
589
+ try {
590
+ const textContent = result.content[0].text;
591
+ if (typeof textContent === 'string') {
592
+ const parsed = JSON.parse(textContent);
593
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
594
+ }
595
+ }
596
+ catch (e) {
597
+ return null;
598
+ }
599
+ }
600
+ return null;
601
+ }
327
602
  function registerApiStatusCheckTool(server) {
328
- server.tool(TOOL_NAME$9, DESCRIPTION$9, {}, {
329
- title: 'Check API Connections and Github Organizations',
603
+ server.registerTool(API_STATUS_CHECK_TOOL_NAME, {
330
604
  description: DESCRIPTION$9,
331
- readOnlyHint: true,
332
- destructiveHint: false,
333
- idempotentHint: true,
334
- openWorldHint: false,
605
+ inputSchema: {},
606
+ annotations: {
607
+ title: 'Check API Connections and Github Organizations',
608
+ readOnlyHint: true,
609
+ destructiveHint: false,
610
+ idempotentHint: true,
611
+ openWorldHint: false,
612
+ },
335
613
  }, async () => {
336
614
  try {
337
615
  let githubConnected = false;
@@ -342,32 +620,22 @@ function registerApiStatusCheckTool(server) {
342
620
  try {
343
621
  const authResult = await executeGitHubCommand('auth', ['status']);
344
622
  if (!authResult.isError) {
345
- let authData;
346
- try {
347
- authData = JSON.parse(authResult.content[0].text);
348
- }
349
- catch (parseError) {
350
- // JSON parsing error - this is unexpected, propagate it
351
- throw new Error(`GitHub auth response JSON parsing failed: ${parseError.message}`);
352
- }
353
- const isAuthenticated = authData.result?.includes('Logged in') ||
354
- authData.result?.includes('github.com');
623
+ const execResult = parseExecResult(authResult);
624
+ const isAuthenticated = typeof execResult?.result === 'string'
625
+ ? execResult.result.includes('Logged in') ||
626
+ execResult.result.includes('github.com')
627
+ : false;
355
628
  if (isAuthenticated) {
356
629
  githubConnected = true;
357
630
  // Get user organizations using direct GitHub CLI command
358
631
  try {
359
632
  const orgsResult = await executeGitHubCommand('org', ['list', '--limit=50'], { cache: false });
360
- if (!orgsResult.isError) {
361
- let execResult;
362
- try {
363
- execResult = JSON.parse(orgsResult.content[0].text);
364
- }
365
- catch (parseError) {
366
- // JSON parsing error for organizations - treat as no orgs available
367
- execResult = { result: '' };
368
- }
369
- const output = execResult.result;
370
- // Parse organizations into clean array
633
+ const orgsExecResult = parseExecResult(orgsResult);
634
+ const output = typeof orgsExecResult?.result === 'string'
635
+ ? orgsExecResult.result
636
+ : '';
637
+ // Parse organizations into clean array
638
+ if (typeof output === 'string') {
371
639
  organizations = output
372
640
  .split('\n')
373
641
  .map((org) => org.trim())
@@ -402,23 +670,12 @@ function registerApiStatusCheckTool(server) {
402
670
  if (!npmResult.isError) {
403
671
  npmConnected = true;
404
672
  // Get registry info
405
- try {
406
- const registryResult = await executeNpmCommand('config', ['get', 'registry'], { timeout: 3000 });
407
- if (!registryResult.isError) {
408
- let registryData;
409
- try {
410
- registryData = JSON.parse(registryResult.content[0].text);
411
- }
412
- catch (parseError) {
413
- // JSON parsing error for registry - use default
414
- registryData = { result: 'https://registry.npmjs.org/' };
415
- }
416
- registry = registryData.result.trim();
417
- }
418
- }
419
- catch {
420
- registry = 'https://registry.npmjs.org/'; // default fallback
421
- }
673
+ const registryResult = await executeNpmCommand('config', ['get', 'registry'], { timeout: 3000 });
674
+ const registryExecResult = parseExecResult(registryResult);
675
+ registry =
676
+ typeof registryExecResult?.result === 'string'
677
+ ? registryExecResult.result.trim()
678
+ : 'https://registry.npmjs.org/'; // default fallback
422
679
  }
423
680
  }
424
681
  catch (error) {
@@ -432,404 +689,277 @@ function registerApiStatusCheckTool(server) {
432
689
  npmConnected = false;
433
690
  }
434
691
  return createResult({
435
- github: {
436
- connected: githubConnected,
437
- organizations,
438
- },
439
- npm: {
440
- connected: npmConnected,
441
- registry,
692
+ data: {
693
+ login: {
694
+ github: {
695
+ connected: githubConnected,
696
+ user_organizations: organizations,
697
+ },
698
+ npm: {
699
+ connected: npmConnected,
700
+ registry: registry || 'https://registry.npmjs.org/',
701
+ },
702
+ hints: [
703
+ 'Use user organizations to search private repositories when requested - verify access by checking query and repository structure',
704
+ ],
705
+ },
442
706
  },
443
707
  });
444
708
  }
445
709
  catch (error) {
446
- return createResult('API status check failed - verify GitHub CLI and NPM are installed and accessible', true);
710
+ return createResult({
711
+ error: `${ERROR_MESSAGES.API_STATUS_CHECK_FAILED}\nError: ${error instanceof Error ? error.message : 'Unknown error'}\n\nThis usually indicates a system configuration issue. Please verify GitHub CLI and NPM are properly installed.`,
712
+ });
447
713
  }
448
714
  });
449
715
  }
450
716
 
451
- const TOOL_NAME$8 = 'github_search_code';
452
- const DESCRIPTION$8 = `Search code across GitHub repositories using strategic boolean operators and filters with "gh code search" command.
453
-
454
- SEARCH PATTERNS:
455
- OR (broad): "useState hook" → "useState OR hook" (auto-default for multi-word)
456
- AND (precise): "react AND hooks" (both terms required)
457
- NOT (filter): "auth NOT test" (exclude unwanted)
458
- Exact phrases: "useState hook" (quoted for literal match)
459
- Combine with filters: language, owner, path for laser focus
460
-
461
- RESTRICTIVENESS: OR (broadest) < AND < Exact Phrase (most precise)`;
462
- function registerGitHubSearchCodeTool(server) {
463
- server.tool(TOOL_NAME$8, DESCRIPTION$8, {
464
- query: z
465
- .string()
466
- .min(1)
467
- .describe('Search query with strategic boolean operators. SEARCH PATTERNS: OR (auto-default): "useState hook" → "useState OR hook" for BROADEST discovery. AND (explicit): "react AND hooks" requires BOTH terms for RESTRICTIVE intersection. EXACT PHRASE (escaped quotes): "useState hook" finds literal sequence for MOST PRECISE targeting. NOT (filtering): "auth NOT test" excludes unwanted results. USAGE GUIDE: Use OR for exploration/alternatives, AND for specific combinations, exact phrases for documentation/APIs, NOT for removing noise. RESTRICTIVENESS: OR < AND < Exact Phrase. No parentheses - simple boolean logic only.'),
468
- owner: z
469
- .union([z.string(), z.array(z.string())])
470
- .optional()
471
- .describe('Repository owner/organization filter. Examples: "microsoft", "google". Combines with other filters for targeted search. get from user orgamizations in case of private repositories search (e.g. for employees of organizations)'),
472
- repo: z
473
- .union([z.string(), z.array(z.string())])
474
- .optional()
475
- .describe('Specific repositories in "owner/repo" format. Examples: "facebook/react", "microsoft/vscode". Requires owner parameter.'),
476
- language: z
477
- .string()
478
- .optional()
479
- .describe('Programming language filter. Examples: "javascript", "python", "typescript", "go". Highly effective for targeted searches.'),
480
- extension: z
481
- .string()
482
- .optional()
483
- .describe('File extension filter without dot. Examples: "js", "ts", "py", "md", "json". Precise file type targeting.'),
484
- filename: z
485
- .string()
486
- .optional()
487
- .describe('Exact filename filter. Examples: "package.json", "Dockerfile", "README.md", "index.js". Perfect for config files.'),
488
- path: z
489
- .string()
490
- .optional()
491
- .describe('Directory path filter. Examples: "src/", "test/", "docs/", "components/". Focus search on specific directories.'),
492
- size: z
493
- .string()
494
- .optional()
495
- .describe('File size filter in KB with operators (e.g., ">100", "<50", "10..100").'),
496
- limit: z
497
- .number()
498
- .int()
499
- .min(1)
500
- .max(50)
501
- .optional()
502
- .default(30)
503
- .describe('Maximum results to return (1-50, default: 30).'),
504
- match: z
505
- .union([z.enum(['file', 'path']), z.array(z.enum(['file', 'path']))])
506
- .optional()
507
- .describe('Search scope: "file" searches code content, "path" searches filenames/paths. Use "path" to find files by name.'),
508
- visibility: z
509
- .enum(['public', 'private', 'internal'])
510
- .optional()
511
- .describe('Repository visibility filter: "public", "private", or "internal". Defaults to accessible repositories.'),
512
- }, {
513
- title: TOOL_NAME$8,
717
+ const GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME = 'githubViewRepoStructure';
718
+ const DESCRIPTION$8 = `Explore repository structure and navigate directories. Auto-detects branches and provides file/folder listings with size information. Parameters: owner (required - GitHub username/org), repo (required - repository name), branch (required), path (optional).`;
719
+ function registerViewRepositoryStructureTool(server) {
720
+ server.registerTool(GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME, {
514
721
  description: DESCRIPTION$8,
515
- readOnlyHint: true,
516
- destructiveHint: false,
517
- idempotentHint: true,
518
- openWorldHint: true,
722
+ inputSchema: {
723
+ owner: z
724
+ .string()
725
+ .min(1)
726
+ .max(100)
727
+ .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
728
+ .describe(`Repository owner/org name (e.g., 'microsoft', 'google', NOT 'microsoft/vscode')`),
729
+ repo: z
730
+ .string()
731
+ .min(1)
732
+ .max(100)
733
+ .regex(/^[a-zA-Z0-9._-]+$/, 'Invalid repository name format')
734
+ .describe('Repository name (case-sensitive)'),
735
+ branch: z
736
+ .string()
737
+ .min(1)
738
+ .max(255)
739
+ .regex(/^[^\s]+$/, 'Branch name cannot contain spaces')
740
+ .describe('Branch name. Falls back to default branch if not found'),
741
+ path: z
742
+ .string()
743
+ .optional()
744
+ .default('')
745
+ .refine(path => !path.includes('..'), 'Path traversal not allowed')
746
+ .refine(path => path.length <= 500, 'Path too long')
747
+ .describe('Directory path within repository. Leave empty for root.'),
748
+ },
749
+ annotations: {
750
+ title: 'GitHub Repository Explorer',
751
+ readOnlyHint: true,
752
+ destructiveHint: false,
753
+ idempotentHint: true,
754
+ openWorldHint: true,
755
+ },
519
756
  }, async (args) => {
520
757
  try {
521
- // Validate parameter combinations
522
- const validationError = validateSearchParameters(args);
523
- if (validationError) {
524
- return createResult(validationError, true);
525
- }
526
- const result = await searchGitHubCode(args);
527
- if (result.isError) {
528
- return result;
529
- }
530
- const execResult = JSON.parse(result.content[0].text);
531
- const codeResults = JSON.parse(execResult.result);
532
- // GitHub CLI returns a direct array, not an object with total_count and items
533
- const items = Array.isArray(codeResults) ? codeResults : [];
534
- return createSuccessResult$1({
535
- query: args.query,
536
- processed_query: parseSearchQuery(args.query, args),
537
- total_count: items.length,
538
- items: items,
539
- cli_command: execResult.command,
540
- debug_info: {
541
- has_complex_boolean_logic: hasComplexBooleanLogic(args.query),
542
- escaped_args: buildGitHubCliArgs(args),
543
- original_query: args.query,
544
- },
545
- });
758
+ const result = await viewRepositoryStructure(args);
759
+ return result;
546
760
  }
547
761
  catch (error) {
548
- const errorMessage = error.message || '';
549
- // Handle JSON parsing errors
550
- if (errorMessage.includes('JSON')) {
551
- return createErrorResult$1('GitHub CLI returned invalid response - check if GitHub CLI is up to date with "gh version" and try again', error);
552
- }
553
- return createErrorResult$1('GitHub code search failed - verify parameters and try with simpler query or specific filters (language, owner, path)', error);
762
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
763
+ return createResult({
764
+ error: `Failed to explore repository. ${errorMessage}. Verify repository exists and is accessible`,
765
+ });
554
766
  }
555
767
  });
556
768
  }
557
769
  /**
558
- * Enhanced query parser that handles exact strings, boolean operators, and filters
559
- */
560
- function parseSearchQuery(query, filters) {
561
- // Step 1: Handle quoted strings more intelligently
562
- // Convert escaped quotes to simple quotes to avoid shell escaping issues
563
- let processedQuery = query.replace(/\\"/g, '"');
564
- // Step 2: Preserve exact phrases (quoted strings)
565
- const exactPhrases = [];
566
- // Extract quoted strings and replace with placeholders
567
- const quotedMatches = processedQuery.match(/"[^"]+"/g) || [];
568
- quotedMatches.forEach((match, index) => {
569
- const placeholder = `__EXACT_PHRASE_${index}__`;
570
- exactPhrases.push(match);
571
- processedQuery = processedQuery.replace(match, placeholder);
572
- });
573
- // Step 3: Check complexity BEFORE adding auto-OR logic
574
- const originalHasComplexLogic = hasComplexBooleanLogic(processedQuery);
575
- // Step 4: Smart boolean logic - default to OR between terms if no explicit operators
576
- let searchQuery = processedQuery;
577
- // Check if query already has explicit boolean operators
578
- if (!originalHasComplexLogic) {
579
- // Split by whitespace and join with OR for better search results
580
- const terms = processedQuery
581
- .trim()
582
- .split(/\s+/)
583
- .filter(term => term.length > 0);
584
- if (terms.length > 1) {
585
- searchQuery = terms.join(' OR ');
586
- }
587
- }
588
- // Step 5: Handle filters differently based on ORIGINAL query complexity
589
- const githubFilters = [];
590
- // Always add path and visibility to query string (they don't have CLI equivalents)
591
- if (filters.path) {
592
- githubFilters.push(`path:${filters.path}`);
593
- }
594
- if (filters.visibility) {
595
- githubFilters.push(`visibility:${filters.visibility}`);
596
- }
597
- // For complex boolean queries, add ALL filters to query string to avoid CLI conflicts
598
- if (originalHasComplexLogic) {
599
- if (filters.language) {
600
- githubFilters.push(`language:${filters.language}`);
601
- }
602
- // For complex queries with both language and extension, prioritize language
603
- if (filters.extension && !filters.language) {
604
- githubFilters.push(`extension:${filters.extension}`);
605
- }
606
- if (filters.filename) {
607
- githubFilters.push(`filename:${filters.filename}`);
608
- }
609
- if (filters.size) {
610
- githubFilters.push(`size:${filters.size}`);
611
- }
612
- }
613
- // Step 6: Combine query with GitHub filters using proper spacing
614
- if (githubFilters.length > 0) {
615
- searchQuery = `${searchQuery} ${githubFilters.join(' ')}`;
616
- }
617
- // Step 7: Restore exact phrases
618
- exactPhrases.forEach((phrase, index) => {
619
- const placeholder = `__EXACT_PHRASE_${index}__`;
620
- searchQuery = searchQuery.replace(placeholder, phrase);
621
- });
622
- return searchQuery.trim();
623
- }
624
- /**
625
- * Check if query contains complex boolean logic that might conflict with CLI flags
626
- */
627
- function hasComplexBooleanLogic(query) {
628
- const booleanOperators = /\b(AND|OR|NOT)\b/i;
629
- return booleanOperators.test(query);
630
- }
631
- /**
632
- * Build command line arguments for GitHub CLI with improved parameter handling
770
+ * Views the structure of a GitHub repository at a specific path.
771
+ * Optimized for code analysis workflows with smart defaults and clear errors.
633
772
  */
634
- function buildGitHubCliArgs(params) {
635
- const args = ['code'];
636
- // Parse and add the main search query
637
- const searchQuery = parseSearchQuery(params.query, params);
638
- args.push(searchQuery);
639
- // Determine strategy based on ORIGINAL query complexity, not processed query
640
- const hasComplexLogic = hasComplexBooleanLogic(params.query);
641
- // For simple queries, use CLI flags for better performance and validation
642
- if (!hasComplexLogic) {
643
- if (params.language) {
644
- args.push(`--language=${params.language}`);
645
- }
646
- if (params.extension) {
647
- args.push(`--extension=${params.extension}`);
648
- }
649
- if (params.filename) {
650
- args.push(`--filename=${params.filename}`);
651
- }
652
- if (params.size) {
653
- args.push(`--size=${params.size}`);
654
- }
655
- }
656
- // For complex queries, filters are already in the query string (handled by parseSearchQuery)
657
- // Always add limit
658
- if (params.limit) {
659
- args.push(`--limit=${params.limit}`);
660
- }
661
- // Handle match parameter with conflict resolution
662
- if (params.match) {
663
- const matchValues = Array.isArray(params.match)
664
- ? params.match
665
- : [params.match];
666
- // Use the first match type when multiple are provided
667
- const matchValue = matchValues[0];
668
- args.push(`--match=${matchValue}`);
669
- }
670
- // Handle owner parameter - can be string or array
671
- if (params.owner && !params.repo) {
672
- const ownerValues = Array.isArray(params.owner)
673
- ? params.owner
674
- : [params.owner];
675
- ownerValues.forEach(owner => args.push(`--owner=${owner}`));
676
- }
677
- // Handle repository filters with improved validation
678
- if (params.owner && params.repo) {
679
- const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
680
- const repos = Array.isArray(params.repo) ? params.repo : [params.repo];
681
- // Create repo filters for each owner/repo combination
682
- owners.forEach(owner => {
683
- repos.forEach(repo => {
684
- // Handle both "owner/repo" format and just "repo" format
685
- if (repo.includes('/')) {
686
- args.push(`--repo=${repo}`);
773
+ async function viewRepositoryStructure(params) {
774
+ const cacheKey = generateCacheKey('gh-repo-structure', params);
775
+ return withCache(cacheKey, async () => {
776
+ const { owner, repo, branch, path = '' } = params;
777
+ try {
778
+ // Clean up path
779
+ const cleanPath = path.startsWith('/') ? path.substring(1) : path;
780
+ // Try the requested branch first, then fallback to main/master
781
+ const branchesToTry = await getSmartBranchFallback(owner, repo, branch);
782
+ let items = [];
783
+ let usedBranch = branch;
784
+ let lastError = null;
785
+ let attemptCount = 0;
786
+ const maxAttempts = 3; // Prevent infinite loops
787
+ for (const tryBranch of branchesToTry) {
788
+ if (attemptCount >= maxAttempts)
789
+ break;
790
+ attemptCount++;
791
+ try {
792
+ const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${tryBranch}`;
793
+ const result = await executeGitHubCommand('api', [apiPath], {
794
+ cache: false,
795
+ });
796
+ if (!result.isError) {
797
+ const execResult = JSON.parse(result.content[0].text);
798
+ const apiItems = execResult.result;
799
+ items = Array.isArray(apiItems) ? apiItems : [apiItems];
800
+ usedBranch = tryBranch;
801
+ break;
802
+ }
803
+ else {
804
+ lastError = new Error(result.content[0].text);
805
+ }
806
+ }
807
+ catch (error) {
808
+ lastError = error instanceof Error ? error : new Error(String(error));
809
+ // Try next branch
810
+ continue;
811
+ }
812
+ }
813
+ if (items.length === 0) {
814
+ // Use the most descriptive error message
815
+ const errorMsg = lastError?.message || 'Unknown error';
816
+ if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
817
+ if (path) {
818
+ return createResult({
819
+ error: `Path "${path}" not found. Verify the path or use github_search_code to find files`,
820
+ });
821
+ }
822
+ else {
823
+ return createResult({
824
+ error: `Repository not found: ${owner}/${repo}. Check spelling and case sensitivity`,
825
+ });
826
+ }
827
+ }
828
+ else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
829
+ return createResult({
830
+ error: `Access denied to ${owner}/${repo}. Repository may be private - use api_status_check`,
831
+ });
687
832
  }
688
833
  else {
689
- args.push(`--repo=${owner}/${repo}`);
834
+ return createResult({
835
+ error: `Failed to access ${owner}/${repo}. Check network connection`,
836
+ });
837
+ }
838
+ }
839
+ // Limit total items to 100 for efficiency
840
+ const limitedItems = items.slice(0, 100);
841
+ // Sort: directories first, then alphabetically
842
+ limitedItems.sort((a, b) => {
843
+ if (a.type !== b.type) {
844
+ return a.type === 'dir' ? -1 : 1;
690
845
  }
846
+ return a.name.localeCompare(b.name);
691
847
  });
692
- });
693
- }
694
- // JSON output with all available fields
695
- args.push('--json=repository,path,textMatches,sha,url');
696
- return args;
697
- }
698
- async function searchGitHubCode(params) {
699
- const cacheKey = generateCacheKey('gh-code', params);
700
- return withCache(cacheKey, async () => {
701
- try {
702
- const args = buildGitHubCliArgs(params);
703
- const result = await executeGitHubCommand('search', args, {
704
- cache: false,
848
+ // Create simplified, token-efficient structure
849
+ const files = limitedItems
850
+ .filter(item => item.type === 'file')
851
+ .map(item => ({
852
+ name: item.name,
853
+ size: item.size,
854
+ url: item.path, // Use path for fetching
855
+ }));
856
+ const folders = limitedItems
857
+ .filter(item => item.type === 'dir')
858
+ .map(item => ({
859
+ name: item.name,
860
+ url: item.path, // Use path for browsing
861
+ }));
862
+ return createResult({
863
+ data: {
864
+ repository: `${owner}/${repo}`,
865
+ branch: usedBranch,
866
+ path: cleanPath || '/',
867
+ githubBasePath: `https://api.github.com/repos/${owner}/${repo}/contents/`,
868
+ files: {
869
+ count: files.length,
870
+ files: files,
871
+ },
872
+ folders: {
873
+ count: folders.length,
874
+ folders: folders,
875
+ },
876
+ },
705
877
  });
706
- return result;
707
878
  }
708
879
  catch (error) {
709
- const errorMessage = error.message || '';
710
- // Parse specific GitHub CLI error types
711
- if (errorMessage.includes('authentication')) {
712
- return createErrorResult$1('GitHub CLI authentication required - run the api_status_check tool to verify authentication and available organizations', error);
713
- }
714
- if (errorMessage.includes('rate limit')) {
715
- return createErrorResult$1('GitHub API rate limit exceeded - wait a few minutes before searching again or use more specific filters to reduce results', error);
716
- }
717
- if (errorMessage.includes('validation failed') ||
718
- errorMessage.includes('Invalid query')) {
719
- return createErrorResult$1('Invalid search query syntax - check boolean operators (AND/OR/NOT), quotes for exact phrases, and filter formats. Try simplifying your query', error);
720
- }
721
- if (errorMessage.includes('repository not found') ||
722
- errorMessage.includes('owner not found')) {
723
- return createErrorResult$1('Repository or owner not found - run api_status_check tool to verify available organizations and access permissions', error);
724
- }
725
- if (errorMessage.includes('timeout')) {
726
- return createErrorResult$1('Search timeout - query too broad or complex. Try adding filters like language, owner, or path to narrow results', error);
727
- }
728
- // Generic fallback with helpful guidance
729
- return createErrorResult$1('GitHub code search failed - run api_status_check tool to verify authentication and permissions, or try simplifying your query', error);
880
+ return createResult({
881
+ error: `Failed to access repository. ${error}. Verify repository name and authentication`,
882
+ });
730
883
  }
731
884
  });
732
885
  }
733
886
  /**
734
- * Validate parameter combinations to prevent conflicts
887
+ * Smart branch detection with automatic fallback to common branch names.
735
888
  */
736
- function validateSearchParameters(params) {
737
- // Query validation
738
- if (!params.query.trim()) {
739
- return 'Empty search query - provide a search term like "useState", "function init", or "api AND endpoint"';
740
- }
741
- if (params.query.length > 1000) {
742
- return 'Search query too long - limit to 1000 characters. Try breaking into smaller, focused searches';
743
- }
744
- // Repository validation
745
- if (params.repo && !params.owner) {
746
- return 'Missing owner parameter - when searching specific repositories, format as owner/repo (e.g., "microsoft/vscode") or provide both owner and repo parameters';
747
- }
748
- // Invalid characters in query
749
- if (params.query.includes('\\') && !params.query.includes('\\"')) {
750
- return 'Invalid escape characters in query - use quotes for exact phrases: "exact phrase" instead of escaping';
751
- }
752
- // Boolean operator validation
753
- const invalidBooleans = params.query.match(/\b(and|or|not)\b/g);
754
- if (invalidBooleans) {
755
- return `Boolean operators must be uppercase - use ${invalidBooleans.map(op => op.toUpperCase()).join(', ')} instead of ${invalidBooleans.join(', ')}`;
756
- }
757
- // Unmatched quotes
758
- const quoteCount = (params.query.match(/"/g) || []).length;
759
- if (quoteCount % 2 !== 0) {
760
- return 'Unmatched quotes in query - ensure all quotes are properly paired for exact phrase matching';
889
+ async function getSmartBranchFallback(owner, repo, requestedBranch) {
890
+ const branches = [requestedBranch];
891
+ try {
892
+ // Try to get repository info to find default branch
893
+ const repoInfoResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
894
+ cache: false,
895
+ });
896
+ if (!repoInfoResult.isError) {
897
+ const execResult = JSON.parse(repoInfoResult.content[0].text);
898
+ const repoData = execResult.result;
899
+ const defaultBranch = repoData.default_branch;
900
+ if (defaultBranch && !branches.includes(defaultBranch)) {
901
+ branches.push(defaultBranch);
902
+ }
903
+ }
761
904
  }
762
- // Size parameter validation
763
- if (params.size && !/^[<>]=?\d+$|^\d+\.\.\d+$|^\d+$/.test(params.size)) {
764
- return 'Invalid size format - use ">100", "<50", "10..100", or "100" for file size filtering';
905
+ catch {
906
+ // If we can't get repo info, proceed with standard fallbacks
765
907
  }
766
- return null; // No validation errors
908
+ // Add common branch names if not already included
909
+ const commonBranches = ['main', 'master', 'develop', 'dev'];
910
+ commonBranches.forEach(branch => {
911
+ if (!branches.includes(branch)) {
912
+ branches.push(branch);
913
+ }
914
+ });
915
+ return branches;
767
916
  }
768
917
 
769
- const TOOL_NAME$7 = 'github_get_file_content';
770
- const DESCRIPTION$7 = `Read file content. This tool REQUIRES exact path verification from github_get_contents or package view exports. If fetching fails, re-check file existence with github_get_contents or branch name.`;
918
+ const GITHUB_GET_FILE_CONTENT_TOOL_NAME = 'githubGetFileContent';
919
+ const DESCRIPTION$7 = `Fetch file content from GitHub repositories. Use ${GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME} first to explore repository structure and find exact file paths. Supports automatic branch fallback (main/master) and handles files up to 300KB. Parameters: owner (required - GitHub username/org), repo (required - repository name), branch (required), filePath (required).`;
771
920
  function registerFetchGitHubFileContentTool(server) {
772
- server.tool(TOOL_NAME$7, DESCRIPTION$7, {
773
- owner: z
774
- .string()
775
- .min(1)
776
- .max(100)
777
- .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/)
778
- .describe(`Repository owner/organization (e.g., 'microsoft', 'facebook')`),
779
- repo: z
780
- .string()
781
- .min(1)
782
- .max(100)
783
- .regex(/^[a-zA-Z0-9._-]+$/)
784
- .describe(`Repository name (e.g., 'vscode', 'react'). Case-sensitive.`),
785
- branch: z
786
- .string()
787
- .min(1)
788
- .max(255)
789
- .regex(/^[^\s]+$/)
790
- .describe(`Branch name (e.g., 'main', 'master'). Auto-fallback to common branches if not found.`),
791
- filePath: z
792
- .string()
793
- .min(1)
794
- .describe(`File path from repository root (e.g., 'README.md', 'src/index.js'). Use github_get_contents to explore structure.`),
795
- }, {
796
- title: TOOL_NAME$7,
921
+ server.registerTool(GITHUB_GET_FILE_CONTENT_TOOL_NAME, {
797
922
  description: DESCRIPTION$7,
798
- readOnlyHint: true,
799
- destructiveHint: false,
800
- idempotentHint: true,
801
- openWorldHint: true,
923
+ inputSchema: {
924
+ owner: z
925
+ .string()
926
+ .min(1)
927
+ .max(100)
928
+ .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/)
929
+ .describe(`Repository owner/org name (e.g., 'microsoft', 'google', NOT 'microsoft/vscode')`),
930
+ repo: z
931
+ .string()
932
+ .min(1)
933
+ .max(100)
934
+ .regex(/^[a-zA-Z0-9._-]+$/)
935
+ .describe(`Repository name only (e.g., 'vscode', 'react', NOT 'microsoft/vscode')`),
936
+ branch: z
937
+ .string()
938
+ .min(1)
939
+ .max(255)
940
+ .regex(/^[^\s]+$/)
941
+ .describe(`Branch name. Falls back to main/master if not found`),
942
+ filePath: z
943
+ .string()
944
+ .min(1)
945
+ .describe(`Exact file path from repo root (e.g., src/index.js, README.md)`),
946
+ },
947
+ annotations: {
948
+ title: 'GitHub File Content - Direct Access',
949
+ readOnlyHint: true,
950
+ destructiveHint: false,
951
+ idempotentHint: true,
952
+ openWorldHint: true,
953
+ },
802
954
  }, async (args) => {
803
955
  try {
804
956
  const result = await fetchGitHubFileContent(args);
805
- if (result.content && result.content[0] && !result.isError) {
806
- const { data, parsed } = parseJsonResponse(result.content[0].text);
807
- if (parsed) {
808
- return createResult({
809
- file: `${args.owner}/${args.repo}/${args.filePath}`,
810
- content: data.content || data,
811
- metadata: {
812
- branch: args.branch,
813
- size: data.size,
814
- encoding: data.encoding,
815
- },
816
- });
817
- }
818
- else {
819
- // Return raw file content
820
- return createResult({
821
- file: `${args.owner}/${args.repo}/${args.filePath}`,
822
- content: data,
823
- metadata: {
824
- branch: args.branch,
825
- },
826
- });
827
- }
828
- }
829
957
  return result;
830
958
  }
831
959
  catch (error) {
832
- return createResult('File fetch failed - verify file path exists or try github_get_contents first', true);
960
+ return createResult({
961
+ error: 'Failed to fetch file. Verify path with github_get_contents first',
962
+ });
833
963
  }
834
964
  });
835
965
  }
@@ -862,20 +992,25 @@ async function fetchGitHubFileContent(params) {
862
992
  }
863
993
  // Handle common errors
864
994
  if (errorMsg.includes('404')) {
865
- return createErrorResult$1('File not found - verify path with github_get_contents first', new Error(filePath));
995
+ return createResult({
996
+ error: 'File not found. Use github_view_repo_structure to explore repository structure',
997
+ });
866
998
  }
867
999
  else if (errorMsg.includes('403')) {
868
- return createErrorResult$1('Access denied - repository may be private or require authentication', new Error(`${owner}/${repo}`));
1000
+ return createResult({
1001
+ error: 'Access denied. Repository may be private - use apiStatusCheck to verify',
1002
+ });
869
1003
  }
870
1004
  else if (errorMsg.includes('maxBuffer') ||
871
1005
  errorMsg.includes('stdout maxBuffer length exceeded')) {
872
- return createErrorResult$1(` File too large to fetch (buffer limit exceeded)\n\n` +
873
- `This usually happens with files >300KB. Consider:\n` +
874
- ` Use github_search_code to find specific patterns\n` +
875
- ` Browse directory structure with github_get_contents`, new Error(`Buffer overflow: ${filePath}`));
1006
+ return createResult({
1007
+ error: 'File too large (>300KB). Use githubSearchCode to search for patterns within the file',
1008
+ });
876
1009
  }
877
1010
  else {
878
- return createErrorResult$1('Fetch failed - check repository and file path', new Error(errorMsg));
1011
+ return createResult({
1012
+ error: 'Failed to fetch file. Verify repository name and file path',
1013
+ });
879
1014
  }
880
1015
  }
881
1016
  return await processFileContent(result, owner, repo, branch, filePath);
@@ -885,738 +1020,1045 @@ async function fetchGitHubFileContent(params) {
885
1020
  // Handle maxBuffer errors that escape the main try-catch
886
1021
  if (errorMessage.includes('maxBuffer') ||
887
1022
  errorMessage.includes('stdout maxBuffer length exceeded')) {
888
- return createErrorResult$1(` File too large to fetch (buffer limit exceeded)\n\n` +
889
- `This usually happens with files >300KB. Consider:\n` +
890
- ` Use github_search_code to find specific patterns instead of fetching the whole file\n` +
891
- ` Browse directory structure with github_get_contents`, error);
1023
+ return createResult({
1024
+ error: 'File too large (>300KB). Use github_search_code to search for patterns within the file',
1025
+ });
892
1026
  }
893
- return createErrorResult$1('Unexpected error during file fetch - check connection and permissions', error);
1027
+ return createResult({
1028
+ error: 'Unexpected error. Check network connection and try again',
1029
+ });
894
1030
  }
895
1031
  });
896
1032
  }
897
1033
  async function processFileContent(result, owner, repo, branch, filePath) {
898
1034
  // Extract the actual content from the exec result
899
1035
  const execResult = JSON.parse(result.content[0].text);
900
- const fileData = JSON.parse(execResult.result);
1036
+ const fileData = execResult.result;
901
1037
  // Check if it's a directory
902
1038
  if (Array.isArray(fileData)) {
903
- return createErrorResult$1('Path is directory - use github_get_contents to browse directory structure', new Error(filePath));
1039
+ return createResult({
1040
+ error: 'Path is a directory. Use github_view_repo_structure to list directory contents',
1041
+ });
904
1042
  }
905
- const fileSize = fileData.size || 0;
1043
+ const fileSize = typeof fileData.size === 'number' ? fileData.size : 0;
906
1044
  const MAX_FILE_SIZE = 300 * 1024; // 300KB limit for better performance and reliability
907
1045
  // Check file size with helpful message
908
1046
  if (fileSize > MAX_FILE_SIZE) {
909
1047
  const fileSizeKB = Math.round(fileSize / 1024);
910
1048
  const maxSizeKB = Math.round(MAX_FILE_SIZE / 1024);
911
- return createErrorResult$1(` File too large to display (${fileSizeKB}KB > ${maxSizeKB}KB limit)\n\n` +
912
- `For large files like this, consider:\n` +
913
- ` Use github_get_contents to browse directory structure\n` +
914
- ` Search specific code patterns with github_search_code`, new Error(`File: ${filePath} (${fileSizeKB}KB)`));
1049
+ return createResult({
1050
+ error: `File too large (${fileSizeKB}KB > ${maxSizeKB}KB). Use githubSearchCode to search within the file`,
1051
+ });
1052
+ }
1053
+ // Get and decode content with validation
1054
+ if (!fileData.content) {
1055
+ return createResult({
1056
+ error: 'File is empty - no content to display',
1057
+ });
915
1058
  }
916
- // Get and decode content
917
- const base64Content = fileData.content?.replace(/\s/g, ''); // Remove all whitespace
1059
+ const base64Content = fileData.content.replace(/\s/g, ''); // Remove all whitespace
918
1060
  if (!base64Content) {
919
- return createErrorResult$1('Empty file - file has no content to display', new Error(filePath));
1061
+ return createResult({
1062
+ error: 'File is empty - no content to display',
1063
+ });
920
1064
  }
921
1065
  let decodedContent;
922
1066
  try {
923
1067
  const buffer = Buffer.from(base64Content, 'base64');
924
1068
  // Simple binary check - look for null bytes
925
1069
  if (buffer.indexOf(0) !== -1) {
926
- return createErrorResult$1('Binary file detected - cannot display binary content as text', new Error(filePath));
1070
+ return createResult({
1071
+ error: 'Binary file detected. Cannot display as text - download directly from GitHub',
1072
+ });
927
1073
  }
928
1074
  decodedContent = buffer.toString('utf-8');
929
1075
  }
930
1076
  catch (decodeError) {
931
- return createErrorResult$1('Decode failed - file encoding not supported or corrupted', new Error(filePath));
932
- }
933
- // Return simplified response
934
- const response = {
935
- filePath,
936
- owner,
937
- repo,
938
- branch,
939
- content: decodedContent,
940
- metadata: {
941
- size: fileSize,
942
- lines: decodedContent.split('\n').length,
943
- encoding: 'utf-8',
1077
+ return createResult({
1078
+ error: 'Failed to decode file. Encoding may not be supported (expected UTF-8)',
1079
+ });
1080
+ }
1081
+ return createResult({
1082
+ data: {
1083
+ filePath,
1084
+ owner,
1085
+ repo,
1086
+ branch,
1087
+ content: decodedContent,
944
1088
  },
945
- };
946
- return createSuccessResult$1(response);
1089
+ });
947
1090
  }
948
1091
 
949
- const TOOL_NAME$6 = 'github_search_repositories';
950
- const DESCRIPTION$6 = `Search repositories by name/description. Start shallow and go broad: use topics for exploratory discovery (e.g., topic:["cli","typescript","api"]) to find ecosystem patterns.
951
- PRIMARY FILTERS work alone: owner, language, stars, topic, forks. SECONDARY FILTERS require a query or primary filter: license, created, archived, includeForks, updated, visibility, match.
952
- SMART REPOS SEARCH PATTERNS: Use topic:["cli","typescript"] for semantic discovery; stars:">100" for quality; owner:"microsoft" for organization repos. Query supports GitHub syntax: "language:Go OR language:Rust".
953
-
954
- EFFICIENCY NOTE: If you have a package name, use npm_view_package FIRST to get repositoryGitUrl - this tool becomes UNNECESSARY
955
-
956
- SMART INTEGRATION: When finding packages → npm_view_package + npm_package_search provide direct repo access
957
- AVOID: Searching for "react" repos when npm_view_package("react") gives you the exact repository instantly`;
958
- function registerSearchGitHubReposTool(server) {
959
- server.tool(TOOL_NAME$6, DESCRIPTION$6, {
960
- query: z
961
- .string()
962
- .optional()
963
- .describe('Search query with GitHub syntax: "cli shell" (AND), "vim plugin" (phrase), "language:Go OR language:Rust" (OR). Optional - can search with just primary filters.'),
964
- // PRIMARY FILTERS (can work alone)
965
- owner: z
966
- .string()
967
- .optional()
968
- .describe('Repository owner/organization (e.g., "microsoft", "facebook").'),
969
- language: z
970
- .string()
971
- .optional()
972
- .describe('Programming language (e.g., "javascript", "python", "go").'),
973
- stars: z
974
- .string()
975
- .optional()
976
- .describe('Stars count with ranges: "100", ">500", "<50", "10..100", ">=1000". Use >100 for quality projects.'),
977
- topic: z
978
- .array(z.string())
979
- .optional()
980
- .describe('Filter by topics (e.g., ["cli", "typescript", "api"]).'),
981
- forks: z.number().optional().describe('Exact forks count.'),
982
- numberOfTopics: z
983
- .number()
984
- .optional()
985
- .describe('Filter on number of topics.'),
986
- // SECONDARY FILTERS (require query or primary filter)
987
- license: z
988
- .array(z.string())
989
- .optional()
990
- .describe('License types (e.g., ["mit", "apache-2.0"]).'),
991
- match: z
992
- .enum(['name', 'description', 'readme'])
993
- .optional()
994
- .describe('Search scope: "name", "description", or "readme".'),
995
- visibility: z
996
- .enum(['public', 'private', 'internal'])
997
- .optional()
998
- .describe('Repository visibility filter.'),
999
- created: z
1000
- .string()
1001
- .optional()
1002
- .describe('Created date filter: ">2020-01-01", "<2023-12-31", "2022-01-01..2023-12-31".'),
1003
- updated: z
1004
- .string()
1005
- .optional()
1006
- .describe('Updated date filter (same format as created).'),
1007
- archived: z.boolean().optional().describe('Filter by archived state.'),
1008
- includeForks: z
1009
- .enum(['false', 'true', 'only'])
1010
- .optional()
1011
- .describe('Include forks: "false" (default), "true", or "only".'),
1012
- goodFirstIssues: z
1013
- .string()
1014
- .optional()
1015
- .describe('Filter by good first issues count (e.g., ">=10", ">5").'),
1016
- helpWantedIssues: z
1017
- .string()
1018
- .optional()
1019
- .describe('Filter by help wanted issues count (e.g., ">=5", ">10").'),
1020
- followers: z
1021
- .number()
1022
- .optional()
1023
- .describe('Filter by number of followers.'),
1024
- size: z
1025
- .string()
1026
- .optional()
1027
- .describe('Repository size filter in KB (e.g., ">100", "<50", "10..100").'),
1028
- // Sorting and limits
1029
- sort: z
1030
- .enum(['forks', 'help-wanted-issues', 'stars', 'updated', 'best-match'])
1031
- .optional()
1032
- .default('best-match')
1033
- .describe('Sort criteria (default: best-match)'),
1034
- order: z
1035
- .enum(['asc', 'desc'])
1036
- .optional()
1037
- .default('desc')
1038
- .describe('Result order (default: desc)'),
1039
- limit: z
1040
- .number()
1041
- .int()
1042
- .min(1)
1043
- .max(50)
1044
- .optional()
1045
- .default(25)
1046
- .describe('Maximum results (default: 25, max: 50)'),
1047
- }, {
1048
- title: TOOL_NAME$6,
1092
+ const GITHUB_SEARCH_CODE_TOOL_NAME = 'githubSearchCode';
1093
+ const DESCRIPTION$6 = `Search code across GitHub repositories. Start with simple 1-2 word queries, then refine with filters like language, owner, or filename. Supports exact phrase matching with quotes. Parameters: query (required), language (optional), owner (optional - GitHub username/org, NOT owner/repo), filename (optional), extension (optional), match (optional), size (optional), limit (optional).`;
1094
+ function registerGitHubSearchCodeTool(server) {
1095
+ server.registerTool(GITHUB_SEARCH_CODE_TOOL_NAME, {
1049
1096
  description: DESCRIPTION$6,
1050
- readOnlyHint: true,
1051
- destructiveHint: false,
1052
- idempotentHint: true,
1053
- openWorldHint: true,
1097
+ inputSchema: {
1098
+ query: z
1099
+ .string()
1100
+ .min(1)
1101
+ .describe('Search terms. START SIMPLE: Use 1-2 words first (e.g., "useState", "auth"). Add more terms only after seeing initial results.'),
1102
+ language: z
1103
+ .string()
1104
+ .optional()
1105
+ .describe('Language filter (javascript, python, etc). Use only when needed.'),
1106
+ owner: z
1107
+ .union([z.string(), z.array(z.string())])
1108
+ .optional()
1109
+ .describe('Repository owner/org (for organization-specific searches). Format: username or org-name only, NOT owner/repo. Use this to search within a specific organization.'),
1110
+ filename: z
1111
+ .string()
1112
+ .optional()
1113
+ .describe('Specific filename to search. Use for targeted searches.'),
1114
+ extension: z
1115
+ .string()
1116
+ .optional()
1117
+ .describe('File extension (.js, .py, etc). Alternative to language filter.'),
1118
+ match: z
1119
+ .union([z.enum(['file', 'path']), z.array(z.enum(['file', 'path']))])
1120
+ .optional()
1121
+ .describe('Search scope: "file" for content, "path" for filenames. Default: file content.'),
1122
+ size: z
1123
+ .string()
1124
+ .optional()
1125
+ .describe('File size in KB. Format: >10, <100, or 10..50'),
1126
+ limit: z
1127
+ .number()
1128
+ .int()
1129
+ .min(1)
1130
+ .max(100)
1131
+ .optional()
1132
+ .default(30)
1133
+ .describe('Results limit (1-100). Default: 30'),
1134
+ },
1135
+ annotations: {
1136
+ title: 'GitHub Code Search - Smart & Efficient',
1137
+ readOnlyHint: true,
1138
+ destructiveHint: false,
1139
+ idempotentHint: true,
1140
+ openWorldHint: true,
1141
+ },
1054
1142
  }, async (args) => {
1055
1143
  try {
1056
- // Updated validation logic for primary filters
1057
- const hasPrimaryFilter = args.query?.trim() ||
1058
- args.owner ||
1059
- args.language ||
1060
- args.topic ||
1061
- args.stars ||
1062
- args.forks;
1063
- if (!hasPrimaryFilter) {
1064
- return createResult('Requires query or primary filter (owner, language, stars, topic, forks)', true);
1144
+ // Validate parameter combinations
1145
+ const validationError = validateSearchParameters(args);
1146
+ if (validationError) {
1147
+ return createResult({ error: validationError });
1065
1148
  }
1066
- // Search repositories using GitHub CLI
1067
- const result = await searchGitHubRepos(args);
1068
- return result;
1069
- }
1070
- catch (error) {
1071
- return createResult('Repository search failed - check query syntax, filters, or try broader terms', true);
1072
- }
1073
- });
1074
- }
1075
- async function searchGitHubRepos(params) {
1076
- const cacheKey = generateCacheKey('gh-repos', params);
1077
- return withCache(cacheKey, async () => {
1078
- try {
1079
- const { command, args } = buildGitHubReposSearchCommand(params);
1080
- const result = await executeGitHubCommand(command, args, {
1081
- cache: false,
1082
- });
1149
+ const result = await searchGitHubCode(args);
1083
1150
  if (result.isError) {
1084
1151
  return result;
1085
1152
  }
1086
- // Extract the actual content from the exec result
1087
1153
  const execResult = JSON.parse(result.content[0].text);
1088
- const rawContent = execResult.result;
1089
- // Parse JSON results and provide structured analysis
1090
- let repositories = [];
1091
- const analysis = {
1092
- totalFound: 0,
1093
- languages: new Set(),
1094
- avgStars: 0,
1095
- recentlyUpdated: 0,
1096
- topStarred: [],
1097
- };
1098
- // Parse JSON response from GitHub CLI
1099
- repositories = JSON.parse(rawContent);
1100
- if (Array.isArray(repositories) && repositories.length > 0) {
1101
- analysis.totalFound = repositories.length;
1102
- // Analyze repository data
1103
- let totalStars = 0;
1104
- const now = new Date();
1105
- const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
1106
- repositories.forEach(repo => {
1107
- // Collect languages
1108
- if (repo.language) {
1109
- analysis.languages.add(repo.language);
1110
- }
1111
- // Calculate average stars (use correct field name)
1112
- if (repo.stargazersCount) {
1113
- totalStars += repo.stargazersCount;
1114
- }
1115
- // Count recently updated repositories (use correct field name)
1116
- if (repo.updatedAt) {
1117
- const updatedDate = new Date(repo.updatedAt);
1118
- if (updatedDate > thirtyDaysAgo) {
1119
- analysis.recentlyUpdated++;
1120
- }
1121
- }
1154
+ const codeResults = execResult.result;
1155
+ // GitHub CLI returns a direct array, not an object with total_count and items
1156
+ const items = Array.isArray(codeResults) ? codeResults : [];
1157
+ // Smart handling for no results - provide actionable suggestions
1158
+ if (items.length === 0) {
1159
+ // Provide progressive search guidance based on current parameters
1160
+ let specificSuggestion = SUGGESTIONS.CODE_SEARCH_NO_RESULTS;
1161
+ // If filters were used, suggest removing them first
1162
+ if (args.language || args.owner || args.filename || args.extension) {
1163
+ specificSuggestion = SUGGESTIONS.CODE_SEARCH_NO_RESULTS;
1164
+ }
1165
+ return createResult({
1166
+ error: getErrorWithSuggestion({
1167
+ baseError: createNoResultsError('code'),
1168
+ suggestion: specificSuggestion,
1169
+ }),
1122
1170
  });
1123
- analysis.avgStars =
1124
- repositories.length > 0
1125
- ? Math.round(totalStars / repositories.length)
1126
- : 0;
1127
- // Get all repositories with comprehensive data
1128
- analysis.topStarred = repositories.map(repo => ({
1129
- name: repo.fullName || repo.name,
1130
- stars: repo.stargazersCount || 0,
1131
- description: repo.description || 'No description',
1132
- language: repo.language || 'Unknown',
1133
- url: repo.url,
1134
- forks: repo.forksCount || 0,
1135
- isPrivate: repo.isPrivate || false,
1136
- isArchived: repo.isArchived || false,
1137
- isFork: repo.isFork || false,
1138
- topics: [], // GitHub CLI search repos doesn't provide topics in JSON output
1139
- license: repo.license?.name || null,
1140
- hasIssues: repo.hasIssues || false,
1141
- openIssuesCount: repo.openIssuesCount || 0,
1142
- createdAt: repo.createdAt,
1143
- updatedAt: repo.updatedAt,
1144
- visibility: repo.visibility || 'public',
1145
- owner: repo.owner?.login || repo.owner,
1146
- }));
1147
1171
  }
1148
- return createSuccessResult$1({
1149
- query: params.query,
1150
- total: analysis.totalFound,
1151
- ...(analysis.totalFound > 0
1152
- ? {
1153
- repositories: analysis.topStarred,
1154
- summary: {
1155
- languages: Array.from(analysis.languages).slice(0, 10),
1156
- avgStars: analysis.avgStars,
1157
- recentlyUpdated: analysis.recentlyUpdated,
1158
- },
1159
- }
1160
- : {
1161
- repositories: [],
1162
- }),
1163
- });
1172
+ // Transform to optimized format
1173
+ const optimizedResult = transformToOptimizedFormat$1(items);
1174
+ return createResult({ data: optimizedResult });
1164
1175
  }
1165
1176
  catch (error) {
1166
- return createErrorResult$1('GitHub repository search failed - verify connection or try simpler query', error);
1177
+ const errorMessage = error.message || '';
1178
+ return handleSearchError(errorMessage);
1167
1179
  }
1168
1180
  });
1169
1181
  }
1170
- function buildGitHubReposSearchCommand(params) {
1171
- // Build query following GitHub CLI patterns
1172
- const query = params.query?.trim() || '';
1173
- const args = ['repos'];
1174
- // Only add query if it exists and handle it properly
1175
- if (query) {
1176
- // For repository search, treat multi-word queries as a single quoted string
1177
- // This matches GitHub CLI expected behavior for repo searches
1178
- if (query.includes(' ')) {
1179
- args.push(query); // Let GitHub CLI handle the quoting
1182
+ /**
1183
+ * Handles various search errors and returns a formatted CallToolResult.
1184
+ */
1185
+ function handleSearchError(errorMessage) {
1186
+ // Common GitHub search errors with helpful suggestions
1187
+ if (errorMessage.includes('JSON')) {
1188
+ return createResult({
1189
+ error: ERROR_MESSAGES.CLI_INVALID_RESPONSE,
1190
+ });
1191
+ }
1192
+ if (errorMessage.includes('authentication')) {
1193
+ return createResult({
1194
+ error: createAuthenticationError(),
1195
+ });
1196
+ }
1197
+ if (errorMessage.includes('rate limit')) {
1198
+ return createResult({
1199
+ error: createRateLimitError(true),
1200
+ });
1201
+ }
1202
+ if (errorMessage.includes('timed out')) {
1203
+ return createResult({
1204
+ error: ERROR_MESSAGES.SEARCH_TIMEOUT,
1205
+ });
1206
+ }
1207
+ if (errorMessage.includes('validation failed') ||
1208
+ errorMessage.includes('Invalid query')) {
1209
+ return createResult({
1210
+ error: ERROR_MESSAGES.INVALID_QUERY_SYNTAX,
1211
+ });
1212
+ }
1213
+ if (errorMessage.includes('repository not found') ||
1214
+ errorMessage.includes('owner not found')) {
1215
+ return createResult({
1216
+ error: ERROR_MESSAGES.REPO_OR_OWNER_NOT_FOUND,
1217
+ });
1218
+ }
1219
+ // Generic fallback with guidance
1220
+ return createResult({
1221
+ error: `Code search failed: ${errorMessage}\n${SUGGESTIONS.SIMPLIFY_QUERY}`,
1222
+ });
1223
+ }
1224
+ /**
1225
+ * Transform GitHub CLI response to optimized format with enhanced metadata
1226
+ */
1227
+ function transformToOptimizedFormat$1(items) {
1228
+ // Extract repository info if single repo search
1229
+ const singleRepo = extractSingleRepository$1(items);
1230
+ const optimizedItems = items.map(item => ({
1231
+ path: item.path,
1232
+ matches: item.textMatches?.map(match => ({
1233
+ context: optimizeTextMatch(match.fragment, 120), // Increased context for better understanding
1234
+ positions: match.matches?.map(m => Array.isArray(m.indices) && m.indices.length >= 2
1235
+ ? [m.indices[0], m.indices[1]]
1236
+ : [0, 0]) || [],
1237
+ })) || [],
1238
+ url: singleRepo ? item.path : simplifyGitHubUrl(item.url),
1239
+ }));
1240
+ const result = {
1241
+ items: optimizedItems,
1242
+ total_count: items.length,
1243
+ };
1244
+ // Add repository info if single repo
1245
+ if (singleRepo) {
1246
+ result.repository = {
1247
+ name: singleRepo.nameWithOwner,
1248
+ url: simplifyRepoUrl(singleRepo.url),
1249
+ };
1250
+ }
1251
+ return result;
1252
+ }
1253
+ /**
1254
+ * Extract single repository if all results are from same repo
1255
+ */
1256
+ function extractSingleRepository$1(items) {
1257
+ if (items.length === 0)
1258
+ return null;
1259
+ const firstRepo = items[0].repository;
1260
+ const allSameRepo = items.every(item => item.repository.nameWithOwner === firstRepo.nameWithOwner);
1261
+ return allSameRepo ? firstRepo : null;
1262
+ }
1263
+ /**
1264
+ * Build command line arguments for GitHub CLI with improved parameter handling.
1265
+ * Ensures exact string search capability with proper quote and escape handling.
1266
+ *
1267
+ * This function is refactored to correctly distinguish between search qualifiers
1268
+ * (like `language` and `extension`), which will be passed as separate arguments
1269
+ * to `gh search`, and command-line flags (like `--size` and `--limit`).
1270
+ */
1271
+ function buildGitHubCliArgs(params) {
1272
+ const args = ['code'];
1273
+ // Extract qualifiers from the query
1274
+ const queryParts = params.query.trim().split(/\s+/);
1275
+ const searchTerms = [];
1276
+ const qualifiers = [];
1277
+ queryParts.forEach(part => {
1278
+ if (part.includes(':')) {
1279
+ qualifiers.push(part);
1180
1280
  }
1181
1281
  else {
1182
- args.push(query);
1183
- }
1184
- }
1185
- // Add JSON output with specific fields for structured data parsing
1186
- // Note: 'topics' field is not available in GitHub CLI search repos JSON output
1187
- args.push('--json', 'name,fullName,description,language,stargazersCount,forksCount,updatedAt,createdAt,url,owner,isPrivate,license,hasIssues,openIssuesCount,isArchived,isFork,visibility');
1188
- // PRIMARY FILTERS - Handle owner as single string (BaseSearchParams) or array
1189
- if (params.owner) {
1190
- const ownerValue = Array.isArray(params.owner)
1191
- ? params.owner.join(',')
1192
- : params.owner;
1193
- args.push(`--owner=${ownerValue}`);
1194
- }
1195
- if (params.language)
1196
- args.push(`--language=${params.language}`);
1197
- if (params.forks !== undefined)
1198
- args.push(`--forks=${params.forks}`);
1199
- if (params.topic && params.topic.length > 0)
1200
- args.push(`--topic=${params.topic.join(',')}`);
1201
- if (params.numberOfTopics !== undefined)
1202
- args.push(`--number-topics=${params.numberOfTopics}`);
1203
- // Only add stars filter if it's a valid numeric value or range
1204
- if (params.stars !== undefined &&
1205
- params.stars !== '*' &&
1206
- params.stars.trim() !== '') {
1207
- // Validate that stars parameter contains valid numeric patterns
1208
- const starsValue = params.stars.trim();
1209
- const isValidStars = /^(\d+|>\d+|<\d+|\d+\.\.\d+|>=\d+|<=\d+)$/.test(starsValue);
1210
- if (isValidStars) {
1211
- // Don't add quotes around the stars value - GitHub CLI handles this internally
1212
- args.push(`--stars=${starsValue}`);
1213
- }
1214
- }
1215
- // SECONDARY FILTERS - only add if we have primary filters
1216
- if (params.archived !== undefined)
1217
- args.push(`--archived=${params.archived}`);
1218
- if (params.created)
1219
- args.push(`--created=${params.created}`);
1220
- if (params.includeForks)
1221
- args.push(`--include-forks=${params.includeForks}`);
1222
- if (params.license && params.license.length > 0)
1223
- args.push(`--license=${params.license.join(',')}`);
1224
- if (params.match)
1225
- args.push(`--match=${params.match}`);
1226
- if (params.updated)
1227
- args.push(`--updated=${params.updated}`);
1228
- if (params.visibility)
1229
- args.push(`--visibility=${params.visibility}`);
1230
- if (params.goodFirstIssues)
1231
- args.push(`--good-first-issues=${params.goodFirstIssues}`);
1232
- if (params.helpWantedIssues)
1233
- args.push(`--help-wanted-issues=${params.helpWantedIssues}`);
1234
- if (params.followers !== undefined)
1235
- args.push(`--followers=${params.followers}`);
1236
- if (params.size)
1237
- args.push(`--size=${params.size}`);
1238
- // SORTING AND LIMITS
1239
- if (params.limit)
1282
+ searchTerms.push(part);
1283
+ }
1284
+ });
1285
+ // Add search terms if any
1286
+ if (searchTerms.length > 0) {
1287
+ args.push(searchTerms.join(' '));
1288
+ }
1289
+ // Add extracted qualifiers
1290
+ qualifiers.forEach(qualifier => {
1291
+ args.push(qualifier);
1292
+ });
1293
+ // Add explicit parameters as qualifiers
1294
+ if (params.language && !params.query.includes('language:')) {
1295
+ args.push(`language:${params.language}`);
1296
+ }
1297
+ if (params.owner &&
1298
+ !params.query.includes('org:') &&
1299
+ !params.query.includes('user:')) {
1300
+ const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
1301
+ owners.forEach(owner => args.push(`org:${owner}`));
1302
+ }
1303
+ if (params.filename && !params.query.includes('filename:')) {
1304
+ args.push(`filename:${params.filename}`);
1305
+ }
1306
+ if (params.extension && !params.query.includes('extension:')) {
1307
+ args.push(`extension:${params.extension}`);
1308
+ }
1309
+ if (params.size && !params.query.includes('size:')) {
1310
+ args.push(`size:${params.size}`);
1311
+ }
1312
+ // Handle match parameter
1313
+ if (params.match) {
1314
+ const matches = Array.isArray(params.match) ? params.match : [params.match];
1315
+ args.push(`in:${matches.join(',')}`);
1316
+ }
1317
+ // Add limit
1318
+ if (params.limit) {
1240
1319
  args.push(`--limit=${params.limit}`);
1241
- if (params.order)
1242
- args.push(`--order=${params.order}`);
1243
- // Use best-match as default, only specify sort if different from default
1244
- const sortBy = params.sort || 'best-match';
1245
- if (sortBy !== 'best-match') {
1246
- args.push(`--sort=${sortBy}`);
1247
1320
  }
1248
- return { command: 'search', args };
1321
+ // Add JSON output format
1322
+ args.push('--json=repository,path,textMatches,sha,url');
1323
+ return args;
1249
1324
  }
1250
-
1251
- const TOOL_NAME$5 = 'github_search_commits';
1252
- const DESCRIPTION$5 = `Search commit history with powerful boolean logic and exact phrase matching. Use advanced GitHub search syntax including AND/OR operators for precise commit discovery. Understand code evolution patterns, track bug fixes, and analyze development workflows over time. Filter by author, date ranges, commit content, and repository metadata with surgical precision.`;
1253
- function registerSearchGitHubCommitsTool(server) {
1254
- server.tool(TOOL_NAME$5, DESCRIPTION$5, {
1255
- query: z
1256
- .string()
1257
- .optional()
1258
- .describe('Search query with POWERFUL boolean logic and exact phrase matching. BOOLEAN OPERATORS: "fix AND bug" (both required), "fix OR update" (either term), "readme typo" (implicit AND). EXACT PHRASES: "initial commit" (precise phrase matching). ADVANCED SYNTAX: "author:john OR committer:jane" (user qualifiers), "-- -author:botuser" (exclusions). STRENGTH: Surgical precision for commit discovery across millions of repositories. Optional - can search with just filters.'),
1259
- // Basic filters
1260
- owner: z
1261
- .string()
1262
- .optional()
1263
- .describe('Repository owner/organization. Leave empty for global search.'),
1264
- repo: z
1265
- .string()
1266
- .optional()
1267
- .describe('Repository name. Do exploratory search without repo filter first'),
1268
- // Author filters
1269
- author: z.string().optional().describe('Filter by commit author'),
1270
- authorDate: z
1271
- .string()
1272
- .optional()
1273
- .describe('Filter by authored date (format: >2020-01-01, <2023-12-31)'),
1274
- authorEmail: z.string().optional().describe('Filter by author email'),
1275
- authorName: z.string().optional().describe('Filter by author name'),
1276
- // Committer filters
1277
- committer: z.string().optional().describe('Filter by committer'),
1278
- committerDate: z
1279
- .string()
1280
- .optional()
1281
- .describe('Filter by committed date (format: >2020-01-01, <2023-12-31)'),
1282
- committerEmail: z
1283
- .string()
1284
- .optional()
1285
- .describe('Filter by committer email'),
1286
- committerName: z.string().optional().describe('Filter by committer name'),
1287
- // Hash filters
1288
- hash: z.string().optional().describe('Filter by commit hash'),
1289
- parent: z.string().optional().describe('Filter by parent hash'),
1290
- tree: z.string().optional().describe('Filter by tree hash'),
1291
- // Boolean filters
1292
- merge: z.boolean().optional().describe('Filter merge commits'),
1293
- visibility: z
1294
- .enum(['public', 'private', 'internal'])
1295
- .optional()
1296
- .describe('Filter by repository visibility'),
1297
- // Sorting and limits
1298
- sort: z
1299
- .enum(['author-date', 'committer-date', 'best-match'])
1300
- .optional()
1301
- .default('best-match')
1302
- .describe('Sort criteria (default: best-match)'),
1303
- order: z
1304
- .enum(['asc', 'desc'])
1305
- .optional()
1306
- .default('desc')
1307
- .describe('Order (default: desc)'),
1308
- limit: z
1309
- .number()
1310
- .int()
1311
- .min(1)
1312
- .max(50)
1313
- .optional()
1314
- .default(25)
1315
- .describe('Maximum results (default: 25, max: 50)'),
1316
- }, {
1317
- title: TOOL_NAME$5,
1318
- description: DESCRIPTION$5,
1319
- readOnlyHint: true,
1320
- destructiveHint: false,
1321
- idempotentHint: true,
1322
- openWorldHint: true,
1323
- }, async (args) => {
1325
+ async function searchGitHubCode(params) {
1326
+ const cacheKey = generateCacheKey('gh-code', params);
1327
+ return withCache(cacheKey, async () => {
1324
1328
  try {
1325
- // Query is optional - can search with just filters
1326
- if (!args.query?.trim() &&
1327
- !args.owner &&
1328
- !args.author &&
1329
- !args.committer &&
1330
- !args.repo) {
1331
- return createResult('Either query or at least one filter is required', true);
1332
- }
1333
- const result = await searchGitHubCommits(args);
1329
+ const args = buildGitHubCliArgs(params);
1330
+ const result = await executeGitHubCommand('search', args, {
1331
+ cache: false,
1332
+ });
1334
1333
  return result;
1335
1334
  }
1336
1335
  catch (error) {
1337
- return createResult('Commit search failed - check query syntax, filters, or repository access', true);
1336
+ const errorMessage = error.message || '';
1337
+ return handleSearchError(errorMessage); // Delegating error handling
1338
1338
  }
1339
1339
  });
1340
1340
  }
1341
- async function searchGitHubCommits(params) {
1342
- const cacheKey = generateCacheKey('gh-commits', params);
1343
- return withCache(cacheKey, async () => {
1341
+ /**
1342
+ * Enhanced validation with helpful suggestions
1343
+ */
1344
+ function validateSearchParameters(params) {
1345
+ // Query validation
1346
+ if (!params.query.trim()) {
1347
+ return ERROR_MESSAGES.EMPTY_QUERY;
1348
+ }
1349
+ if (params.query.length > 1000) {
1350
+ return ERROR_MESSAGES.QUERY_TOO_LONG_1000;
1351
+ }
1352
+ // Repository validation - ensure owner is provided correctly
1353
+ if (params.owner &&
1354
+ typeof params.owner === 'string' &&
1355
+ params.owner.includes('/')) {
1356
+ return 'Owner parameter should contain only the username/org name, not owner/repo format. For repository-specific searches, use org: or user: qualifiers in the query.';
1357
+ }
1358
+ if (Array.isArray(params.owner)) {
1359
+ const hasSlashFormat = params.owner.some(owner => owner.includes('/'));
1360
+ if (hasSlashFormat) {
1361
+ return 'Owner parameter should contain only usernames/org names, not owner/repo format. For repository-specific searches, use repo: qualifier in the query.';
1362
+ }
1363
+ }
1364
+ // Add validation for file size limit
1365
+ if (params.size) {
1366
+ if (!/^([<>]\d+|\d+\.\.\d+)$/.test(params.size)) {
1367
+ return ERROR_MESSAGES.INVALID_SIZE_FORMAT;
1368
+ }
1369
+ }
1370
+ // Validate search scope
1371
+ if (params.match) {
1372
+ const validScopes = ['file', 'path'];
1373
+ const scopes = Array.isArray(params.match) ? params.match : [params.match];
1374
+ if (!scopes.every(scope => validScopes.includes(scope))) {
1375
+ return ERROR_MESSAGES.INVALID_SEARCH_SCOPE;
1376
+ }
1377
+ }
1378
+ // Note about repository limitations (This is a note, not a hard error)
1379
+ // This return statement was returning null before, so it shouldn't be an issue
1380
+ // if (params.repo || params.owner) {
1381
+ // return null; // Return warning about repository limitations
1382
+ // }
1383
+ return null; // No validation errors
1384
+ }
1385
+
1386
+ const GITHUB_SEARCH_COMMITS_TOOL_NAME = 'githubSearchCommits';
1387
+ const DESCRIPTION$5 = `Search commit history across GitHub repositories. Supports filtering by repository, author, date ranges, and commit attributes. Parameters: query (optional), owner (optional - GitHub username/org, NOT owner/repo), repo (optional - repository name, use with owner for specific repo), author (optional), authorName (optional), authorEmail (optional), committer (optional), committerName (optional), committerEmail (optional), authorDate (optional), committerDate (optional), hash (optional), parent (optional), tree (optional), merge (optional), visibility (optional), limit (optional), sort (optional), order (optional).`;
1388
+ function registerGitHubSearchCommitsTool(server) {
1389
+ server.registerTool(GITHUB_SEARCH_COMMITS_TOOL_NAME, {
1390
+ description: DESCRIPTION$5,
1391
+ inputSchema: {
1392
+ query: z
1393
+ .string()
1394
+ .optional()
1395
+ .describe('Search terms. Start simple: "bug fix", "refactor". Use quotes for exact phrases.'),
1396
+ // Repository filters
1397
+ owner: z
1398
+ .string()
1399
+ .optional()
1400
+ .describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
1401
+ repo: z
1402
+ .string()
1403
+ .optional()
1404
+ .describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
1405
+ // Author filters
1406
+ author: z
1407
+ .string()
1408
+ .optional()
1409
+ .describe('GitHub username of commit author'),
1410
+ authorName: z
1411
+ .string()
1412
+ .optional()
1413
+ .describe('Full name of commit author'),
1414
+ authorEmail: z.string().optional().describe('Email of commit author'),
1415
+ // Committer filters
1416
+ committer: z
1417
+ .string()
1418
+ .optional()
1419
+ .describe('GitHub username of committer'),
1420
+ committerName: z.string().optional().describe('Full name of committer'),
1421
+ committerEmail: z.string().optional().describe('Email of committer'),
1422
+ // Date filters
1423
+ authorDate: z
1424
+ .string()
1425
+ .optional()
1426
+ .describe('When authored. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
1427
+ committerDate: z
1428
+ .string()
1429
+ .optional()
1430
+ .describe('When committed. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
1431
+ // Hash filters
1432
+ hash: z.string().optional().describe('Commit SHA (full or partial)'),
1433
+ parent: z.string().optional().describe('Parent commit SHA'),
1434
+ tree: z.string().optional().describe('Tree SHA'),
1435
+ // State filters
1436
+ merge: z
1437
+ .boolean()
1438
+ .optional()
1439
+ .describe('Only merge commits (true) or exclude them (false)'),
1440
+ // Visibility
1441
+ visibility: z
1442
+ .enum(['public', 'private', 'internal'])
1443
+ .optional()
1444
+ .describe('Repository visibility filter'),
1445
+ // Pagination and sorting
1446
+ limit: z
1447
+ .number()
1448
+ .int()
1449
+ .min(1)
1450
+ .max(50)
1451
+ .optional()
1452
+ .default(25)
1453
+ .describe('Results limit (1-50). Default: 25'),
1454
+ sort: z
1455
+ .enum(['author-date', 'committer-date'])
1456
+ .optional()
1457
+ .describe('Sort by date. Default: best match'),
1458
+ order: z
1459
+ .enum(['asc', 'desc'])
1460
+ .optional()
1461
+ .default('desc')
1462
+ .describe('Sort order. Default: desc'),
1463
+ },
1464
+ annotations: {
1465
+ title: 'GitHub Commit Search - Smart History Analysis',
1466
+ readOnlyHint: true,
1467
+ destructiveHint: false,
1468
+ idempotentHint: true,
1469
+ openWorldHint: true,
1470
+ },
1471
+ }, async (args) => {
1344
1472
  try {
1345
- const { command, args } = buildGitHubCommitsSearchCommand(params);
1346
- const result = await executeGitHubCommand(command, args, {
1347
- cache: false,
1348
- });
1473
+ const result = await searchGitHubCommits(args);
1349
1474
  if (result.isError) {
1350
1475
  return result;
1351
1476
  }
1352
- // Extract the actual content from the exec result
1353
1477
  const execResult = JSON.parse(result.content[0].text);
1354
- const rawContent = execResult.result;
1355
- // Parse JSON results and provide structured analysis
1356
- let commits = [];
1357
- const analysis = {
1358
- totalFound: 0,
1359
- recentCommits: 0,
1360
- topAuthors: [],
1361
- repositories: new Set(),
1362
- };
1363
- // Parse JSON response from GitHub CLI
1364
- commits = JSON.parse(rawContent);
1365
- if (Array.isArray(commits) && commits.length > 0) {
1366
- analysis.totalFound = commits.length;
1367
- // Simple analysis
1368
- const now = new Date();
1369
- const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
1370
- const authorCounts = {};
1371
- commits.forEach(commit => {
1372
- // Count recent commits
1373
- const commitDate = commit.commit?.author?.date || commit.commit?.committer?.date;
1374
- if (commitDate && new Date(commitDate) > thirtyDaysAgo) {
1375
- analysis.recentCommits++;
1376
- }
1377
- // Count authors
1378
- const authorName = commit.commit?.author?.name || commit.author?.login || 'Unknown';
1379
- authorCounts[authorName] = (authorCounts[authorName] || 0) + 1;
1380
- // Track repositories
1381
- if (commit.repository?.fullName) {
1382
- analysis.repositories.add(commit.repository.fullName);
1383
- }
1384
- });
1385
- // Get top authors
1386
- analysis.topAuthors = Object.entries(authorCounts)
1387
- .sort(([, a], [, b]) => b - a)
1388
- .slice(0, 5)
1389
- .map(([name, count]) => ({ name, commits: count }));
1390
- // Format commits for output
1391
- const formattedCommits = commits.map(commit => ({
1392
- sha: commit.sha,
1393
- message: commit.commit?.message || '',
1394
- author: {
1395
- name: commit.commit?.author?.name,
1396
- email: commit.commit?.author?.email,
1397
- date: commit.commit?.author?.date,
1398
- login: commit.author?.login,
1399
- },
1400
- committer: {
1401
- name: commit.commit?.committer?.name,
1402
- email: commit.commit?.committer?.email,
1403
- date: commit.commit?.committer?.date,
1404
- login: commit.committer?.login,
1405
- },
1406
- repository: commit.repository
1407
- ? {
1408
- name: commit.repository.name,
1409
- fullName: commit.repository.fullName,
1410
- url: commit.repository.url,
1411
- description: commit.repository.description,
1412
- }
1413
- : null,
1414
- url: commit.url,
1415
- parents: commit.parents?.map((p) => p.sha) || [],
1416
- }));
1417
- return createSuccessResult$1({
1418
- query: params.query,
1419
- total: analysis.totalFound,
1420
- commits: formattedCommits,
1421
- summary: {
1422
- recentCommits: analysis.recentCommits,
1423
- topAuthors: analysis.topAuthors,
1424
- repositories: Array.from(analysis.repositories),
1425
- },
1478
+ const commits = execResult.result;
1479
+ // GitHub CLI returns a direct array
1480
+ const items = Array.isArray(commits) ? commits : [];
1481
+ // Smart handling for no results - provide actionable suggestions
1482
+ if (items.length === 0) {
1483
+ return createResult({
1484
+ error: createNoResultsError('commits'),
1426
1485
  });
1427
1486
  }
1428
- return createSuccessResult$1({
1429
- query: params.query,
1430
- total: 0,
1431
- commits: [],
1432
- });
1487
+ // Transform to optimized format
1488
+ const optimizedResult = transformCommitsToOptimizedFormat(items, args);
1489
+ return createResult({ data: optimizedResult });
1433
1490
  }
1434
1491
  catch (error) {
1435
- return createErrorResult$1('GitHub commit search failed - verify repository exists or try different filters', error);
1492
+ const errorMessage = error.message || '';
1493
+ if (errorMessage.includes('authentication')) {
1494
+ return createResult({
1495
+ error: createAuthenticationError(),
1496
+ });
1497
+ }
1498
+ if (errorMessage.includes('rate limit')) {
1499
+ return createResult({
1500
+ error: createRateLimitError(false),
1501
+ });
1502
+ }
1503
+ return createResult({
1504
+ error: createSearchFailedError('commits'),
1505
+ });
1436
1506
  }
1437
1507
  });
1438
1508
  }
1439
- function buildGitHubCommitsSearchCommand(params) {
1440
- // Build query following GitHub CLI patterns
1441
- const query = params.query?.trim() || '';
1442
- // Handle complex queries (with qualifiers, operators, or --) differently
1443
- const hasComplexSyntax = query.includes('--') ||
1444
- query.includes(':') ||
1445
- query.includes('OR') ||
1446
- query.includes('AND') ||
1447
- query.includes('(') ||
1448
- query.includes(')') ||
1449
- query.startsWith('-');
1450
- const args = ['commits'];
1451
- // Only add query if it exists
1452
- if (query) {
1453
- if (hasComplexSyntax) {
1454
- // For complex queries with special syntax, handle carefully
1455
- const queryParts = query.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
1456
- queryParts.forEach(part => {
1457
- // If part contains shell special characters, quote it
1458
- if (/[><=&|$`(){}[\];\\]/.test(part) && !part.includes('"')) {
1459
- args.push(`"${part}"`);
1460
- }
1461
- else {
1462
- args.push(part);
1463
- }
1464
- });
1465
- }
1466
- else {
1467
- // For simple queries, use quoting logic
1468
- const queryString = needsQuoting(query) ? `"${query}"` : query;
1469
- args.push(queryString);
1470
- }
1471
- }
1472
- // Add JSON output with commit fields
1473
- args.push('--json', 'author,commit,committer,id,parents,repository,sha,url');
1474
- // Add all filters
1475
- if (params.author)
1476
- args.push(`--author=${params.author}`);
1477
- if (params.authorDate)
1478
- args.push(`--author-date="${params.authorDate}"`);
1509
+ /**
1510
+ * Transform GitHub CLI response to optimized format
1511
+ */
1512
+ function transformCommitsToOptimizedFormat(items, _params) {
1513
+ // Extract repository info if single repo search
1514
+ const singleRepo = extractSingleRepository(items);
1515
+ const optimizedCommits = items
1516
+ .map(item => ({
1517
+ sha: item.sha,
1518
+ message: getCommitTitle(item.commit?.message ?? ''),
1519
+ author: item.commit?.author?.name ?? item.author?.login ?? 'Unknown',
1520
+ date: toDDMMYYYY(item.commit?.author?.date ?? ''),
1521
+ repository: singleRepo
1522
+ ? undefined
1523
+ : simplifyRepoUrl(item.repository?.url || ''),
1524
+ url: singleRepo
1525
+ ? item.sha
1526
+ : `${simplifyRepoUrl(item.repository?.url || '')}@${item.sha}`,
1527
+ }))
1528
+ .map(commit => {
1529
+ // Remove undefined fields
1530
+ const cleanCommit = {};
1531
+ Object.entries(commit).forEach(([key, value]) => {
1532
+ if (value !== undefined) {
1533
+ cleanCommit[key] = value;
1534
+ }
1535
+ });
1536
+ return cleanCommit;
1537
+ });
1538
+ const result = {
1539
+ commits: optimizedCommits,
1540
+ total_count: items.length,
1541
+ };
1542
+ // Add repository info if single repo
1543
+ if (singleRepo) {
1544
+ result.repository = {
1545
+ name: singleRepo.fullName,
1546
+ description: singleRepo.description,
1547
+ };
1548
+ }
1549
+ return result;
1550
+ }
1551
+ /**
1552
+ * Extract single repository if all results are from same repo
1553
+ */
1554
+ function extractSingleRepository(items) {
1555
+ if (items.length === 0)
1556
+ return null;
1557
+ const firstRepo = items[0].repository;
1558
+ const allSameRepo = items.every(item => item.repository.fullName === firstRepo.fullName);
1559
+ return allSameRepo ? firstRepo : null;
1560
+ }
1561
+ async function searchGitHubCommits(params) {
1562
+ const cacheKey = generateCacheKey('gh-commits', params);
1563
+ return withCache(cacheKey, async () => {
1564
+ try {
1565
+ const args = buildGitHubCommitCliArgs(params);
1566
+ const result = await executeGitHubCommand('search', args, {
1567
+ cache: false,
1568
+ });
1569
+ return result;
1570
+ }
1571
+ catch (error) {
1572
+ const errorMessage = error.message || '';
1573
+ if (errorMessage.includes('authentication')) {
1574
+ return createResult({
1575
+ error: createAuthenticationError(),
1576
+ });
1577
+ }
1578
+ if (errorMessage.includes('rate limit')) {
1579
+ return createResult({
1580
+ error: createRateLimitError(false),
1581
+ });
1582
+ }
1583
+ return createResult({
1584
+ error: createSearchFailedError('commits'),
1585
+ });
1586
+ }
1587
+ });
1588
+ }
1589
+ function buildGitHubCommitCliArgs(params) {
1590
+ const args = ['commits'];
1591
+ // Add query if provided - simplified approach for better results
1592
+ if (params.query) {
1593
+ // Simple, direct query handling - GitHub commit search works better with straightforward queries
1594
+ args.push(params.query.trim());
1595
+ }
1596
+ // Repository filters
1597
+ if (params.owner && params.repo) {
1598
+ args.push(`--repo=${params.owner}/${params.repo}`);
1599
+ }
1600
+ else if (params.owner) {
1601
+ args.push(`--owner=${params.owner}`);
1602
+ }
1603
+ // Author filters
1604
+ if (params.author)
1605
+ args.push(`--author=${params.author}`);
1606
+ if (params.authorName)
1607
+ args.push(`--author-name=${params.authorName}`);
1479
1608
  if (params.authorEmail)
1480
1609
  args.push(`--author-email=${params.authorEmail}`);
1481
- if (params.authorName)
1482
- args.push(`--author-name="${params.authorName}"`);
1610
+ // Committer filters
1483
1611
  if (params.committer)
1484
1612
  args.push(`--committer=${params.committer}`);
1485
- if (params.committerDate)
1486
- args.push(`--committer-date="${params.committerDate}"`);
1613
+ if (params.committerName)
1614
+ args.push(`--committer-name=${params.committerName}`);
1487
1615
  if (params.committerEmail)
1488
1616
  args.push(`--committer-email=${params.committerEmail}`);
1489
- if (params.committerName)
1490
- args.push(`--committer-name="${params.committerName}"`);
1617
+ // Date filters
1618
+ if (params.authorDate)
1619
+ args.push(`--author-date=${params.authorDate}`);
1620
+ if (params.committerDate)
1621
+ args.push(`--committer-date=${params.committerDate}`);
1622
+ // Hash filters
1491
1623
  if (params.hash)
1492
1624
  args.push(`--hash=${params.hash}`);
1493
1625
  if (params.parent)
1494
1626
  args.push(`--parent=${params.parent}`);
1495
1627
  if (params.tree)
1496
1628
  args.push(`--tree=${params.tree}`);
1497
- if (params.merge)
1498
- args.push('--merge');
1629
+ // State filters
1630
+ if (params.merge !== undefined)
1631
+ args.push(`--merge=${params.merge}`);
1632
+ // Visibility
1499
1633
  if (params.visibility)
1500
1634
  args.push(`--visibility=${params.visibility}`);
1501
- // Handle repo and owner
1502
- if (params.repo && params.owner) {
1503
- args.push(`--repo=${params.owner}/${params.repo}`);
1635
+ // Sorting and pagination
1636
+ if (params.sort)
1637
+ args.push(`--sort=${params.sort}`);
1638
+ if (params.order)
1639
+ args.push(`--order=${params.order}`);
1640
+ if (params.limit)
1641
+ args.push(`--limit=${params.limit}`);
1642
+ // JSON output
1643
+ args.push('--json=sha,commit,author,committer,repository,url,parents');
1644
+ return args;
1645
+ }
1646
+
1647
+ const GITHUB_SEARCH_ISSUES_TOOL_NAME = 'githubSearchIssues';
1648
+ const DESCRIPTION$4 = `Search GitHub issues for bug discovery and feature analysis. Supports filtering by state, labels, assignee, dates, and more. Parameters: query (required), owner (optional - GitHub username/org, NOT owner/repo), repo (optional - repository name, use with owner for specific repo), app (optional), archived (optional), assignee (optional), author (optional), closed (optional), commenter (optional), comments (optional), created (optional), includePrs (optional), interactions (optional), involves (optional), labels (optional), language (optional), locked (optional), match (optional), mentions (optional), milestone (optional), noAssignee (optional), noLabel (optional), noMilestone (optional), noProject (optional), project (optional), reactions (optional), state (optional), teamMentions (optional), updated (optional), visibility (optional), sort (optional), order (optional), limit (optional).`;
1649
+ function registerSearchGitHubIssuesTool(server) {
1650
+ server.registerTool(GITHUB_SEARCH_ISSUES_TOOL_NAME, {
1651
+ description: DESCRIPTION$4,
1652
+ inputSchema: {
1653
+ query: z
1654
+ .string()
1655
+ .min(1, 'Search query is required and cannot be empty')
1656
+ .describe('Search terms. Start simple: "error", "crash". Use quotes for exact phrases.'),
1657
+ owner: z
1658
+ .string()
1659
+ .min(1)
1660
+ .optional()
1661
+ .describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
1662
+ repo: z
1663
+ .string()
1664
+ .optional()
1665
+ .describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
1666
+ app: z
1667
+ .string()
1668
+ .optional()
1669
+ .describe('GitHub App that created the issue'),
1670
+ archived: z
1671
+ .boolean()
1672
+ .optional()
1673
+ .describe('Include archived repositories'),
1674
+ assignee: z.string().optional().describe('GitHub username of assignee'),
1675
+ author: z
1676
+ .string()
1677
+ .optional()
1678
+ .describe('GitHub username of issue creator'),
1679
+ closed: z
1680
+ .string()
1681
+ .optional()
1682
+ .describe('When closed. Format: >2020-01-01'),
1683
+ commenter: z
1684
+ .string()
1685
+ .optional()
1686
+ .describe('User who commented on issue'),
1687
+ comments: z
1688
+ .number()
1689
+ .optional()
1690
+ .describe('Comment count. Format: >10, <5, 5..10'),
1691
+ created: z
1692
+ .string()
1693
+ .optional()
1694
+ .describe('When created. Format: >2020-01-01'),
1695
+ includePrs: z
1696
+ .boolean()
1697
+ .optional()
1698
+ .describe('Include pull requests. Default: false'),
1699
+ interactions: z
1700
+ .number()
1701
+ .optional()
1702
+ .describe('Total interactions (reactions + comments)'),
1703
+ involves: z.string().optional().describe('User involved in any way'),
1704
+ labels: z
1705
+ .string()
1706
+ .optional()
1707
+ .describe('Label names (bug, feature, etc.)'),
1708
+ language: z.string().optional().describe('Repository language'),
1709
+ locked: z.boolean().optional().describe('Conversation locked status'),
1710
+ match: z
1711
+ .enum(['title', 'body', 'comments'])
1712
+ .optional()
1713
+ .describe('Search scope. Default: title and body'),
1714
+ mentions: z.string().optional().describe('Issues mentioning this user'),
1715
+ milestone: z.string().optional().describe('Milestone name'),
1716
+ noAssignee: z.boolean().optional().describe('Issues without assignee'),
1717
+ noLabel: z.boolean().optional().describe('Issues without labels'),
1718
+ noMilestone: z
1719
+ .boolean()
1720
+ .optional()
1721
+ .describe('Issues without milestone'),
1722
+ noProject: z.boolean().optional().describe('Issues not in projects'),
1723
+ project: z.string().optional().describe('Project board number'),
1724
+ reactions: z
1725
+ .number()
1726
+ .optional()
1727
+ .describe('Reaction count. Format: >10'),
1728
+ state: z
1729
+ .enum(['open', 'closed'])
1730
+ .optional()
1731
+ .describe('Issue state. Default: all'),
1732
+ teamMentions: z.string().optional().describe('Team mentioned in issue'),
1733
+ updated: z
1734
+ .string()
1735
+ .optional()
1736
+ .describe('When updated. Format: >2020-01-01'),
1737
+ visibility: z
1738
+ .enum(['public', 'private', 'internal'])
1739
+ .optional()
1740
+ .describe('Repository visibility'),
1741
+ sort: z
1742
+ .enum([
1743
+ 'comments',
1744
+ 'created',
1745
+ 'interactions',
1746
+ 'reactions',
1747
+ 'reactions-+1',
1748
+ 'reactions--1',
1749
+ 'reactions-heart',
1750
+ 'reactions-smile',
1751
+ 'reactions-tada',
1752
+ 'reactions-thinking_face',
1753
+ 'updated',
1754
+ 'best-match',
1755
+ ])
1756
+ .optional()
1757
+ .describe('Sort by activity or reactions. Default: best match'),
1758
+ order: z
1759
+ .enum(['asc', 'desc'])
1760
+ .optional()
1761
+ .default('desc')
1762
+ .describe('Sort order. Default: desc'),
1763
+ limit: z
1764
+ .number()
1765
+ .int()
1766
+ .min(1)
1767
+ .max(50)
1768
+ .optional()
1769
+ .default(25)
1770
+ .describe('Results limit (1-50). Default: 25'),
1771
+ },
1772
+ annotations: {
1773
+ title: 'GitHub Issues Search - Bug & Feature Discovery',
1774
+ readOnlyHint: true,
1775
+ destructiveHint: false,
1776
+ idempotentHint: true,
1777
+ openWorldHint: true,
1778
+ },
1779
+ }, async (args) => {
1780
+ if (!args.query?.trim()) {
1781
+ return createResult({
1782
+ error: `${ERROR_MESSAGES.QUERY_REQUIRED} ${SUGGESTIONS.PROVIDE_KEYWORDS}`,
1783
+ });
1784
+ }
1785
+ if (args.query.length > 256) {
1786
+ return createResult({
1787
+ error: ERROR_MESSAGES.QUERY_TOO_LONG,
1788
+ });
1789
+ }
1790
+ try {
1791
+ return await searchGitHubIssues(args);
1792
+ }
1793
+ catch (error) {
1794
+ const errorMessage = error instanceof Error ? error.message : '';
1795
+ if (errorMessage.includes('authentication')) {
1796
+ return createResult({
1797
+ error: createAuthenticationError(),
1798
+ });
1799
+ }
1800
+ if (errorMessage.includes('rate limit')) {
1801
+ return createResult({
1802
+ error: createRateLimitError(false),
1803
+ });
1804
+ }
1805
+ // Generic fallback
1806
+ return createResult({
1807
+ error: createSearchFailedError('issues'),
1808
+ });
1809
+ }
1810
+ });
1811
+ }
1812
+ async function searchGitHubIssues(params) {
1813
+ const cacheKey = generateCacheKey('gh-issues', params);
1814
+ return withCache(cacheKey, async () => {
1815
+ const { command, args } = buildGitHubIssuesAPICommand(params);
1816
+ const result = await executeGitHubCommand(command, args, { cache: false });
1817
+ if (result.isError) {
1818
+ return result;
1819
+ }
1820
+ const execResult = JSON.parse(result.content[0].text);
1821
+ const apiResponse = execResult.result;
1822
+ const issues = apiResponse.items || [];
1823
+ const cleanIssues = issues.map((issue) => ({
1824
+ number: issue.number,
1825
+ title: issue.title,
1826
+ state: issue.state,
1827
+ author: issue.user?.login || '',
1828
+ repository: issue.repository_url?.split('/').slice(-2).join('/') || 'unknown',
1829
+ labels: issue.labels?.map(l => l.name) || [],
1830
+ created_at: toDDMMYYYY(issue.created_at),
1831
+ updated_at: toDDMMYYYY(issue.updated_at),
1832
+ url: issue.html_url,
1833
+ comments: issue.comments,
1834
+ reactions: issue.reactions?.total_count || 0,
1835
+ }));
1836
+ const searchResult = {
1837
+ results: cleanIssues,
1838
+ };
1839
+ return createResult({ data: searchResult });
1840
+ });
1841
+ }
1842
+ function buildGitHubIssuesAPICommand(params) {
1843
+ const queryParts = [];
1844
+ // Start with the base query, but filter out qualifiers that will be added separately
1845
+ const baseQuery = params.query?.trim() || '';
1846
+ // Extract and remove qualifiers from the main query to avoid conflicts
1847
+ const qualifierPatterns = [
1848
+ /\bis:(open|closed)\b/gi,
1849
+ /\blabel:("[^"]*"|[^\s]+)/gi,
1850
+ /\bcreated:([^\s]+)/gi,
1851
+ /\bupdated:([^\s]+)/gi,
1852
+ /\bauthor:([^\s]+)/gi,
1853
+ /\bassignee:([^\s]+)/gi,
1854
+ /\bstate:(open|closed)/gi,
1855
+ /\brepo:([^\s]+)/gi,
1856
+ /\borg:([^\s]+)/gi,
1857
+ ];
1858
+ // Remove extracted qualifiers from base query
1859
+ let cleanQuery = baseQuery;
1860
+ qualifierPatterns.forEach(pattern => {
1861
+ cleanQuery = cleanQuery.replace(pattern, '').trim();
1862
+ });
1863
+ // Add the cleaned query if it has content
1864
+ if (cleanQuery) {
1865
+ queryParts.push(cleanQuery);
1504
1866
  }
1505
- else if (params.repo) {
1506
- args.push(`--repo=${params.repo}`);
1867
+ // Repository/organization qualifiers - prioritize function params over query
1868
+ if (params.owner && params.repo) {
1869
+ queryParts.push(`repo:${params.owner}/${params.repo}`);
1507
1870
  }
1508
1871
  else if (params.owner) {
1509
- args.push(`--owner=${params.owner}`);
1872
+ queryParts.push(`org:${params.owner}`);
1510
1873
  }
1511
- // Sorting
1512
- const sortBy = params.sort || 'best-match';
1513
- if (sortBy !== 'best-match') {
1514
- args.push(`--sort=${sortBy}`);
1874
+ // Build search qualifiers from function parameters (these take precedence)
1875
+ const qualifiers = {
1876
+ author: params.author,
1877
+ assignee: params.assignee,
1878
+ mentions: params.mentions,
1879
+ commenter: params.commenter,
1880
+ involves: params.involves,
1881
+ language: params.language,
1882
+ state: params.state,
1883
+ created: params.created,
1884
+ updated: params.updated,
1885
+ closed: params.closed,
1886
+ };
1887
+ Object.entries(qualifiers).forEach(([key, value]) => {
1888
+ if (value)
1889
+ queryParts.push(`${key}:${value}`);
1890
+ });
1891
+ // Special qualifiers - handle labels carefully
1892
+ if (params.labels) {
1893
+ queryParts.push(`label:"${params.labels}"`);
1894
+ }
1895
+ if (params.milestone)
1896
+ queryParts.push(`milestone:"${params.milestone}"`);
1897
+ if (params.noAssignee)
1898
+ queryParts.push('no:assignee');
1899
+ if (params.noLabel)
1900
+ queryParts.push('no:label');
1901
+ if (params.noMilestone)
1902
+ queryParts.push('no:milestone');
1903
+ if (params.archived !== undefined)
1904
+ queryParts.push(`archived:${params.archived}`);
1905
+ if (params.locked)
1906
+ queryParts.push('is:locked');
1907
+ if (params.visibility)
1908
+ queryParts.push(`is:${params.visibility}`);
1909
+ // Extract qualifiers from original query and add them if not already set by params
1910
+ if (baseQuery.includes('is:') && !params.state) {
1911
+ const isMatch = baseQuery.match(/\bis:(open|closed)\b/i);
1912
+ if (isMatch && !queryParts.some(part => part.startsWith('state:'))) {
1913
+ queryParts.push(`state:${isMatch[1].toLowerCase()}`);
1914
+ }
1915
+ }
1916
+ if (baseQuery.includes('label:') && !params.labels) {
1917
+ const labelMatch = baseQuery.match(/\blabel:("[^"]*"|[^\s]+)/i);
1918
+ if (labelMatch) {
1919
+ const labelValue = labelMatch[1].replace(/"/g, '');
1920
+ queryParts.push(`label:"${labelValue}"`);
1921
+ }
1515
1922
  }
1923
+ if (baseQuery.includes('created:') && !params.created) {
1924
+ const createdMatch = baseQuery.match(/\bcreated:([^\s]+)/i);
1925
+ if (createdMatch) {
1926
+ queryParts.push(`created:${createdMatch[1]}`);
1927
+ }
1928
+ }
1929
+ const query = queryParts.filter(Boolean).join(' ');
1930
+ const limit = Math.min(params.limit || 25, 100);
1931
+ let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
1932
+ if (params.sort)
1933
+ apiPath += `&sort=${params.sort}`;
1516
1934
  if (params.order)
1517
- args.push(`--order=${params.order}`);
1518
- // Limit
1519
- if (params.limit)
1520
- args.push(`--limit=${params.limit}`);
1521
- return { command: 'search', args };
1935
+ apiPath += `&order=${params.order}`;
1936
+ return { command: 'api', args: [apiPath] };
1522
1937
  }
1523
1938
 
1524
1939
  // TODO: add PR commeents. e.g, gh pr view <PR_NUMBER_OR_URL_OR_BRANCH> --comments
1525
- const TOOL_NAME$4 = 'github_search_pull_requests';
1526
- const DESCRIPTION$4 = `Find pull requests and implementations with detailed metadata. Discover feature implementations, code review patterns, and development workflows.
1527
-
1528
- SEARCH PATTERNS:
1529
- Boolean: "fix AND bug", "refactor OR cleanup", "feature NOT draft"
1530
- Exact phrases: "initial commit" (quoted)
1531
- GitHub qualifiers: "is:merged review:approved base:main"
1532
- Combine with filters for targeted PR discovery`;
1940
+ const GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME = 'githubSearchPullRequests';
1941
+ const DESCRIPTION$3 = `Search pull requests for implementation discovery and code review analysis. Supports filtering by state, review status, branches, and more. Parameters: query (required), owner (optional - GitHub username/org, NOT owner/repo), repo (optional - repository name, use with owner for specific repo), author (optional), assignee (optional), mentions (optional), commenter (optional), involves (optional), reviewedBy (optional), reviewRequested (optional), state (optional), head (optional), base (optional), language (optional), created (optional), updated (optional), mergedAt (optional), closed (optional), draft (optional), checks (optional), merged (optional), review (optional), limit (optional), sort (optional), order (optional).`;
1533
1942
  function registerSearchGitHubPullRequestsTool(server) {
1534
- server.tool(TOOL_NAME$4, DESCRIPTION$4, {
1535
- query: z
1536
- .string()
1537
- .min(1, 'Search query is required and cannot be empty')
1538
- .describe('Search query with GITHUB SEARCH SYNTAX support. BOOLEAN OPERATORS: "fix AND bug" (both required), "refactor OR cleanup" (either term), "feature NOT draft" (excludes). EXACT PHRASES: "initial commit" (precise matching). GITHUB QUALIFIERS: "is:merged review:approved base:main" (native GitHub syntax). COMBINED: Mix boolean logic with qualifiers for precise PR discovery.'),
1539
- owner: z.string().optional().describe('Repository owner/organization'),
1540
- repo: z.string().optional().describe('Repository name'),
1541
- author: z.string().optional().describe('Filter by pull request author'),
1542
- assignee: z.string().optional().describe('Filter by assignee'),
1543
- mentions: z.string().optional().describe('Filter by user mentions'),
1544
- commenter: z.string().optional().describe('Filter by comments by user'),
1545
- involves: z.string().optional().describe('Filter by user involvement'),
1546
- reviewedBy: z.string().optional().describe('Filter by user who reviewed'),
1547
- reviewRequested: z
1548
- .string()
1549
- .optional()
1550
- .describe('Filter by user or team requested to review'),
1551
- state: z.enum(['open', 'closed']).optional().describe('Filter by state'),
1552
- head: z.string().optional().describe('Filter by head branch name'),
1553
- base: z.string().optional().describe('Filter by base branch name'),
1554
- language: z.string().optional().describe('Filter by coding language'),
1555
- created: z
1556
- .string()
1557
- .optional()
1558
- .describe("Filter by created date (e.g., '>2022-01-01')"),
1559
- updated: z.string().optional().describe('Filter by last updated date'),
1560
- mergedAt: z.string().optional().describe('Filter by merged date'),
1561
- closed: z.string().optional().describe('Filter by closed date'),
1562
- draft: z.boolean().optional().describe('Filter by draft state'),
1563
- checks: z
1564
- .enum(['pending', 'success', 'failure'])
1565
- .optional()
1566
- .describe('Filter based on status of the checks'),
1567
- merged: z.boolean().optional().describe('Filter based on merged state'),
1568
- review: z
1569
- .enum(['none', 'required', 'approved', 'changes_requested'])
1570
- .optional()
1571
- .describe('Filter based on review status'),
1572
- limit: z
1573
- .number()
1574
- .int()
1575
- .min(1)
1576
- .max(50)
1577
- .optional()
1578
- .default(25)
1579
- .describe('Maximum results (default: 25, max: 50)'),
1580
- sort: z
1581
- .enum([
1582
- 'comments',
1583
- 'reactions',
1584
- 'reactions-+1',
1585
- 'reactions--1',
1586
- 'reactions-smile',
1587
- 'reactions-thinking_face',
1588
- 'reactions-heart',
1589
- 'reactions-tada',
1590
- 'interactions',
1591
- 'created',
1592
- 'updated',
1593
- ])
1594
- .optional()
1595
- .describe('Sort criteria'),
1596
- order: z
1597
- .enum(['asc', 'desc'])
1598
- .optional()
1599
- .default('desc')
1600
- .describe('Order (default: desc)'),
1601
- }, {
1602
- title: TOOL_NAME$4,
1603
- description: DESCRIPTION$4,
1604
- readOnlyHint: true,
1605
- destructiveHint: false,
1606
- idempotentHint: true,
1607
- openWorldHint: true,
1943
+ server.registerTool(GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME, {
1944
+ description: DESCRIPTION$3,
1945
+ inputSchema: {
1946
+ query: z
1947
+ .string()
1948
+ .min(1, 'Search query is required and cannot be empty')
1949
+ .describe('Search terms. Start simple: "refactor", "optimization". Use quotes for exact phrases.'),
1950
+ owner: z
1951
+ .string()
1952
+ .optional()
1953
+ .describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
1954
+ repo: z
1955
+ .string()
1956
+ .optional()
1957
+ .describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
1958
+ author: z.string().optional().describe('GitHub username of PR author'),
1959
+ assignee: z.string().optional().describe('GitHub username of assignee'),
1960
+ mentions: z.string().optional().describe('PRs mentioning this user'),
1961
+ commenter: z.string().optional().describe('User who commented on PR'),
1962
+ involves: z.string().optional().describe('User involved in any way'),
1963
+ reviewedBy: z.string().optional().describe('User who reviewed the PR'),
1964
+ reviewRequested: z
1965
+ .string()
1966
+ .optional()
1967
+ .describe('User/team requested for review'),
1968
+ state: z
1969
+ .enum(['open', 'closed'])
1970
+ .optional()
1971
+ .describe('PR state. Default: all'),
1972
+ head: z.string().optional().describe('Source branch name'),
1973
+ base: z
1974
+ .string()
1975
+ .optional()
1976
+ .describe('Target branch name (main, develop, etc.)'),
1977
+ language: z.string().optional().describe('Repository language'),
1978
+ created: z
1979
+ .string()
1980
+ .optional()
1981
+ .describe('When created. Format: >2020-01-01'),
1982
+ updated: z
1983
+ .string()
1984
+ .optional()
1985
+ .describe('When updated. Format: >2020-01-01'),
1986
+ mergedAt: z
1987
+ .string()
1988
+ .optional()
1989
+ .describe('When merged. Format: >2020-01-01'),
1990
+ closed: z
1991
+ .string()
1992
+ .optional()
1993
+ .describe('When closed. Format: >2020-01-01'),
1994
+ draft: z.boolean().optional().describe('Draft PR status'),
1995
+ checks: z
1996
+ .enum(['pending', 'success', 'failure'])
1997
+ .optional()
1998
+ .describe('CI/CD check status'),
1999
+ merged: z
2000
+ .boolean()
2001
+ .optional()
2002
+ .describe('Only merged PRs (true) or unmerged (false)'),
2003
+ review: z
2004
+ .enum(['none', 'required', 'approved', 'changes_requested'])
2005
+ .optional()
2006
+ .describe('Review status filter'),
2007
+ limit: z
2008
+ .number()
2009
+ .int()
2010
+ .min(1)
2011
+ .max(50)
2012
+ .optional()
2013
+ .default(25)
2014
+ .describe('Results limit (1-50). Default: 25'),
2015
+ sort: z
2016
+ .enum([
2017
+ 'comments',
2018
+ 'reactions',
2019
+ 'reactions-+1',
2020
+ 'reactions--1',
2021
+ 'reactions-smile',
2022
+ 'reactions-thinking_face',
2023
+ 'reactions-heart',
2024
+ 'reactions-tada',
2025
+ 'interactions',
2026
+ 'created',
2027
+ 'updated',
2028
+ ])
2029
+ .optional()
2030
+ .describe('Sort by activity or reactions. Default: best match'),
2031
+ order: z
2032
+ .enum(['asc', 'desc'])
2033
+ .optional()
2034
+ .default('desc')
2035
+ .describe('Sort order. Default: desc'),
2036
+ },
2037
+ annotations: {
2038
+ title: 'GitHub PR Search - Implementation Discovery',
2039
+ readOnlyHint: true,
2040
+ destructiveHint: false,
2041
+ idempotentHint: true,
2042
+ openWorldHint: true,
2043
+ },
1608
2044
  }, async (args) => {
1609
2045
  if (!args.query?.trim()) {
1610
- return createErrorResult$1('Search query is required and cannot be empty - provide keywords to search for pull requests', new Error('Invalid query'));
2046
+ return createResult({
2047
+ error: `${ERROR_MESSAGES.QUERY_REQUIRED} ${SUGGESTIONS.PROVIDE_PR_KEYWORDS}`,
2048
+ });
1611
2049
  }
1612
2050
  if (args.query.length > 256) {
1613
- return createErrorResult$1('Search query is too long. Please limit to 256 characters or less - simplify your search terms', new Error('Query too long'));
2051
+ return createResult({
2052
+ error: ERROR_MESSAGES.QUERY_TOO_LONG,
2053
+ });
1614
2054
  }
1615
2055
  try {
1616
2056
  return await searchGitHubPullRequests(args);
1617
2057
  }
1618
2058
  catch (error) {
1619
- return createErrorResult$1('GitHub pull requests search failed - verify repository access and query syntax', error);
2059
+ return createResult({
2060
+ error: createSearchFailedError('pull_requests'),
2061
+ });
1620
2062
  }
1621
2063
  });
1622
2064
  }
@@ -1629,8 +2071,13 @@ async function searchGitHubPullRequests(params) {
1629
2071
  return result;
1630
2072
  }
1631
2073
  const execResult = JSON.parse(result.content[0].text);
1632
- const apiResponse = JSON.parse(execResult.result);
2074
+ const apiResponse = execResult.result;
1633
2075
  const pullRequests = apiResponse.items || [];
2076
+ if (pullRequests.length === 0) {
2077
+ return createResult({
2078
+ error: createNoResultsError('pull_requests'),
2079
+ });
2080
+ }
1634
2081
  const cleanPRs = pullRequests.map((pr) => {
1635
2082
  const result = {
1636
2083
  number: pr.number,
@@ -1639,8 +2086,8 @@ async function searchGitHubPullRequests(params) {
1639
2086
  author: pr.user?.login || '',
1640
2087
  repository: pr.repository_url?.split('/').slice(-2).join('/') || 'unknown',
1641
2088
  labels: pr.labels?.map(l => l.name) || [],
1642
- created_at: pr.created_at,
1643
- updated_at: pr.updated_at,
2089
+ created_at: toDDMMYYYY(pr.created_at),
2090
+ updated_at: toDDMMYYYY(pr.updated_at),
1644
2091
  url: pr.html_url,
1645
2092
  comments: pr.comments,
1646
2093
  reactions: pr.reactions?.total_count || 0,
@@ -1650,7 +2097,7 @@ async function searchGitHubPullRequests(params) {
1650
2097
  if (pr.merged_at)
1651
2098
  result.merged_at = pr.merged_at;
1652
2099
  if (pr.closed_at)
1653
- result.closed_at = pr.closed_at;
2100
+ result.closed_at = toDDMMYYYY(pr.closed_at);
1654
2101
  if (pr.head?.ref)
1655
2102
  result.head = pr.head.ref;
1656
2103
  if (pr.base?.ref)
@@ -1658,15 +2105,10 @@ async function searchGitHubPullRequests(params) {
1658
2105
  return result;
1659
2106
  });
1660
2107
  const searchResult = {
1661
- searchType: 'prs',
1662
- query: params.query || '',
1663
2108
  results: cleanPRs,
1664
- metadata: {
1665
- total_count: apiResponse.total_count || 0,
1666
- incomplete_results: apiResponse.incomplete_results || false,
1667
- },
2109
+ total_count: apiResponse.total_count || cleanPRs.length,
1668
2110
  };
1669
- return createSuccessResult$1(searchResult);
2111
+ return createResult({ data: searchResult });
1670
2112
  });
1671
2113
  }
1672
2114
  function buildGitHubPullRequestsAPICommand(params) {
@@ -1726,83 +2168,477 @@ function buildGitHubPullRequestsAPICommand(params) {
1726
2168
  return { command: 'api', args: [apiPath] };
1727
2169
  }
1728
2170
 
1729
- const TOOL_NAME$3 = 'npm_package_search';
1730
- const DESCRIPTION$3 = `Search npm packages by keywords using fuzzy matching.
1731
-
1732
- IMPORTANT LIMITATIONS:
1733
- NO BOOLEAN OPERATORS: NPM search does NOT support AND/OR/NOT - use space-separated keywords for broader search
1734
- FUZZY MATCHING ONLY: No exact phrase matching - searches are approximate keyword matching
1735
- KEYWORD-BASED: Best results with simple, space-separated terms like "react hooks" or "cli typescript"
1736
-
1737
- Required when package name is unknown. If you have the exact package name, use npm_view_package directly. This reduces the need to use GitHub search when packages are found.`;
1738
- const MAX_DESCRIPTION_LENGTH = 100;
1739
- const MAX_KEYWORDS = 10;
1740
- function registerNpmSearchTool(server) {
1741
- server.tool(TOOL_NAME$3, DESCRIPTION$3, {
1742
- queries: z
1743
- .union([z.string(), z.array(z.string())])
1744
- .describe('Package names or keywords to search for. NOTE: No boolean operators (AND/OR/NOT) supported - use simple space-separated keywords like "react hooks" or "typescript cli" for fuzzy matching.'),
1745
- searchlimit: z
1746
- .number()
1747
- .int()
1748
- .min(1)
1749
- .max(50)
1750
- .optional()
1751
- .default(20)
1752
- .describe('Max results per query (default: 20, max: 50)'),
1753
- }, {
1754
- title: TOOL_NAME$3,
1755
- description: DESCRIPTION$3,
1756
- readOnlyHint: true,
1757
- destructiveHint: false,
1758
- idempotentHint: true,
1759
- openWorldHint: true,
2171
+ /**
2172
+ * GitHub Repository Search Tool
2173
+ *
2174
+ * MOST EFFECTIVE PATTERNS (based on testing):
2175
+ *
2176
+ * 1. Quality Discovery:
2177
+ * { topic: ["react", "typescript"], stars: "1000..5000", limit: 10 }
2178
+ *
2179
+ * 2. Organization Research:
2180
+ * { owner: ["microsoft", "google"], language: "python", limit: 10 }
2181
+ *
2182
+ * 3. Beginner Projects:
2183
+ * { goodFirstIssues: ">=5", stars: "100..5000", limit: 10 }
2184
+ *
2185
+ * 4. Recent Quality:
2186
+ * { stars: ">1000", created: ">2023-01-01", limit: 10 }
2187
+ *
2188
+ * AVOID: OR queries + language filter, 5+ filters, multi-word OR
2189
+ * TIP: Use limit parameter instead of adding more filters
2190
+ */
2191
+ const GITHUB_SEARCH_REPOSITORIES_TOOL_NAME = 'githubSearchRepositories';
2192
+ const DESCRIPTION$2 = `Discover GitHub repositories with smart filtering. Supports language, stars, topics, ownership, dates, and community metrics. Parameters: query (optional), owner (optional - GitHub username/org, NOT owner/repo), language (optional), stars (optional), topic (optional), forks (optional), numberOfTopics (optional), license (optional), archived (optional), includeForks (optional), visibility (optional), created (optional), updated (optional), size (optional), goodFirstIssues (optional), helpWantedIssues (optional), followers (optional), match (optional), sort (optional), order (optional), limit (optional).`;
2193
+ /**
2194
+ * Extract owner/repo information from various query formats
2195
+ */
2196
+ function extractOwnerRepoFromQuery(query) {
2197
+ let cleanedQuery = query;
2198
+ let extractedOwner;
2199
+ let extractedRepo;
2200
+ const patterns = [
2201
+ // Pattern 1: GitHub URLs (https://github.com/owner/repo)
2202
+ /github\.com\/([^\\s]+)\/([^\\s]+)/i,
2203
+ // Pattern 2: owner/repo format in query
2204
+ /\b([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])\b/,
2205
+ // Pattern 3: NPM package-like references (@scope/package)
2206
+ /@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])/,
2207
+ ];
2208
+ for (const pattern of patterns) {
2209
+ const match = cleanedQuery.match(pattern);
2210
+ if (match) {
2211
+ extractedOwner = match[1];
2212
+ extractedRepo = match[2];
2213
+ cleanedQuery = cleanedQuery.replace(match[0], '').trim();
2214
+ break; // Stop after the first successful match
2215
+ }
2216
+ }
2217
+ return {
2218
+ extractedOwner,
2219
+ extractedRepo,
2220
+ cleanedQuery: cleanedQuery || query, // Ensure original query is returned if cleaned is empty
2221
+ };
2222
+ }
2223
+ function registerSearchGitHubReposTool(server) {
2224
+ server.registerTool(GITHUB_SEARCH_REPOSITORIES_TOOL_NAME, {
2225
+ description: DESCRIPTION$2,
2226
+ inputSchema: {
2227
+ query: z
2228
+ .string()
2229
+ .optional()
2230
+ .describe('Search query. START SIMPLE: Use 1-2 words with NO filters first (e.g., "react", "auth"). Add qualifiers only after initial search.'),
2231
+ // CORE FILTERS (GitHub CLI flags)
2232
+ owner: z
2233
+ .union([z.string(), z.array(z.string())])
2234
+ .optional()
2235
+ .describe('Repository owner or organization name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). For private repos, use organizations from api_status_check (user_organizations). Can be a single value or array.'),
2236
+ language: z
2237
+ .string()
2238
+ .optional()
2239
+ .describe('Programming language filter. Use when results need refinement.'),
2240
+ stars: z
2241
+ .union([
2242
+ z.number().int().min(0),
2243
+ z.string().regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/),
2244
+ ])
2245
+ .optional()
2246
+ .describe('Stars filter. Supports ranges and thresholds.'),
2247
+ topic: z
2248
+ .union([z.string(), z.array(z.string())])
2249
+ .optional()
2250
+ .describe('Topics filter. Can be a single value or array.'),
2251
+ forks: z.number().optional().describe('Number of forks filter.'),
2252
+ // UPDATED: Match CLI parameter name exactly
2253
+ numberOfTopics: z
2254
+ .number()
2255
+ .optional()
2256
+ .describe('Filter by number of topics (indicates documentation quality).'),
2257
+ // QUALITY & STATE FILTERS
2258
+ license: z
2259
+ .union([z.string(), z.array(z.string())])
2260
+ .optional()
2261
+ .describe('License filter. Works well as array ["mit", "apache-2.0"].'),
2262
+ archived: z
2263
+ .boolean()
2264
+ .optional()
2265
+ .describe('Filter archived repositories (true/false).'),
2266
+ includeForks: z
2267
+ .enum(['false', 'true', 'only'])
2268
+ .optional()
2269
+ .describe('Include forks: false (exclude), true (include), only (forks only).'),
2270
+ visibility: z
2271
+ .enum(['public', 'private', 'internal'])
2272
+ .optional()
2273
+ .describe('Repository visibility filter.'),
2274
+ // DATE & SIZE FILTERS
2275
+ created: z
2276
+ .string()
2277
+ .optional()
2278
+ .describe('Created date filter. Format: ">2020-01-01", "<2023-12-31".'),
2279
+ updated: z
2280
+ .string()
2281
+ .optional()
2282
+ .describe('Updated date filter. Good for finding active projects.'),
2283
+ size: z
2284
+ .string()
2285
+ .optional()
2286
+ .describe('Repository size filter in KB. Format: ">1000", "<500".'),
2287
+ // COMMUNITY FILTERS - Match CLI parameter names exactly
2288
+ goodFirstIssues: z
2289
+ .union([
2290
+ z.number().int().min(0),
2291
+ z
2292
+ .string()
2293
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
2294
+ ])
2295
+ .optional()
2296
+ .describe('Good first issues count. WORKING: Filter for beginner-friendly projects. EXCELLENT when combined with stars "100..5000" for quality beginner projects.'),
2297
+ helpWantedIssues: z
2298
+ .union([
2299
+ z.number().int().min(0),
2300
+ z
2301
+ .string()
2302
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
2303
+ ])
2304
+ .optional()
2305
+ .describe('Help wanted issues count. Good for finding projects needing contributors.'),
2306
+ followers: z.number().optional().describe('Followers count filter.'),
2307
+ // SEARCH SCOPE
2308
+ match: z
2309
+ .enum(['name', 'description', 'readme'])
2310
+ .optional()
2311
+ .describe('Search scope: name, description, or readme content.'),
2312
+ // SORTING & LIMITS - Match CLI defaults exactly
2313
+ sort: z
2314
+ .enum([
2315
+ 'forks',
2316
+ 'help-wanted-issues',
2317
+ 'stars',
2318
+ 'updated',
2319
+ 'best-match',
2320
+ ])
2321
+ .optional()
2322
+ .default('best-match')
2323
+ .describe('Sort criteria for results.'),
2324
+ order: z
2325
+ .enum(['asc', 'desc'])
2326
+ .optional()
2327
+ .default('desc')
2328
+ .describe('Sort order direction.'),
2329
+ limit: z
2330
+ .number()
2331
+ .int()
2332
+ .min(1)
2333
+ .max(100)
2334
+ .optional()
2335
+ .default(30)
2336
+ .describe('Maximum results to return (1-100). Default: 30'),
2337
+ },
2338
+ annotations: {
2339
+ title: 'GitHub Repository Search',
2340
+ readOnlyHint: true,
2341
+ destructiveHint: false,
2342
+ idempotentHint: true,
2343
+ openWorldHint: true,
2344
+ },
1760
2345
  }, async (args) => {
1761
2346
  try {
1762
- const queries = Array.isArray(args.queries)
1763
- ? args.queries
1764
- : [args.queries];
1765
- const searchLimit = args.searchlimit || 20;
1766
- const allPackages = [];
1767
- // Search for each query term
1768
- for (const query of queries) {
1769
- const result = await executeNpmCommand('search', [query, `--searchlimit=${searchLimit}`, '--json'], { cache: true });
1770
- if (!result.isError && result.content?.[0]?.text) {
1771
- const packages = parseNpmSearchOutput(result.content[0].text);
1772
- allPackages.push(...packages);
1773
- }
2347
+ // Extract owner/repo from query if present
2348
+ const queryInfo = args.query
2349
+ ? extractOwnerRepoFromQuery(args.query)
2350
+ : {
2351
+ cleanedQuery: '',
2352
+ extractedOwner: undefined,
2353
+ extractedRepo: undefined,
2354
+ };
2355
+ // Merge extracted owner with explicit owner parameter
2356
+ let finalOwner = args.owner;
2357
+ if (queryInfo.extractedOwner && !finalOwner) {
2358
+ finalOwner = queryInfo.extractedOwner;
1774
2359
  }
1775
- const deduplicatedPackages = deduplicatePackages(allPackages);
1776
- if (deduplicatedPackages.length > 0) {
2360
+ // Update parameters with extracted information
2361
+ const enhancedArgs = {
2362
+ ...args,
2363
+ query: queryInfo.cleanedQuery || args.query,
2364
+ owner: finalOwner,
2365
+ };
2366
+ // Enhanced validation logic for primary filters
2367
+ const hasPrimaryFilter = enhancedArgs.query?.trim() ||
2368
+ enhancedArgs.owner ||
2369
+ enhancedArgs.language ||
2370
+ enhancedArgs.topic ||
2371
+ enhancedArgs.stars ||
2372
+ enhancedArgs.forks;
2373
+ if (!hasPrimaryFilter) {
1777
2374
  return createResult({
1778
- query: Array.isArray(args.queries)
1779
- ? args.queries.join(', ')
1780
- : args.queries,
1781
- total: deduplicatedPackages.length,
1782
- results: deduplicatedPackages,
2375
+ error: SUGGESTIONS.REPO_SEARCH_PRIMARY_FILTER,
1783
2376
  });
1784
2377
  }
1785
- return createResult('No packages found', true);
2378
+ // First attempt: Search with current parameters
2379
+ const result = await searchGitHubRepos(enhancedArgs);
2380
+ // Fallback for private repositories: If no results and owner is specified, try with private visibility
2381
+ if (!result.isError) {
2382
+ const resultData = JSON.parse(result.content[0].text);
2383
+ if (resultData.total === 0 &&
2384
+ enhancedArgs.owner &&
2385
+ !enhancedArgs.visibility) {
2386
+ // Try searching with private visibility for organization repos
2387
+ const privateSearchArgs = {
2388
+ ...enhancedArgs,
2389
+ visibility: 'private',
2390
+ };
2391
+ const privateResult = await searchGitHubRepos(privateSearchArgs);
2392
+ if (!privateResult.isError) {
2393
+ const privateData = JSON.parse(privateResult.content[0].text);
2394
+ if (privateData.total > 0) {
2395
+ // Return private results with note
2396
+ return createResult({
2397
+ data: {
2398
+ ...privateData,
2399
+ note: 'Found results in private repositories within the specified organization.',
2400
+ },
2401
+ });
2402
+ }
2403
+ }
2404
+ }
2405
+ }
2406
+ return result;
1786
2407
  }
1787
2408
  catch (error) {
1788
- return createResult('NPM package search failed - check search terms or try different keywords', true);
2409
+ return createResult({
2410
+ error: createSearchFailedError('repositories'),
2411
+ });
1789
2412
  }
1790
2413
  });
1791
2414
  }
1792
- function deduplicatePackages(packages) {
1793
- const seen = new Set();
1794
- return packages.filter(pkg => {
1795
- if (seen.has(pkg.name))
1796
- return false;
1797
- seen.add(pkg.name);
1798
- return true;
1799
- });
1800
- }
1801
- function normalizePackage(pkg) {
1802
- const description = pkg.description || null;
1803
- const truncatedDescription = description && description.length > MAX_DESCRIPTION_LENGTH
1804
- ? description.substring(0, MAX_DESCRIPTION_LENGTH) + '...'
1805
- : description;
2415
+ async function searchGitHubRepos(params) {
2416
+ const cacheKey = generateCacheKey('gh-repos', params);
2417
+ return withCache(cacheKey, async () => {
2418
+ try {
2419
+ const { command, args } = buildGitHubReposSearchCommand(params);
2420
+ const result = await executeGitHubCommand(command, args, {
2421
+ cache: false,
2422
+ });
2423
+ if (result.isError) {
2424
+ return result;
2425
+ }
2426
+ const execResult = JSON.parse(result.content[0].text);
2427
+ const repositories = execResult.result;
2428
+ if (!Array.isArray(repositories) || repositories.length === 0) {
2429
+ return createResult({
2430
+ error: createNoResultsError('repositories'),
2431
+ });
2432
+ }
2433
+ const analysis = {
2434
+ totalFound: 0,
2435
+ languages: new Set(),
2436
+ avgStars: 0,
2437
+ recentlyUpdated: 0,
2438
+ topStarred: [],
2439
+ };
2440
+ analysis.totalFound = repositories.length;
2441
+ // Analyze repository data
2442
+ let totalStars = 0;
2443
+ const now = new Date();
2444
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
2445
+ repositories.forEach(repo => {
2446
+ // Collect languages
2447
+ if (repo.language) {
2448
+ analysis.languages.add(repo.language);
2449
+ }
2450
+ // Calculate average stars (use correct field name)
2451
+ if (typeof repo.stargazersCount === 'number') {
2452
+ totalStars += repo.stargazersCount;
2453
+ }
2454
+ // Count recently updated repositories (use correct field name)
2455
+ if (repo.updatedAt) {
2456
+ const updatedDate = new Date(repo.updatedAt);
2457
+ if (!isNaN(updatedDate.getTime()) && updatedDate > thirtyDaysAgo) {
2458
+ analysis.recentlyUpdated++;
2459
+ }
2460
+ }
2461
+ });
2462
+ analysis.avgStars =
2463
+ repositories.length > 0
2464
+ ? Math.round(totalStars / repositories.length)
2465
+ : 0;
2466
+ // Get all repositories with comprehensive data
2467
+ analysis.topStarred = repositories.map(repo => ({
2468
+ name: repo.fullName || repo.name,
2469
+ stars: repo.stargazersCount || 0,
2470
+ description: repo.description || 'No description',
2471
+ language: repo.language || 'Unknown',
2472
+ url: repo.url,
2473
+ forks: repo.forksCount || 0,
2474
+ isPrivate: repo.isPrivate || false,
2475
+ isArchived: repo.isArchived || false,
2476
+ isFork: repo.isFork || false,
2477
+ topics: [], // GitHub CLI search repos doesn't provide topics in JSON output
2478
+ license: repo.license?.name || null,
2479
+ hasIssues: repo.hasIssues || false,
2480
+ openIssuesCount: repo.openIssuesCount || 0,
2481
+ createdAt: toDDMMYYYY(repo.createdAt),
2482
+ updatedAt: toDDMMYYYY(repo.updatedAt),
2483
+ visibility: repo.visibility || 'public',
2484
+ owner: repo.owner?.login || repo.owner,
2485
+ }));
2486
+ return createResult({
2487
+ data: {
2488
+ total_count: analysis.totalFound,
2489
+ ...(analysis.totalFound > 0
2490
+ ? {
2491
+ repositories: analysis.topStarred,
2492
+ summary: {
2493
+ languages: Array.from(analysis.languages).slice(0, 10),
2494
+ avgStars: analysis.avgStars,
2495
+ recentlyUpdated: analysis.recentlyUpdated,
2496
+ },
2497
+ }
2498
+ : {
2499
+ repositories: [],
2500
+ }),
2501
+ },
2502
+ });
2503
+ }
2504
+ catch (error) {
2505
+ return createResult({
2506
+ error: createSearchFailedError('repositories'),
2507
+ });
2508
+ }
2509
+ });
2510
+ }
2511
+ function buildGitHubReposSearchCommand(params) {
2512
+ const query = params.query?.trim() || '';
2513
+ const args = ['repos'];
2514
+ const hasEmbeddedQualifiers = query &&
2515
+ /\b(stars|language|org|repo|topic|user|created|updated|size|license|archived|fork|good-first-issues|help-wanted-issues):/i.test(query);
2516
+ if (query) {
2517
+ args.push(query);
2518
+ }
2519
+ args.push('--json=name,fullName,description,language,stargazersCount,forksCount,updatedAt,createdAt,url,owner,isPrivate,license,hasIssues,openIssuesCount,isArchived,isFork,visibility');
2520
+ const addArg = (paramName, cliFlag, condition = true, formatter) => {
2521
+ const value = params[paramName];
2522
+ if (value !== undefined && condition) {
2523
+ if (Array.isArray(value)) {
2524
+ args.push(`--${cliFlag}=${value.join(',')}`);
2525
+ }
2526
+ else if (formatter) {
2527
+ args.push(`--${cliFlag}=${formatter(value)}`);
2528
+ }
2529
+ else {
2530
+ args.push(`--${cliFlag}=${value.toString()}`);
2531
+ }
2532
+ }
2533
+ };
2534
+ // CORE FILTERS
2535
+ addArg('owner', 'owner', !hasEmbeddedQualifiers);
2536
+ addArg('language', 'language', !hasEmbeddedQualifiers);
2537
+ addArg('forks', 'forks', !hasEmbeddedQualifiers);
2538
+ addArg('topic', 'topic', !hasEmbeddedQualifiers);
2539
+ addArg('numberOfTopics', 'number-topics');
2540
+ addArg('stars', 'stars', !hasEmbeddedQualifiers, value => typeof value === 'number' ? value.toString() : value.trim());
2541
+ // QUALITY & STATE FILTERS
2542
+ addArg('archived', 'archived');
2543
+ addArg('includeForks', 'include-forks');
2544
+ addArg('visibility', 'visibility');
2545
+ addArg('license', 'license');
2546
+ // DATE & SIZE FILTERS
2547
+ addArg('created', 'created');
2548
+ addArg('updated', 'updated');
2549
+ addArg('size', 'size');
2550
+ // COMMUNITY FILTERS
2551
+ addArg('goodFirstIssues', 'good-first-issues', true, value => typeof value === 'number' ? value.toString() : value);
2552
+ addArg('helpWantedIssues', 'help-wanted-issues', true, value => typeof value === 'number' ? value.toString() : value);
2553
+ addArg('followers', 'followers');
2554
+ // SEARCH SCOPE
2555
+ addArg('match', 'match');
2556
+ // SORTING AND LIMITS
2557
+ addArg('limit', 'limit');
2558
+ addArg('order', 'order');
2559
+ const sortBy = params.sort || 'best-match';
2560
+ if (sortBy !== 'best-match') {
2561
+ args.push(`--sort=${sortBy}`);
2562
+ }
2563
+ return { command: 'search', args };
2564
+ }
2565
+
2566
+ const NPM_PACKAGE_SEARCH_TOOL_NAME = 'npmPackageSearch';
2567
+ const DESCRIPTION$1 = `Search NPM packages with fuzzy matching. Supports multiple search terms and aggregates results. Use functional keywords like "react hooks", "auth", or "testing". Parameters: queries (required, string or array), searchLimit (optional).`;
2568
+ const MAX_DESCRIPTION_LENGTH = 100;
2569
+ const MAX_KEYWORDS = 10;
2570
+ function registerNpmSearchTool(server) {
2571
+ server.registerTool(NPM_PACKAGE_SEARCH_TOOL_NAME, {
2572
+ description: DESCRIPTION$1,
2573
+ inputSchema: {
2574
+ queries: z
2575
+ .union([z.string(), z.array(z.string())])
2576
+ .describe('Search terms for packages. Use functionality keywords: "react hooks", "cli tool", "testing"'),
2577
+ searchLimit: z
2578
+ .number()
2579
+ .int()
2580
+ .min(1)
2581
+ .max(50)
2582
+ .optional()
2583
+ .default(20)
2584
+ .describe('Results limit per query (1-50). Default: 20'),
2585
+ },
2586
+ annotations: {
2587
+ title: 'NPM Package Search',
2588
+ readOnlyHint: true,
2589
+ destructiveHint: false,
2590
+ idempotentHint: true,
2591
+ openWorldHint: true,
2592
+ },
2593
+ }, async (args) => {
2594
+ try {
2595
+ const queries = Array.isArray(args.queries)
2596
+ ? args.queries
2597
+ : [args.queries];
2598
+ const searchLimit = args.searchLimit || 20;
2599
+ const allPackages = [];
2600
+ // Search for each query term
2601
+ for (const query of queries) {
2602
+ const result = await executeNpmCommand('search', [query, `--searchlimit=${searchLimit}`, '--json'], { cache: true });
2603
+ if (!result.isError && result.content?.[0]?.text) {
2604
+ const packages = parseNpmSearchOutput(result.content[0].text);
2605
+ allPackages.push(...packages);
2606
+ }
2607
+ }
2608
+ const deduplicatedPackages = deduplicatePackages(allPackages);
2609
+ if (deduplicatedPackages.length > 0) {
2610
+ return createResult({
2611
+ data: {
2612
+ total_count: deduplicatedPackages.length,
2613
+ results: deduplicatedPackages,
2614
+ },
2615
+ });
2616
+ }
2617
+ return createResult({
2618
+ error: createNoResultsError('packages'),
2619
+ });
2620
+ }
2621
+ catch (error) {
2622
+ return createResult({
2623
+ error: createSearchFailedError('packages'),
2624
+ });
2625
+ }
2626
+ });
2627
+ }
2628
+ function deduplicatePackages(packages) {
2629
+ const seen = new Set();
2630
+ return packages.filter(pkg => {
2631
+ if (seen.has(pkg.name))
2632
+ return false;
2633
+ seen.add(pkg.name);
2634
+ return true;
2635
+ });
2636
+ }
2637
+ function normalizePackage(pkg) {
2638
+ const description = pkg.description || null;
2639
+ const truncatedDescription = description && description.length > MAX_DESCRIPTION_LENGTH
2640
+ ? description.substring(0, MAX_DESCRIPTION_LENGTH) + '...'
2641
+ : description;
1806
2642
  const keywords = pkg.keywords || [];
1807
2643
  const limitedKeywords = keywords.slice(0, MAX_KEYWORDS);
1808
2644
  return {
@@ -1816,9 +2652,7 @@ function normalizePackage(pkg) {
1816
2652
  function parseNpmSearchOutput(output) {
1817
2653
  try {
1818
2654
  const wrapper = JSON.parse(output);
1819
- const commandResult = typeof wrapper.result === 'string'
1820
- ? JSON.parse(wrapper.result)
1821
- : wrapper.result;
2655
+ const commandResult = wrapper.result;
1822
2656
  let packages = [];
1823
2657
  // Handle different npm search output formats
1824
2658
  if (Array.isArray(commandResult)) {
@@ -1832,619 +2666,407 @@ function parseNpmSearchOutput(output) {
1832
2666
  }
1833
2667
  return packages.map(normalizePackage);
1834
2668
  }
1835
- catch {
2669
+ catch (error) {
2670
+ logger.warn('Failed to parse NPM search results:', error);
1836
2671
  return [];
1837
2672
  }
1838
2673
  }
1839
2674
 
1840
- const TOOL_NAME$2 = 'github_get_contents';
1841
- const DESCRIPTION$2 = `Browse repository structure and verify file existence. Use before github_get_file_content to confirm files exist and understand organization, especially when the path is uncertain.`;
1842
- function registerViewRepositoryStructureTool(server) {
1843
- server.tool(TOOL_NAME$2, DESCRIPTION$2, {
1844
- owner: z
1845
- .string()
1846
- .min(1)
1847
- .max(100)
1848
- .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
1849
- .describe(`Repository owner/organization (e.g., 'microsoft', 'facebook'). Use github_search_repositories to discover valid owners.`),
1850
- repo: z
1851
- .string()
1852
- .min(1)
1853
- .max(100)
1854
- .regex(/^[a-zA-Z0-9._-]+$/, 'Invalid repository name format')
1855
- .describe('Repository name (e.g., "vscode", "react"). Case-sensitive. Use github_search_repositories to find exact names.'),
1856
- branch: z
1857
- .string()
1858
- .min(1)
1859
- .max(255)
1860
- .regex(/^[^\s]+$/, 'Branch name cannot contain spaces')
1861
- .describe("Target branch name (e.g., 'main', 'canary', 'develop'). " +
1862
- 'Auto-detects repository default if not found. ' +
1863
- 'Use github_search_repositories or api calls to discover valid branches first.'),
1864
- path: z
1865
- .string()
1866
- .optional()
1867
- .default('')
1868
- .refine(path => !path.includes('..'), 'Path traversal not allowed')
1869
- .refine(path => path.length <= 500, 'Path too long')
1870
- .describe('Directory path within repository (e.g., "src/components", "packages/core"). ' +
1871
- 'Leave empty for root. Use previous results to navigate deeper.'),
1872
- }, {
1873
- title: TOOL_NAME$2,
1874
- description: DESCRIPTION$2,
1875
- readOnlyHint: true,
1876
- destructiveHint: false,
1877
- idempotentHint: true,
1878
- openWorldHint: true,
2675
+ const NPM_VIEW_PACKAGE_TOOL_NAME = 'npmViewPackage';
2676
+ const DESCRIPTION = `View detailed NPM package information including repository URL, exports, version history, dependencies, and download stats. Returns optimized metadata for code navigation. Parameters: packageName (required).`;
2677
+ function registerNpmViewPackageTool(server) {
2678
+ server.registerTool(NPM_VIEW_PACKAGE_TOOL_NAME, {
2679
+ description: DESCRIPTION,
2680
+ inputSchema: {
2681
+ packageName: z
2682
+ .string()
2683
+ .min(1)
2684
+ .describe('NPM package name (e.g., "react", "express", "@types/node")'),
2685
+ },
2686
+ annotations: {
2687
+ title: 'NPM Package Analyzer',
2688
+ readOnlyHint: true,
2689
+ destructiveHint: false,
2690
+ idempotentHint: true,
2691
+ openWorldHint: true,
2692
+ },
1879
2693
  }, async (args) => {
1880
2694
  try {
1881
- const result = await viewRepositoryStructure(args);
2695
+ const result = await viewNpmPackage(args.packageName);
1882
2696
  if (result.isError) {
1883
- return createResult(result.content[0].text, true);
1884
- }
1885
- if (result.content && result.content[0] && !result.isError) {
1886
- const { data, parsed } = parseJsonResponse(result.content[0].text);
1887
- if (parsed) {
1888
- const typedResult = {
1889
- path: data.path,
1890
- baseUrl: data.baseUrl,
1891
- files: data.files || [],
1892
- folders: data.folders || [],
1893
- ...(data.branchFallback && {
1894
- branchFallback: data.branchFallback,
1895
- }),
1896
- };
1897
- return createResult(typedResult);
1898
- }
2697
+ return result;
1899
2698
  }
1900
- return result;
2699
+ const execResult = JSON.parse(result.content[0].text);
2700
+ const packageData = execResult.result;
2701
+ // Transform to optimized format
2702
+ const optimizedResult = transformToOptimizedFormat(packageData);
2703
+ return createResult({ data: optimizedResult });
1901
2704
  }
1902
2705
  catch (error) {
1903
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1904
- return createResult(`Repository exploration failed: ${errorMessage} - verify repository exists and is accessible`, true);
1905
- }
1906
- });
1907
- }
1908
- /**
1909
- * Views the structure of a GitHub repository at a specific path.
1910
- *
1911
- * Features:
1912
- * - Smart branch detection: fetches repository default branch automatically
1913
- * - Intelligent fallback: tries requested -> default -> common branches
1914
- * - Input validation: prevents path traversal and validates GitHub naming
1915
- * - Clear error context: provides descriptive error messages
1916
- * - Efficient caching: avoids redundant API calls
1917
- */
1918
- async function viewRepositoryStructure(params) {
1919
- const cacheKey = generateCacheKey('gh-repo-structure', params);
1920
- return withCache(cacheKey, async () => {
1921
- const { owner, repo, branch, path = '' } = params;
1922
- try {
1923
- // Clean up path
1924
- const cleanPath = path.startsWith('/') ? path.substring(1) : path;
1925
- // Try the requested branch first, then fallback to main/master
1926
- const branchesToTry = await getSmartBranchFallback(owner, repo, branch);
1927
- let items = [];
1928
- let usedBranch = branch;
1929
- let lastError = null;
1930
- for (const tryBranch of branchesToTry) {
1931
- try {
1932
- const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${tryBranch}`;
1933
- const result = await executeGitHubCommand('api', [apiPath], {
1934
- cache: false,
1935
- });
1936
- if (!result.isError) {
1937
- const execResult = JSON.parse(result.content[0].text);
1938
- const apiItems = JSON.parse(execResult.result);
1939
- items = Array.isArray(apiItems) ? apiItems : [apiItems];
1940
- usedBranch = tryBranch;
1941
- break;
1942
- }
1943
- else {
1944
- lastError = new Error(result.content[0].text);
1945
- }
1946
- }
1947
- catch (error) {
1948
- lastError = error instanceof Error ? error : new Error(String(error));
1949
- // Try next branch
1950
- continue;
1951
- }
2706
+ const errorMessage = error.message || '';
2707
+ if (errorMessage.includes('not found')) {
2708
+ return createResult({
2709
+ error: 'Package not found. Check spelling and use exact package name from npm',
2710
+ });
1952
2711
  }
1953
- if (items.length === 0) {
1954
- // Use the most descriptive error message
1955
- const errorMsg = lastError?.message || 'Unknown error';
1956
- if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
1957
- if (path) {
1958
- throw new Error(`Path "${path}" not found - verify path exists or use github_search_code to find files`);
1959
- }
1960
- else {
1961
- throw new Error(`Repository not found: ${owner}/${repo} - verify owner/repo names or use github_search_repositories`);
1962
- }
1963
- }
1964
- else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
1965
- throw new Error(`Access denied to repository ${owner}/${repo} - repository may be private or require authentication`);
1966
- }
1967
- else {
1968
- throw new Error(`Access failed: ${owner}/${repo} - check connection or repository permissions`);
1969
- }
2712
+ if (errorMessage.includes('network')) {
2713
+ return createResult({
2714
+ error: 'Network error. Check internet connection and try again',
2715
+ });
1970
2716
  }
1971
- // Limit total items to 100 for efficiency
1972
- const limitedItems = items.slice(0, 100);
1973
- // Sort: directories first, then alphabetically
1974
- limitedItems.sort((a, b) => {
1975
- if (a.type !== b.type) {
1976
- return a.type === 'dir' ? -1 : 1;
1977
- }
1978
- return a.name.localeCompare(b.name);
1979
- });
1980
- // Create base URL for GitHub API - construct it reliably from the parameters
1981
- const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${cleanPath ? cleanPath + '/' : ''}`;
1982
- // Transform to lean structure with simplified paths and URLs
1983
- const files = limitedItems
1984
- .filter(item => item.type === 'file')
1985
- .map(item => {
1986
- // Simplify path by removing redundant prefix
1987
- let simplifiedPath = item.name; // Use just the filename for files in current directory
1988
- // If we're in a subdirectory and the item path is longer than just the name,
1989
- // show relative path from current directory
1990
- if (cleanPath && item.path.startsWith(cleanPath + '/')) {
1991
- simplifiedPath = item.path.substring(cleanPath.length + 1);
1992
- }
1993
- else if (!cleanPath) {
1994
- // At root level, use the full path but without leading slash
1995
- simplifiedPath = item.path;
1996
- }
1997
- // Extract just the filename and query params from the URL
1998
- const urlParts = item.url.split('/');
1999
- const filename = urlParts[urlParts.length - 1]; // Gets "filename?ref=branch"
2000
- return {
2001
- name: simplifiedPath,
2002
- size: item.size,
2003
- url: filename, // Just the filename and query params
2004
- };
2717
+ return createResult({
2718
+ error: 'Failed to fetch package information. Try again or check npm status',
2005
2719
  });
2006
- const folders = limitedItems
2007
- .filter(item => item.type === 'dir')
2008
- .map(item => `${item.name}/`);
2009
- const result = {
2010
- path: `${owner}/${repo}${path ? `/${path}` : ''}`,
2011
- baseUrl: `${baseUrl}?ref=${usedBranch}`, // Include complete base URL with branch
2012
- files,
2013
- folders,
2014
- ...(usedBranch !== branch && {
2015
- branchFallback: {
2016
- requested: branch,
2017
- used: usedBranch,
2018
- message: `Used '${usedBranch}' instead of '${branch}'`,
2019
- },
2020
- }),
2021
- };
2022
- return createSuccessResult$1(result);
2023
- }
2024
- catch (error) {
2025
- const errorMessage = error instanceof Error ? error.message : String(error);
2026
- return createErrorResult$1('Repository access failed - verify repository exists and check authentication', new Error(errorMessage));
2027
2720
  }
2028
2721
  });
2029
2722
  }
2030
2723
  /**
2031
- * Intelligently determines the best branches to try for a repository.
2032
- * Attempts to fetch the default branch first, then falls back to common branches.
2724
+ * Transform NPM CLI response to optimized format for code analysis
2033
2725
  */
2034
- async function getSmartBranchFallback(owner, repo, requestedBranch) {
2035
- const branches = [requestedBranch];
2036
- try {
2037
- // Try to get repository info to find default branch
2038
- const repoInfoResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
2039
- cache: false,
2040
- });
2041
- if (!repoInfoResult.isError) {
2042
- const repoData = JSON.parse(JSON.parse(repoInfoResult.content[0].text).result);
2043
- const defaultBranch = repoData.default_branch;
2044
- if (defaultBranch && !branches.includes(defaultBranch)) {
2045
- branches.push(defaultBranch);
2046
- }
2047
- }
2048
- }
2049
- catch {
2050
- // If we can't get repo info, proceed with standard fallbacks
2726
+ function transformToOptimizedFormat(packageData) {
2727
+ // Extract repository URL and simplify
2728
+ const repoUrl = packageData.repository?.url || packageData.repositoryGitUrl || '';
2729
+ const repository = repoUrl ? simplifyRepoUrl(repoUrl) : '';
2730
+ // Simplify exports to essential entry points only
2731
+ const exports = packageData.exports
2732
+ ? simplifyExports(packageData.exports)
2733
+ : undefined;
2734
+ // Get version timestamps from time object and limit to last 5
2735
+ const timeData = packageData.time || {};
2736
+ const versionList = Array.isArray(packageData.versions)
2737
+ ? packageData.versions
2738
+ : [];
2739
+ const recentVersions = versionList.slice(-5).map((version) => ({
2740
+ version,
2741
+ date: timeData[version] ? toDDMMYYYY(timeData[version]) : 'Unknown',
2742
+ }));
2743
+ const result = {
2744
+ name: packageData.name,
2745
+ version: packageData.version,
2746
+ description: packageData.description || '',
2747
+ license: packageData.license || 'Unknown',
2748
+ repository,
2749
+ size: humanizeBytes(packageData.dist?.unpackedSize || 0),
2750
+ created: timeData.created ? toDDMMYYYY(timeData.created) : 'Unknown',
2751
+ updated: timeData.modified ? toDDMMYYYY(timeData.modified) : 'Unknown',
2752
+ versions: recentVersions,
2753
+ stats: {
2754
+ total_versions: versionList.length,
2755
+ weekly_downloads: packageData.weeklyDownloads,
2756
+ },
2757
+ };
2758
+ // Add exports only if they exist and are useful
2759
+ if (exports && Object.keys(exports).length > 0) {
2760
+ result.exports = exports;
2051
2761
  }
2052
- // Add common branch names if not already included
2053
- const commonBranches = ['main', 'master', 'develop', 'dev'];
2054
- commonBranches.forEach(branch => {
2055
- if (!branches.includes(branch)) {
2056
- branches.push(branch);
2057
- }
2058
- });
2059
- return branches;
2060
- }
2061
-
2062
- const TOOL_NAME$1 = 'github_search_issues';
2063
- const DESCRIPTION$1 = `Find GitHub issues with rich metadata (labels, reactions, comments, state). Discover pain points, feature requests, and bug patterns with boolean logic and GitHub qualifiers.
2064
-
2065
- SEARCH PATTERNS:
2066
- Boolean: "bug AND crash", "feature OR enhancement", "error NOT test"
2067
- Exact phrases: "memory leak" (quoted)
2068
- GitHub qualifiers: "is:open label:bug author:username"
2069
- Combine with filters for precision`;
2070
- function registerSearchGitHubIssuesTool(server) {
2071
- server.tool(TOOL_NAME$1, DESCRIPTION$1, {
2072
- query: z
2073
- .string()
2074
- .min(1, 'Search query is required and cannot be empty')
2075
- .describe('Search query with GITHUB SEARCH SYNTAX support. BOOLEAN OPERATORS: "bug AND crash" (both required), "feature OR enhancement" (either term), "error NOT test" (excludes). EXACT PHRASES: "memory leak" (precise matching). GITHUB QUALIFIERS: "is:open label:bug author:username" (native GitHub syntax). COMBINED: Mix boolean logic with qualifiers for precise issue discovery.'),
2076
- owner: z
2077
- .string()
2078
- .min(1)
2079
- .optional()
2080
- .describe('Repository owner/organization. Leave empty for global search.'),
2081
- repo: z
2082
- .string()
2083
- .optional()
2084
- .describe('Repository name. Do exploratory search without repo filter first'),
2085
- app: z.string().optional().describe('Filter by GitHub App author'),
2086
- archived: z
2087
- .boolean()
2088
- .optional()
2089
- .describe('Filter by repository archived state'),
2090
- assignee: z.string().optional().describe('Filter by assignee'),
2091
- author: z.string().optional().describe('Filter by issue author'),
2092
- closed: z.string().optional().describe('Filter by closed date'),
2093
- commenter: z.string().optional().describe('Filter by user who commented'),
2094
- comments: z.number().optional().describe('Filter by number of comments'),
2095
- created: z
2096
- .string()
2097
- .optional()
2098
- .describe("Filter by created date (e.g., '>2022-01-01')"),
2099
- includePrs: z
2100
- .boolean()
2101
- .optional()
2102
- .describe('Include pull requests in results'),
2103
- interactions: z
2104
- .number()
2105
- .optional()
2106
- .describe('Filter by reactions and comments count'),
2107
- involves: z.string().optional().describe('Filter by user involvement'),
2108
- labels: z.string().optional().describe('Filter by labels'),
2109
- language: z.string().optional().describe('Filter by coding language'),
2110
- locked: z
2111
- .boolean()
2112
- .optional()
2113
- .describe('Filter by locked conversation status'),
2114
- match: z
2115
- .enum(['title', 'body', 'comments'])
2116
- .optional()
2117
- .describe('Restrict search to specific field'),
2118
- mentions: z.string().optional().describe('Filter by user mentions'),
2119
- milestone: z.string().optional().describe('Filter by milestone title'),
2120
- noAssignee: z.boolean().optional().describe('Filter by missing assignee'),
2121
- noLabel: z.boolean().optional().describe('Filter by missing label'),
2122
- noMilestone: z
2123
- .boolean()
2124
- .optional()
2125
- .describe('Filter by missing milestone'),
2126
- noProject: z.boolean().optional().describe('Filter by missing project'),
2127
- project: z.string().optional().describe('Filter by project board'),
2128
- reactions: z.number().optional().describe('Filter by reactions count'),
2129
- state: z
2130
- .enum(['open', 'closed'])
2131
- .optional()
2132
- .describe('Filter by issue state'),
2133
- teamMentions: z.string().optional().describe('Filter by team mentions'),
2134
- updated: z.string().optional().describe('Filter by last updated date'),
2135
- visibility: z
2136
- .enum(['public', 'private', 'internal'])
2137
- .optional()
2138
- .describe('Filter by repository visibility'),
2139
- sort: z
2140
- .enum([
2141
- 'comments',
2142
- 'created',
2143
- 'interactions',
2144
- 'reactions',
2145
- 'reactions-+1',
2146
- 'reactions--1',
2147
- 'reactions-heart',
2148
- 'reactions-smile',
2149
- 'reactions-tada',
2150
- 'reactions-thinking_face',
2151
- 'updated',
2152
- 'best-match',
2153
- ])
2154
- .optional()
2155
- .describe('Sort criteria'),
2156
- order: z
2157
- .enum(['asc', 'desc'])
2158
- .optional()
2159
- .default('desc')
2160
- .describe('Order (default: desc)'),
2161
- limit: z
2162
- .number()
2163
- .int()
2164
- .min(1)
2165
- .max(50)
2166
- .optional()
2167
- .default(25)
2168
- .describe('Maximum results (default: 25, max: 50)'),
2169
- }, {
2170
- title: TOOL_NAME$1,
2171
- description: DESCRIPTION$1,
2172
- readOnlyHint: true,
2173
- destructiveHint: false,
2174
- idempotentHint: true,
2175
- openWorldHint: true,
2176
- }, async (args) => {
2177
- if (!args.query?.trim()) {
2178
- return createErrorResult$1('Search query is required and cannot be empty - provide keywords to search for issues', new Error('Invalid query'));
2179
- }
2180
- if (args.query.length > 256) {
2181
- return createErrorResult$1('Search query is too long. Please limit to 256 characters or less - simplify your search terms', new Error('Query too long'));
2182
- }
2183
- try {
2184
- return await searchGitHubIssues(args);
2185
- }
2186
- catch (error) {
2187
- return createErrorResult$1('GitHub issues search failed - check repository exists and query is valid', error);
2188
- }
2189
- });
2190
- }
2191
- async function searchGitHubIssues(params) {
2192
- const cacheKey = generateCacheKey('gh-issues', params);
2193
- return withCache(cacheKey, async () => {
2194
- const { command, args } = buildGitHubIssuesAPICommand(params);
2195
- const result = await executeGitHubCommand(command, args, { cache: false });
2196
- if (result.isError) {
2197
- return result;
2198
- }
2199
- const execResult = JSON.parse(result.content[0].text);
2200
- const apiResponse = JSON.parse(execResult.result);
2201
- const issues = apiResponse.items || [];
2202
- const cleanIssues = issues.map((issue) => ({
2203
- number: issue.number,
2204
- title: issue.title,
2205
- state: issue.state,
2206
- author: issue.user?.login || '',
2207
- repository: issue.repository_url?.split('/').slice(-2).join('/') || 'unknown',
2208
- labels: issue.labels?.map(l => l.name) || [],
2209
- created_at: issue.created_at,
2210
- updated_at: issue.updated_at,
2211
- url: issue.html_url,
2212
- comments: issue.comments,
2213
- reactions: issue.reactions?.total_count || 0,
2214
- }));
2215
- const searchResult = {
2216
- searchType: 'issues',
2217
- query: params.query || '',
2218
- results: cleanIssues,
2219
- metadata: {
2220
- total_count: apiResponse.total_count || 0,
2221
- incomplete_results: apiResponse.incomplete_results || false,
2222
- },
2223
- };
2224
- return createSuccessResult$1(searchResult);
2225
- });
2762
+ return result;
2226
2763
  }
2227
- function buildGitHubIssuesAPICommand(params) {
2228
- const queryParts = [params.query?.trim() || ''];
2229
- // Repository/organization qualifiers
2230
- if (params.owner && params.repo) {
2231
- queryParts.push(`repo:${params.owner}/${params.repo}`);
2232
- }
2233
- else if (params.owner) {
2234
- queryParts.push(`org:${params.owner}`);
2764
+ /**
2765
+ * Simplify exports to show only essential entry points for code navigation
2766
+ */
2767
+ function simplifyExports(exports) {
2768
+ if (typeof exports === 'string') {
2769
+ return { main: exports };
2235
2770
  }
2236
- // Build search qualifiers from params
2237
- const qualifiers = {
2238
- author: params.author,
2239
- assignee: params.assignee,
2240
- mentions: params.mentions,
2241
- commenter: params.commenter,
2242
- involves: params.involves,
2243
- language: params.language,
2244
- state: params.state,
2245
- created: params.created,
2246
- updated: params.updated,
2247
- closed: params.closed,
2248
- };
2249
- Object.entries(qualifiers).forEach(([key, value]) => {
2250
- if (value)
2251
- queryParts.push(`${key}:${value}`);
2252
- });
2253
- // Special qualifiers
2254
- if (params.labels)
2255
- queryParts.push(`label:"${params.labels}"`);
2256
- if (params.milestone)
2257
- queryParts.push(`milestone:"${params.milestone}"`);
2258
- if (params.noAssignee)
2259
- queryParts.push('no:assignee');
2260
- if (params.noLabel)
2261
- queryParts.push('no:label');
2262
- if (params.noMilestone)
2263
- queryParts.push('no:milestone');
2264
- if (params.archived !== undefined)
2265
- queryParts.push(`archived:${params.archived}`);
2266
- if (params.locked)
2267
- queryParts.push('is:locked');
2268
- if (params.visibility)
2269
- queryParts.push(`is:${params.visibility}`);
2270
- const query = queryParts.filter(Boolean).join(' ');
2271
- const limit = Math.min(params.limit || 25, 100);
2272
- let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
2273
- if (params.sort)
2274
- apiPath += `&sort=${params.sort}`;
2275
- if (params.order)
2276
- apiPath += `&order=${params.order}`;
2277
- return { command: 'api', args: [apiPath] };
2278
- }
2279
-
2280
- const TOOL_NAME = 'npm_view_package';
2281
- const DESCRIPTION = `Get comprehensive NPM package metadata efficiently. Returns repository URL, exports, dependencies, and version history without needing GitHub searches. Essential for finding package source code and understanding project structure.`;
2282
- function registerNpmViewPackageTool(server) {
2283
- server.tool(TOOL_NAME, DESCRIPTION, {
2284
- packageName: z
2285
- .string()
2286
- .min(1, 'Package name is required')
2287
- .describe('NPM package name to analyze. Returns complete package context including exports (critical for GitHub file discovery), repository URL, dependencies, and version history.'),
2288
- }, {
2289
- title: TOOL_NAME,
2290
- description: DESCRIPTION,
2291
- readOnlyHint: true,
2292
- destructiveHint: false,
2293
- idempotentHint: true,
2294
- openWorldHint: true,
2295
- }, async (args) => {
2296
- try {
2297
- if (!args.packageName || args.packageName.trim() === '') {
2298
- return createResult('Package name is required - provide a valid NPM package name', true);
2771
+ if (typeof exports === 'object') {
2772
+ const simplified = {};
2773
+ // Extract main entry point
2774
+ if (exports['.']) {
2775
+ const mainExport = exports['.'];
2776
+ if (typeof mainExport === 'string') {
2777
+ simplified.main = mainExport;
2299
2778
  }
2300
- // Basic package name validation
2301
- if (!/^[a-z0-9@._/-]+$/.test(args.packageName)) {
2302
- return createResult('Invalid package name format - use standard NPM naming (e.g., "package-name" or "@scope/package")', true);
2779
+ else if (mainExport.default) {
2780
+ simplified.main = mainExport.default;
2781
+ }
2782
+ else if (mainExport.import) {
2783
+ simplified.main = mainExport.import;
2303
2784
  }
2304
- const result = await npmViewPackage(args.packageName);
2305
- return result;
2306
2785
  }
2307
- catch (error) {
2308
- return createResult('Failed to get package metadata - verify package exists on NPM registry', true);
2786
+ // Extract types if available with safe property access
2787
+ if (exports['./types'] ||
2788
+ (exports['.'] && typeof exports['.'] === 'object' && exports['.'].types)) {
2789
+ simplified.types = exports['./types'] || exports['.'].types;
2309
2790
  }
2310
- });
2311
- }
2312
- // Helper function to process versions
2313
- function processVersions(time) {
2314
- const semanticVersionRegex = /^\d+\.\d+\.\d+$/;
2315
- const versions = Object.entries(time || {})
2316
- .filter(([key]) => key !== 'created' && key !== 'modified')
2317
- .filter(([version]) => semanticVersionRegex.test(version))
2318
- .sort(([, a], [, b]) => new Date(b).getTime() - new Date(a).getTime());
2319
- return {
2320
- recent: versions
2321
- .slice(0, 10)
2322
- .map(([version, releaseDate]) => ({ version, releaseDate })),
2323
- stats: {
2324
- total: Object.keys(time || {}).length - 2, // exclude 'created' and 'modified'
2325
- official: versions.length,
2326
- },
2327
- };
2791
+ // Add a few other important exports (max 3 total)
2792
+ let count = 0;
2793
+ for (const [key, value] of Object.entries(exports)) {
2794
+ if (count >= 3 || key === '.' || key === './types')
2795
+ continue;
2796
+ if (key.includes('package.json') || key.includes('node_modules'))
2797
+ continue;
2798
+ simplified[key] =
2799
+ typeof value === 'object' ? value.default || value : value;
2800
+ count++;
2801
+ }
2802
+ return simplified;
2803
+ }
2804
+ return { main: 'index.js' };
2328
2805
  }
2329
- async function npmViewPackage(packageName) {
2330
- const cacheKey = generateCacheKey('npm-view-package', { packageName });
2806
+ async function viewNpmPackage(packageName) {
2807
+ const cacheKey = generateCacheKey('npm-view', { packageName });
2331
2808
  return withCache(cacheKey, async () => {
2332
2809
  try {
2333
2810
  const result = await executeNpmCommand('view', [packageName, '--json'], {
2334
- cache: true,
2811
+ cache: false,
2335
2812
  });
2336
- if (result.isError) {
2337
- return result;
2338
- }
2339
- // Parse the result from the executed command
2340
- const commandOutput = JSON.parse(result.content[0].text);
2341
- const npmData = JSON.parse(commandOutput.result);
2342
- // Process versions
2343
- const versionData = processVersions(npmData.time);
2344
- // Extract registry URL from tarball
2345
- const registryUrl = npmData.dist?.tarball?.match(/^(https?:\/\/[^/]+)/)?.[1] || '';
2346
- // Build result
2347
- const viewResult = {
2348
- name: npmData.name,
2349
- latest: npmData['dist-tags']?.latest || '',
2350
- license: npmData.license || '',
2351
- timeCreated: npmData.time?.created || '',
2352
- timeModified: npmData.time?.modified || '',
2353
- repositoryGitUrl: npmData.repository?.url || '',
2354
- registryUrl,
2355
- description: npmData.description || '',
2356
- size: npmData.dist?.unpackedSize || 0,
2357
- dependencies: npmData.dependencies || {},
2358
- devDependencies: npmData.devDependencies || {},
2359
- exports: npmData.exports || {},
2360
- versions: versionData.recent,
2361
- versionStats: versionData.stats,
2362
- };
2363
- return createSuccessResult$1(viewResult);
2813
+ return result;
2364
2814
  }
2365
2815
  catch (error) {
2366
- return createErrorResult$1('Failed to get npm package metadata - package may not exist or registry unavailable', error);
2816
+ const errorMessage = error.message || '';
2817
+ if (errorMessage.includes('404')) {
2818
+ return createResult({
2819
+ error: 'Package not found on NPM registry. Verify the exact package name',
2820
+ });
2821
+ }
2822
+ return createResult({
2823
+ error: 'Failed to execute NPM command. Check npm installation',
2824
+ });
2367
2825
  }
2368
2826
  });
2369
2827
  }
2370
2828
 
2829
+ const PROMPT_SYSTEM_PROMPT = `You are an expert code research assistant that knows how to understand users needs and search for the right information
2830
+ using gh cli for github and npm cli for packages.
2831
+
2832
+ CRITICAL SEARCH PRINCIPLES:
2833
+
2834
+ ## 1. PROGRESSIVE SEARCH STRATEGY (MOST IMPORTANT):
2835
+ a) START BROAD: Begin with simple, general terms (1-2 words max)
2836
+ b) ANALYZE RESULTS: Learn from what you find (repo names, owners, common patterns)
2837
+ c) REFINE GRADUALLY: Add filters only after understanding the landscape
2838
+ d) MULTIPLE ANGLES: Try different search terms if first approach yields no results
2839
+
2840
+ ## 2. SEARCH PROGRESSION EXAMPLES:
2841
+ - User asks about "React state management"
2842
+ 1st search: "state" or "redux" (BROAD)
2843
+ 2nd search: "react state" with language:javascript (REFINED)
2844
+ 3rd search: owner:facebook with specific terms (TARGETED)
2845
+
2846
+ - User asks about "authentication libraries"
2847
+ 1st search: "auth" or "authentication" (BROAD)
2848
+ 2nd search: Add language filter based on results
2849
+ 3rd search: Focus on specific owners/topics found
2850
+
2851
+ ## 3. HANDLING NO RESULTS:
2852
+ - NEVER give up after one failed search
2853
+ - Try progressively BROADER terms
2854
+ - Remove ALL filters and try core keywords
2855
+ - Search for related concepts (e.g., "auth" → "login" → "session")
2856
+ - Check different owners/organizations
2857
+ - For code search: try searching in popular repos first
2858
+
2859
+ ## 4. SMART FILTER USAGE:
2860
+ - NO FILTERS on first search (unless user specifies)
2861
+ - Add ONE filter at a time based on results
2862
+ - Common progression: query → +language → +stars → +owner
2863
+ - Reserve complex filters for final refinement
2864
+
2865
+ ## 5. TOOL SYNERGY:
2866
+ - Use repository search to find relevant repos FIRST
2867
+ - Then use code search within those repos
2868
+ - Check npm packages for JavaScript/TypeScript queries
2869
+ - Verify access with api_status_check for private repos
2870
+
2871
+ ## 6. RESEARCH BEST PRACTICES:
2872
+ - Conduct COMPREHENSIVE research with multiple searches
2873
+ - Learn from each search to improve the next
2874
+ - Provide context about your search strategy
2875
+ - Always verify technical details with actual code
2876
+
2877
+ ## 7. COMPLEX ANALYSIS PATTERNS:
2878
+
2879
+ ### Multi-Framework Comparison (e.g., React vs Vue):
2880
+ 1. Search repos separately: "react", then "vue"
2881
+ 2. Find core implementation files: "scheduler" in React, "reactivity" in Vue
2882
+ 3. Search specific features: "concurrent", "fiber", "proxy"
2883
+ 4. Compare similar functionalities across repos
2884
+
2885
+ ### Architecture Analysis (e.g., State Management):
2886
+ 1. Start broad: "state" or "store"
2887
+ 2. Identify major libraries: Redux, Zustand, Jotai
2888
+ 3. Search implementation patterns: "reducer", "atom", "selector"
2889
+ 4. Analyze each approach separately, then compare
2890
+
2891
+ ### Evolution Tracking (e.g., Feature History):
2892
+ 1. Use commit search with broad terms first
2893
+ 2. Narrow by date ranges progressively
2894
+ 3. Track changes in specific files over time
2895
+ 4. Identify key contributors and their patterns
2896
+
2897
+ ### Performance Analysis:
2898
+ 1. Search for benchmarks: "benchmark", "perf", "performance"
2899
+ 2. Look for optimization commits: "optimize", "faster", "improve"
2900
+ 3. Find profiling code: "profile", "measure", "timing"
2901
+ 4. Compare implementation strategies
2902
+
2903
+ ## 8. CROSS-REPOSITORY INTELLIGENCE:
2904
+ - When comparing frameworks, search EACH separately first
2905
+ - Build mental model of each codebase structure
2906
+ - Use discovered patterns to refine searches
2907
+ - Connect findings across repositories for insights
2908
+
2909
+ ## 9. TOOL-SPECIFIC BEST PRACTICES:
2910
+
2911
+ ### ${API_STATUS_CHECK_TOOL_NAME}:
2912
+ - Run FIRST when dealing with private repositories
2913
+ - Use organizations list to scope searches
2914
+ - Verify authentication before extensive searches
2915
+
2916
+ ### ${GITHUB_SEARCH_REPOSITORIES_TOOL_NAME}:
2917
+ - Start with topic/language, add stars/forks filters later
2918
+ - Use date ranges for trending analysis
2919
+ - Combine multiple searches for comprehensive discovery
2920
+
2921
+ ### ${GITHUB_SEARCH_CODE_TOOL_NAME}:
2922
+ - Begin with function/class names, not full signatures
2923
+ - Use extension filters for targeted searches
2924
+ - Try partial matches before exact phrases
2925
+
2926
+ ### ${GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME}:
2927
+ - Navigate from root, then drill down
2928
+ - Use for understanding project organization
2929
+ - Check common paths: src/, lib/, packages/
2930
+
2931
+ ### ${GITHUB_GET_FILE_CONTENT_TOOL_NAME}:
2932
+ - Verify file paths with repo structure first
2933
+ - Use for implementation details and documentation
2934
+ - Remember 300KB limit for large files
2935
+
2936
+ ### ${GITHUB_SEARCH_COMMITS_TOOL_NAME}:
2937
+ - Search by feature keywords, not commit hashes
2938
+ - Use author filter for contributor analysis
2939
+ - Date ranges help track feature evolution
2940
+
2941
+ ### ${GITHUB_SEARCH_ISSUES_TOOL_NAME} & ${GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME}:
2942
+ - Search for problem descriptions, not solutions
2943
+ - Use state filters progressively
2944
+ - Labels reveal project categorization
2945
+
2946
+ ### ${NPM_PACKAGE_SEARCH_TOOL_NAME}:
2947
+ - Use functional terms: "router", "validator", "parser"
2948
+ - Search multiple related terms in parallel
2949
+ - Aggregate results for comprehensive view
2950
+
2951
+ ### ${NPM_VIEW_PACKAGE_TOOL_NAME}:
2952
+ - Check repository field for source code access
2953
+ - Review exports for API understanding
2954
+ - Use version history to gauge stability
2955
+
2956
+ ## 10. CHAIN OF THOUGHT OPTIMIZATION:
2957
+ - Plan search sequence before executing
2958
+ - Document reasoning for each search refinement
2959
+ - Build knowledge progressively, don't jump to specifics
2960
+ - Validate findings with multiple sources
2961
+ `;
2962
+
2371
2963
  const SERVER_CONFIG = {
2372
2964
  name: 'octocode-mcp',
2373
2965
  version: '1.0.0',
2374
- description: `Comprehensive code analysis assistant: Deep exploration and understanding of complex implementations in GitHub repositories and npm packages.
2375
- Specialized in architectural analysis, algorithm explanations, and complete technical documentation.`,
2966
+ description: PROMPT_SYSTEM_PROMPT,
2376
2967
  };
2377
2968
  function registerAllTools(server) {
2378
2969
  const toolRegistrations = [
2379
- { name: 'ApiStatusCheck', fn: registerApiStatusCheckTool },
2380
- { name: 'GitHubSearchCode', fn: registerGitHubSearchCodeTool },
2970
+ { name: API_STATUS_CHECK_TOOL_NAME, fn: registerApiStatusCheckTool },
2971
+ { name: GITHUB_SEARCH_CODE_TOOL_NAME, fn: registerGitHubSearchCodeTool },
2381
2972
  {
2382
- name: 'FetchGitHubFileContent',
2973
+ name: GITHUB_GET_FILE_CONTENT_TOOL_NAME,
2383
2974
  fn: registerFetchGitHubFileContentTool,
2384
2975
  },
2385
- { name: 'SearchGitHubRepos', fn: registerSearchGitHubReposTool },
2386
- { name: 'SearchGitHubCommits', fn: registerSearchGitHubCommitsTool },
2387
2976
  {
2388
- name: 'SearchGitHubPullRequests',
2977
+ name: GITHUB_SEARCH_REPOSITORIES_TOOL_NAME,
2978
+ fn: registerSearchGitHubReposTool,
2979
+ },
2980
+ {
2981
+ name: GITHUB_SEARCH_COMMITS_TOOL_NAME,
2982
+ fn: registerGitHubSearchCommitsTool,
2983
+ },
2984
+ {
2985
+ name: GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME,
2389
2986
  fn: registerSearchGitHubPullRequestsTool,
2390
2987
  },
2391
- { name: 'NpmSearch', fn: registerNpmSearchTool },
2988
+ { name: NPM_PACKAGE_SEARCH_TOOL_NAME, fn: registerNpmSearchTool },
2392
2989
  {
2393
- name: 'ViewRepositoryStructure',
2990
+ name: GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME,
2394
2991
  fn: registerViewRepositoryStructureTool,
2395
2992
  },
2396
- { name: 'SearchGitHubIssues', fn: registerSearchGitHubIssuesTool },
2397
- { name: 'NpmViewPackage', fn: registerNpmViewPackageTool },
2993
+ {
2994
+ name: GITHUB_SEARCH_ISSUES_TOOL_NAME,
2995
+ fn: registerSearchGitHubIssuesTool,
2996
+ },
2997
+ { name: NPM_VIEW_PACKAGE_TOOL_NAME, fn: registerNpmViewPackageTool },
2398
2998
  ];
2999
+ logger.info(`Registering ${toolRegistrations.length} tools...`);
3000
+ let successCount = 0;
2399
3001
  for (const tool of toolRegistrations) {
2400
3002
  try {
3003
+ logger.debug(`Registering tool: ${tool.name}`);
2401
3004
  tool.fn(server);
3005
+ successCount++;
3006
+ logger.info(`✓ Successfully registered: ${tool.name}`);
2402
3007
  }
2403
3008
  catch (error) {
2404
- // ignore
3009
+ logger.error(`✗ Failed to register ${tool.name}:`, error);
3010
+ // Continue with other tools instead of failing completely
2405
3011
  }
2406
3012
  }
3013
+ if (successCount === 0) {
3014
+ throw new Error('No tools were successfully registered');
3015
+ }
3016
+ logger.info(`All tools registration completed - ${successCount}/${toolRegistrations.length} successful`);
2407
3017
  }
2408
3018
  async function startServer() {
2409
3019
  try {
2410
- const server = new McpServer(SERVER_CONFIG, {
2411
- capabilities: {
2412
- tools: {},
2413
- resources: {},
2414
- prompts: {},
2415
- },
2416
- instructions: `
2417
- ${PROMPT_SYSTEM_PROMPT}
2418
- `,
2419
- });
3020
+ logger.info('Creating MCP server...');
3021
+ const server = new McpServer(SERVER_CONFIG);
2420
3022
  registerAllTools(server);
2421
3023
  const transport = new StdioServerTransport();
2422
3024
  await server.connect(transport);
2423
- const gracefulShutdown = async (_signal) => {
3025
+ logger.info('=== Server Connected Successfully ===');
3026
+ // Ensure all buffered output is sent
3027
+ process.stdout.uncork();
3028
+ process.stderr.uncork();
3029
+ const gracefulShutdown = async (signal) => {
2424
3030
  try {
2425
- await server.close();
3031
+ logger.info(`Received ${signal}, shutting down gracefully...`);
3032
+ clearAllCache();
3033
+ // Give server time to close properly
3034
+ await Promise.race([
3035
+ server.close(),
3036
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Server close timeout')), 5000)),
3037
+ ]);
2426
3038
  process.exit(0);
2427
3039
  }
2428
3040
  catch (error) {
3041
+ logger.error('Error during shutdown:', error);
2429
3042
  process.exit(1);
2430
3043
  }
2431
3044
  };
3045
+ // Handle process signals
2432
3046
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
2433
3047
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3048
+ // Handle stdin close (important for MCP)
2434
3049
  process.stdin.on('close', async () => {
2435
3050
  await gracefulShutdown('STDIN_CLOSE');
2436
3051
  });
2437
- process.on('uncaughtException', () => {
3052
+ // Handle uncaught errors
3053
+ process.on('uncaughtException', error => {
3054
+ logger.error('Uncaught exception:', error);
2438
3055
  gracefulShutdown('UNCAUGHT_EXCEPTION');
2439
3056
  });
2440
- process.on('unhandledRejection', () => {
3057
+ process.on('unhandledRejection', (reason, promise) => {
3058
+ logger.error('Unhandled rejection at:', promise, 'reason:', reason);
2441
3059
  gracefulShutdown('UNHANDLED_REJECTION');
2442
3060
  });
3061
+ // Keep process alive
3062
+ process.stdin.resume();
2443
3063
  }
2444
3064
  catch (error) {
3065
+ logger.error('Error details:', error);
2445
3066
  process.exit(1);
2446
3067
  }
2447
3068
  }
2448
- startServer().catch(() => {
3069
+ startServer().catch(error => {
3070
+ logger.error('Error:', error);
2449
3071
  process.exit(1);
2450
3072
  });