octocode-mcp 2.3.5 → 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.
- package/build/index.js +1836 -2048
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -8,44 +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
11
|
const VERSION = 'v1';
|
|
46
12
|
const cache = new NodeCache({
|
|
47
13
|
stdTTL: 86400, // 24 hour cache
|
|
48
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
|
|
49
17
|
});
|
|
50
18
|
function generateCacheKey(prefix, params) {
|
|
51
19
|
const paramString = JSON.stringify(params, Object.keys(params).sort());
|
|
@@ -66,6 +34,9 @@ async function withCache(cacheKey, operation) {
|
|
|
66
34
|
}
|
|
67
35
|
return result;
|
|
68
36
|
}
|
|
37
|
+
function clearAllCache() {
|
|
38
|
+
cache.flushAll();
|
|
39
|
+
}
|
|
69
40
|
|
|
70
41
|
const safeExecAsync = promisify(exec);
|
|
71
42
|
// Allowed command prefixes - this prevents shell injection by restricting to safe commands
|
|
@@ -85,10 +56,9 @@ function createSuccessResult(data) {
|
|
|
85
56
|
};
|
|
86
57
|
}
|
|
87
58
|
function createErrorResult(message, error) {
|
|
59
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
88
60
|
return {
|
|
89
|
-
content: [
|
|
90
|
-
{ type: 'text', text: `${message}: ${error.message}` },
|
|
91
|
-
],
|
|
61
|
+
content: [{ type: 'text', text: `${message}: ${errorMessage}` }],
|
|
92
62
|
isError: true,
|
|
93
63
|
};
|
|
94
64
|
}
|
|
@@ -128,23 +98,31 @@ function getShellConfig(preferredWindowsShell) {
|
|
|
128
98
|
};
|
|
129
99
|
}
|
|
130
100
|
/**
|
|
131
|
-
*
|
|
101
|
+
* Escape shell arguments with improved GitHub CLI query handling
|
|
132
102
|
*/
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
return (/\b(AND|OR|NOT)\b/.test(query) ||
|
|
136
|
-
/\s+(AND|OR|NOT)\s+/.test(query) ||
|
|
137
|
-
/"[^"]*\s+(AND|OR|NOT)\s+[^"]*"/.test(query));
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Escape shell arguments with improved GitHub CLI boolean query handling
|
|
141
|
-
*/
|
|
142
|
-
function escapeShellArg(arg, shellType, isGitHubQuery) {
|
|
103
|
+
function escapeShellArg(arg, shellType, isGitHubQuery // Flag to indicate if this is the main GitHub search query argument
|
|
104
|
+
) {
|
|
143
105
|
// Auto-detect shell type if not provided
|
|
144
106
|
if (!shellType) {
|
|
145
107
|
const isWindows = platform() === 'win32';
|
|
146
108
|
shellType = isWindows ? 'cmd' : 'unix';
|
|
147
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
|
+
}
|
|
148
126
|
switch (shellType) {
|
|
149
127
|
case 'powershell':
|
|
150
128
|
return escapePowerShellArg(arg);
|
|
@@ -182,25 +160,17 @@ function escapeWindowsCmdArg(arg) {
|
|
|
182
160
|
* Escape arguments for Unix shells with special handling for GitHub CLI queries
|
|
183
161
|
*/
|
|
184
162
|
function escapeUnixShellArg(arg, isGitHubQuery) {
|
|
185
|
-
//
|
|
186
|
-
if (isGitHubQuery &&
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
/\s/.test(arg) &&
|
|
194
|
-
!arg.startsWith('"') &&
|
|
195
|
-
!arg.endsWith('"')) {
|
|
196
|
-
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
197
|
-
}
|
|
198
|
-
// For already quoted GitHub queries, pass through with escaped internal quotes
|
|
199
|
-
if (isGitHubQuery && arg.startsWith('"') && arg.endsWith('"')) {
|
|
200
|
-
return arg.replace(/\\"/g, '\\\\"'); // Escape already escaped quotes
|
|
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}"`;
|
|
201
171
|
}
|
|
202
172
|
// Standard Unix shell escaping for other arguments
|
|
203
|
-
if (/[
|
|
173
|
+
if (/[^a-zA-Z0-9\-_./=@:]/.test(arg)) {
|
|
204
174
|
return `'${arg.replace(/'/g, "'\"'\"'")}'`;
|
|
205
175
|
}
|
|
206
176
|
return arg;
|
|
@@ -241,12 +211,21 @@ async function executeGitHubCommand(command, args = [], options = {}) {
|
|
|
241
211
|
// Get shell configuration
|
|
242
212
|
const shellConfig = getShellConfig(options.windowsShell);
|
|
243
213
|
// Build command with validated prefix and properly escaped arguments
|
|
244
|
-
//
|
|
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
|
|
245
216
|
const escapedArgs = args.map((arg, index) => {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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);
|
|
250
229
|
});
|
|
251
230
|
const fullCommand = `gh ${command} ${escapedArgs.join(' ')}`;
|
|
252
231
|
const executeGhCommand = () => executeCommand(fullCommand, 'github', options, shellConfig);
|
|
@@ -307,9 +286,17 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
|
|
|
307
286
|
const errorType = type === 'npm' ? 'NPM command error' : 'GitHub CLI command error';
|
|
308
287
|
return createErrorResult(errorType, new Error(stderr));
|
|
309
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
|
+
}
|
|
310
297
|
return createSuccessResult({
|
|
311
298
|
command: fullCommand,
|
|
312
|
-
result:
|
|
299
|
+
result: parsedResult,
|
|
313
300
|
timestamp: new Date().toISOString(),
|
|
314
301
|
type,
|
|
315
302
|
platform: platform(),
|
|
@@ -326,37 +313,91 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
|
|
|
326
313
|
}
|
|
327
314
|
}
|
|
328
315
|
|
|
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
|
+
|
|
329
371
|
function createResult(options) {
|
|
330
|
-
const { data, error
|
|
372
|
+
const { data, error } = options;
|
|
331
373
|
if (error) {
|
|
332
374
|
const errorMessage = typeof error === 'string'
|
|
333
375
|
? error
|
|
334
376
|
: error.message || 'Unknown error';
|
|
335
|
-
|
|
336
|
-
const errorResponse = { error: errorMessage };
|
|
337
|
-
if (suggestions) {
|
|
338
|
-
errorResponse.suggestions = suggestions;
|
|
339
|
-
}
|
|
340
|
-
if (cli_command) {
|
|
341
|
-
errorResponse.cli_command = cli_command;
|
|
342
|
-
}
|
|
377
|
+
const errorResponse = errorMessage;
|
|
343
378
|
return {
|
|
344
|
-
content: [{ type: 'text', text:
|
|
379
|
+
content: [{ type: 'text', text: errorResponse }],
|
|
345
380
|
isError: true,
|
|
346
381
|
};
|
|
347
382
|
}
|
|
348
|
-
|
|
349
|
-
if (cli_command && data && typeof data === 'object') {
|
|
350
|
-
const dataWithCli = { ...data, cli_command };
|
|
383
|
+
try {
|
|
351
384
|
return {
|
|
352
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
385
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
353
386
|
isError: false,
|
|
354
387
|
};
|
|
355
388
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
}
|
|
360
401
|
}
|
|
361
402
|
/**
|
|
362
403
|
* Convert ISO timestamp to DDMMYYYY format
|
|
@@ -417,10 +458,149 @@ function optimizeTextMatch(fragment, maxLength = 100) {
|
|
|
417
458
|
return truncated + '…';
|
|
418
459
|
}
|
|
419
460
|
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
}
|
|
422
602
|
function registerApiStatusCheckTool(server) {
|
|
423
|
-
server.registerTool(
|
|
603
|
+
server.registerTool(API_STATUS_CHECK_TOOL_NAME, {
|
|
424
604
|
description: DESCRIPTION$9,
|
|
425
605
|
inputSchema: {},
|
|
426
606
|
annotations: {
|
|
@@ -440,32 +620,22 @@ function registerApiStatusCheckTool(server) {
|
|
|
440
620
|
try {
|
|
441
621
|
const authResult = await executeGitHubCommand('auth', ['status']);
|
|
442
622
|
if (!authResult.isError) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
// JSON parsing error - this is unexpected, propagate it
|
|
449
|
-
throw new Error(`GitHub auth response JSON parsing failed: ${parseError.message}`);
|
|
450
|
-
}
|
|
451
|
-
const isAuthenticated = authData.result?.includes('Logged in') ||
|
|
452
|
-
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;
|
|
453
628
|
if (isAuthenticated) {
|
|
454
629
|
githubConnected = true;
|
|
455
630
|
// Get user organizations using direct GitHub CLI command
|
|
456
631
|
try {
|
|
457
632
|
const orgsResult = await executeGitHubCommand('org', ['list', '--limit=50'], { cache: false });
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// JSON parsing error for organizations - treat as no orgs available
|
|
465
|
-
execResult = { result: '' };
|
|
466
|
-
}
|
|
467
|
-
const output = execResult.result;
|
|
468
|
-
// 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') {
|
|
469
639
|
organizations = output
|
|
470
640
|
.split('\n')
|
|
471
641
|
.map((org) => org.trim())
|
|
@@ -500,23 +670,12 @@ function registerApiStatusCheckTool(server) {
|
|
|
500
670
|
if (!npmResult.isError) {
|
|
501
671
|
npmConnected = true;
|
|
502
672
|
// Get registry info
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
catch (parseError) {
|
|
511
|
-
// JSON parsing error for registry - use default
|
|
512
|
-
registryData = { result: 'https://registry.npmjs.org/' };
|
|
513
|
-
}
|
|
514
|
-
registry = registryData.result.trim();
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
registry = 'https://registry.npmjs.org/'; // default fallback
|
|
519
|
-
}
|
|
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
|
|
520
679
|
}
|
|
521
680
|
}
|
|
522
681
|
catch (error) {
|
|
@@ -529,120 +688,66 @@ function registerApiStatusCheckTool(server) {
|
|
|
529
688
|
}
|
|
530
689
|
npmConnected = false;
|
|
531
690
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
691
|
+
return createResult({
|
|
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
|
+
],
|
|
542
705
|
},
|
|
543
|
-
hints: [
|
|
544
|
-
'use user organizations: to search on private repositories in case the user asked about private repo - check by query nd structure',
|
|
545
|
-
],
|
|
546
706
|
},
|
|
547
|
-
};
|
|
548
|
-
return createResult({ data: loginStatus });
|
|
707
|
+
});
|
|
549
708
|
}
|
|
550
709
|
catch (error) {
|
|
551
710
|
return createResult({
|
|
552
|
-
error:
|
|
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.`,
|
|
553
712
|
});
|
|
554
713
|
}
|
|
555
714
|
});
|
|
556
715
|
}
|
|
557
716
|
|
|
558
|
-
const
|
|
559
|
-
const DESCRIPTION$8 = `
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
- "useState OR useEffect" - Find either hook
|
|
563
|
-
- "useState AND useEffect" - Find both hooks together
|
|
564
|
-
- "authentication AND (jwt OR oauth)" - Complex logic combinations
|
|
565
|
-
- "NOT deprecated" - Exclude deprecated code
|
|
566
|
-
|
|
567
|
-
EMBEDDED QUALIFIERS:
|
|
568
|
-
- "useState language:javascript filename:*.jsx" - Hook in React files
|
|
569
|
-
- "authentication language:python path:*/security/*" - Security code in Python
|
|
570
|
-
- "docker OR kubernetes language:yaml extension:yml" - Container configs
|
|
571
|
-
|
|
572
|
-
TRADITIONAL FILTERS (ALSO SUPPORTED):
|
|
573
|
-
- language: "javascript", owner: "microsoft", filename: "package.json"
|
|
574
|
-
|
|
575
|
-
PROVEN PATTERNS: "authentication" → +language → +owner → +filename
|
|
576
|
-
KEY TIPS: language filter = 90% speed boost, boolean operators work perfectly with filters`;
|
|
577
|
-
function registerGitHubSearchCodeTool(server) {
|
|
578
|
-
server.registerTool(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, {
|
|
579
721
|
description: DESCRIPTION$8,
|
|
580
722
|
inputSchema: {
|
|
581
|
-
query: z
|
|
582
|
-
.string()
|
|
583
|
-
.min(1)
|
|
584
|
-
.describe(`Search query with GitHub syntax. BOOLEAN LOGIC: "useState OR useEffect", "authentication AND jwt", "NOT deprecated". EMBEDDED QUALIFIERS: "useState language:javascript", "docker path:*/config/*". EXACT PHRASES: "error handling".
|
|
585
|
-
|
|
586
|
-
POWERFUL EXAMPLES: "useState OR useEffect language:javascript", "authentication AND (jwt OR oauth)", "docker OR kubernetes language:yaml", "NOT deprecated language:python"
|
|
587
|
-
RULES: Boolean operators MUST be uppercase (AND, OR, NOT). Combines perfectly with traditional filters.`),
|
|
588
|
-
language: z
|
|
589
|
-
.string()
|
|
590
|
-
.optional()
|
|
591
|
-
.describe(`MOST EFFECTIVE FILTER - 90% speed boost! Essential for popular languages.
|
|
592
|
-
|
|
593
|
-
POPULAR: javascript, typescript, python, java, go, rust, php, ruby, swift, kotlin, dart
|
|
594
|
-
SYSTEMS: c, cpp, assembly, shell, dockerfile, yaml`),
|
|
595
723
|
owner: z
|
|
596
|
-
.union([z.string(), z.array(z.string())])
|
|
597
|
-
.optional()
|
|
598
|
-
.describe(`HIGH IMPACT - Reduces search space by 95%+
|
|
599
|
-
|
|
600
|
-
EXAMPLES: "microsoft", "google", "facebook" or ["microsoft", "google"]
|
|
601
|
-
POPULAR: microsoft, google, facebook, amazon, apache, hashicorp, kubernetes`),
|
|
602
|
-
filename: z
|
|
603
724
|
.string()
|
|
604
|
-
.
|
|
605
|
-
.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
STRATEGY: filename:package.json + "react typescript"`),
|
|
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')`),
|
|
609
729
|
repo: z
|
|
610
|
-
.union([z.string(), z.array(z.string())])
|
|
611
|
-
.optional()
|
|
612
|
-
.describe(`PRECISE TARGETING for specific repositories
|
|
613
|
-
|
|
614
|
-
FORMAT: "facebook/react", "microsoft/vscode" or ["facebook/react", "vuejs/vue"]
|
|
615
|
-
USE: Deep dive analysis of specific projects`),
|
|
616
|
-
extension: z
|
|
617
|
-
.string()
|
|
618
|
-
.optional()
|
|
619
|
-
.describe(`FILE TYPE PRECISION - More specific than language filter
|
|
620
|
-
|
|
621
|
-
POPULAR: js, ts, jsx, tsx, py, java, go, rs, rb, php, cs, sh, yml, json, md
|
|
622
|
-
USE: extension:tsx (React TypeScript only), extension:dockerfile`),
|
|
623
|
-
match: z
|
|
624
|
-
.union([z.enum(['file', 'path']), z.array(z.enum(['file', 'path']))])
|
|
625
|
-
.optional()
|
|
626
|
-
.describe(`SEARCH SCOPE: "file" (content), "path" (filenames), ["file", "path"] (both)
|
|
627
|
-
|
|
628
|
-
EXAMPLES: match:"path" + "test" (find test files), match:"file" + "useState"`),
|
|
629
|
-
size: z
|
|
630
730
|
.string()
|
|
631
|
-
.optional()
|
|
632
|
-
.describe(`FILE SIZE FILTER: ">100" (>100KB), "<50" (<50KB), "10..100" (range)
|
|
633
|
-
|
|
634
|
-
STRATEGY: "<200" (avoid huge files), ">20" (substantial code), "<10" (configs)`),
|
|
635
|
-
limit: z
|
|
636
|
-
.number()
|
|
637
|
-
.int()
|
|
638
731
|
.min(1)
|
|
639
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()
|
|
640
743
|
.optional()
|
|
641
|
-
.default(
|
|
642
|
-
.
|
|
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.'),
|
|
643
748
|
},
|
|
644
749
|
annotations: {
|
|
645
|
-
title: 'GitHub
|
|
750
|
+
title: 'GitHub Repository Explorer',
|
|
646
751
|
readOnlyHint: true,
|
|
647
752
|
destructiveHint: false,
|
|
648
753
|
idempotentHint: true,
|
|
@@ -650,604 +755,170 @@ STRATEGY: "<200" (avoid huge files), ">20" (substantial code), "<10" (configs)`)
|
|
|
650
755
|
},
|
|
651
756
|
}, async (args) => {
|
|
652
757
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (validationError) {
|
|
656
|
-
return createResult({ error: validationError });
|
|
657
|
-
}
|
|
658
|
-
const result = await searchGitHubCode(args);
|
|
659
|
-
if (result.isError) {
|
|
660
|
-
return result;
|
|
661
|
-
}
|
|
662
|
-
const execResult = JSON.parse(result.content[0].text);
|
|
663
|
-
const codeResults = JSON.parse(execResult.result);
|
|
664
|
-
// GitHub CLI returns a direct array, not an object with total_count and items
|
|
665
|
-
const items = Array.isArray(codeResults) ? codeResults : [];
|
|
666
|
-
// Smart handling for no results - provide actionable suggestions
|
|
667
|
-
if (items.length === 0) {
|
|
668
|
-
return handleNoResults(args, execResult.command);
|
|
669
|
-
}
|
|
670
|
-
// Transform to optimized format
|
|
671
|
-
const optimizedResult = transformToOptimizedFormat$1(items, args);
|
|
672
|
-
return createResult({ data: optimizedResult });
|
|
758
|
+
const result = await viewRepositoryStructure(args);
|
|
759
|
+
return result;
|
|
673
760
|
}
|
|
674
761
|
catch (error) {
|
|
675
|
-
const errorMessage = error.message
|
|
676
|
-
// Handle JSON parsing errors
|
|
677
|
-
if (errorMessage.includes('JSON')) {
|
|
678
|
-
return createResult({
|
|
679
|
-
error: 'GitHub CLI returned invalid response - check if GitHub CLI is up to date with "gh version" and try again',
|
|
680
|
-
});
|
|
681
|
-
}
|
|
762
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
682
763
|
return createResult({
|
|
683
|
-
error:
|
|
684
|
-
suggestions: [
|
|
685
|
-
'Try simpler queries without NOT operators',
|
|
686
|
-
'Use filters (language, owner, filename) instead of complex boolean',
|
|
687
|
-
'Check authentication with api_status_check',
|
|
688
|
-
],
|
|
764
|
+
error: `Failed to explore repository. ${errorMessage}. Verify repository exists and is accessible`,
|
|
689
765
|
});
|
|
690
766
|
}
|
|
691
767
|
});
|
|
692
768
|
}
|
|
693
769
|
/**
|
|
694
|
-
*
|
|
770
|
+
* Views the structure of a GitHub repository at a specific path.
|
|
771
|
+
* Optimized for code analysis workflows with smart defaults and clear errors.
|
|
695
772
|
*/
|
|
696
|
-
function
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
:
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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
|
+
});
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
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;
|
|
845
|
+
}
|
|
846
|
+
return a.name.localeCompare(b.name);
|
|
847
|
+
});
|
|
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
|
+
},
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
return createResult({
|
|
881
|
+
error: `Failed to access repository. ${error}. Verify repository name and authentication`,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
727
884
|
});
|
|
728
885
|
}
|
|
729
886
|
/**
|
|
730
|
-
*
|
|
887
|
+
* Smart branch detection with automatic fallback to common branch names.
|
|
731
888
|
*/
|
|
732
|
-
function
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (params.query.includes(' AND ') || params.query.includes(' OR ')) {
|
|
739
|
-
suggestions.push('Try simpler queries without boolean operators');
|
|
740
|
-
}
|
|
741
|
-
if (params.query.length > 50) {
|
|
742
|
-
suggestions.push('Simplify query - shorter terms often work better');
|
|
743
|
-
}
|
|
744
|
-
// Filter-specific suggestions
|
|
745
|
-
if (!params.language) {
|
|
746
|
-
suggestions.push('Add language filter - this improves results by 90%');
|
|
747
|
-
}
|
|
748
|
-
if (params.language && !params.owner && !params.filename) {
|
|
749
|
-
suggestions.push('Add owner filter to target specific organizations');
|
|
750
|
-
}
|
|
751
|
-
if (params.owner && params.repo) {
|
|
752
|
-
suggestions.push('Remove repo filter and search across all repos in the organization');
|
|
753
|
-
}
|
|
754
|
-
if (params.filename) {
|
|
755
|
-
suggestions.push('Remove filename filter to search all file types');
|
|
756
|
-
}
|
|
757
|
-
if (params.extension && params.language) {
|
|
758
|
-
suggestions.push('Try removing extension filter (language filter might be sufficient)');
|
|
759
|
-
}
|
|
760
|
-
// Always provide generic fallbacks
|
|
761
|
-
if (suggestions.length === 0) {
|
|
762
|
-
suggestions.push('Try a broader search with fewer filters');
|
|
763
|
-
suggestions.push('Use more common/general terms');
|
|
764
|
-
suggestions.push('Check for typos in query or filter values');
|
|
765
|
-
}
|
|
766
|
-
return suggestions.slice(0, 5); // Limit to top 5 suggestions
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* Generate fallback queries that are likely to succeed
|
|
770
|
-
*/
|
|
771
|
-
function generateFallbackQueries(params) {
|
|
772
|
-
const fallbacks = [];
|
|
773
|
-
// Extract key terms from original query
|
|
774
|
-
const keyTerms = extractKeyTerms(params.query);
|
|
775
|
-
// Fallback 1: Simplify query, keep language if present
|
|
776
|
-
if (keyTerms.length > 1) {
|
|
777
|
-
const simplifiedQuery = keyTerms[0];
|
|
778
|
-
fallbacks.push({
|
|
779
|
-
query: simplifiedQuery,
|
|
780
|
-
description: `Search for just "${simplifiedQuery}"${params.language ? ` in ${params.language}` : ''}`,
|
|
781
|
-
rationale: 'Single term searches often yield better results',
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
// Fallback 2: Remove filters but keep core query
|
|
785
|
-
if (params.language || params.owner || params.filename || params.extension) {
|
|
786
|
-
fallbacks.push({
|
|
787
|
-
query: keyTerms.join(' '),
|
|
788
|
-
description: `Search "${keyTerms.join(' ')}" without filters`,
|
|
789
|
-
rationale: 'Removes restrictive filters for broader results',
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
// Fallback 3: Language-specific popular search
|
|
793
|
-
if (params.language) {
|
|
794
|
-
const popularTerms = getPopularTermsForLanguage(params.language);
|
|
795
|
-
if (popularTerms.length > 0) {
|
|
796
|
-
fallbacks.push({
|
|
797
|
-
query: popularTerms[0],
|
|
798
|
-
description: `Popular ${params.language} pattern: "${popularTerms[0]}"`,
|
|
799
|
-
rationale: `Common patterns in ${params.language} development`,
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
// Fallback 4: Broader category search
|
|
804
|
-
const category = inferCategory(params.query);
|
|
805
|
-
if (category) {
|
|
806
|
-
fallbacks.push({
|
|
807
|
-
query: category,
|
|
808
|
-
description: `Broader search: "${category}"`,
|
|
809
|
-
rationale: 'Searches the general category for related patterns',
|
|
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,
|
|
810
895
|
});
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
function extractKeyTerms(query) {
|
|
818
|
-
// Remove quotes, boolean operators, and special characters
|
|
819
|
-
const cleaned = query
|
|
820
|
-
.replace(/["']/g, '')
|
|
821
|
-
.replace(/\b(AND|OR|NOT)\b/gi, '')
|
|
822
|
-
.replace(/[^\w\s]/g, ' ')
|
|
823
|
-
.replace(/\s+/g, ' ')
|
|
824
|
-
.trim();
|
|
825
|
-
return cleaned.split(' ').filter(term => term.length > 2);
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Get popular search terms for specific programming languages
|
|
829
|
-
*/
|
|
830
|
-
function getPopularTermsForLanguage(language) {
|
|
831
|
-
const popularTerms = {
|
|
832
|
-
javascript: [
|
|
833
|
-
'useState',
|
|
834
|
-
'async await',
|
|
835
|
-
'fetch api',
|
|
836
|
-
'event listener',
|
|
837
|
-
'promise',
|
|
838
|
-
],
|
|
839
|
-
typescript: [
|
|
840
|
-
'interface',
|
|
841
|
-
'generic type',
|
|
842
|
-
'type guard',
|
|
843
|
-
'enum',
|
|
844
|
-
'namespace',
|
|
845
|
-
],
|
|
846
|
-
python: ['def function', 'import', 'class method', 'lambda', 'decorator'],
|
|
847
|
-
java: [
|
|
848
|
-
'public class',
|
|
849
|
-
'static method',
|
|
850
|
-
'interface',
|
|
851
|
-
'annotation',
|
|
852
|
-
'spring',
|
|
853
|
-
],
|
|
854
|
-
go: ['func main', 'interface', 'goroutine', 'channel', 'struct'],
|
|
855
|
-
rust: ['fn main', 'impl', 'trait', 'enum', 'match'],
|
|
856
|
-
cpp: ['class', 'template', 'namespace', 'virtual', 'std::'],
|
|
857
|
-
c: ['int main', 'struct', 'malloc', 'pointer', 'header'],
|
|
858
|
-
php: ['function', 'class', 'namespace', 'interface', 'trait'],
|
|
859
|
-
ruby: ['def', 'class', 'module', 'block', 'gem'],
|
|
860
|
-
swift: ['func', 'class', 'protocol', 'extension', 'guard'],
|
|
861
|
-
kotlin: ['fun', 'class', 'interface', 'data class', 'coroutine'],
|
|
862
|
-
dart: ['class', 'function', 'widget', 'async', 'future'],
|
|
863
|
-
shell: ['function', 'if then', 'for loop', 'variable', 'script'],
|
|
864
|
-
yaml: ['version', 'name', 'build', 'deploy', 'config'],
|
|
865
|
-
dockerfile: ['FROM', 'RUN', 'COPY', 'EXPOSE', 'CMD'],
|
|
866
|
-
};
|
|
867
|
-
return popularTerms[language.toLowerCase()] || [];
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Infer search category from query terms
|
|
871
|
-
*/
|
|
872
|
-
function inferCategory(query) {
|
|
873
|
-
const categories = {
|
|
874
|
-
authentication: [
|
|
875
|
-
'auth',
|
|
876
|
-
'login',
|
|
877
|
-
'oauth',
|
|
878
|
-
'jwt',
|
|
879
|
-
'token',
|
|
880
|
-
'passport',
|
|
881
|
-
'session',
|
|
882
|
-
],
|
|
883
|
-
database: ['db', 'sql', 'query', 'model', 'schema', 'migration', 'orm'],
|
|
884
|
-
api: ['rest', 'graphql', 'endpoint', 'route', 'request', 'response'],
|
|
885
|
-
testing: ['test', 'spec', 'mock', 'assert', 'unit', 'integration'],
|
|
886
|
-
config: ['config', 'setting', 'env', 'environment', 'setup'],
|
|
887
|
-
ui: ['component', 'button', 'form', 'modal', 'layout', 'style'],
|
|
888
|
-
error: ['error', 'exception', 'try', 'catch', 'throw', 'handle'],
|
|
889
|
-
performance: ['cache', 'optimization', 'memory', 'speed', 'performance'],
|
|
890
|
-
security: ['security', 'encrypt', 'hash', 'validate', 'sanitize'],
|
|
891
|
-
};
|
|
892
|
-
const queryLower = query.toLowerCase();
|
|
893
|
-
for (const [category, keywords] of Object.entries(categories)) {
|
|
894
|
-
if (keywords.some(keyword => queryLower.includes(keyword))) {
|
|
895
|
-
return category;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* Transform GitHub CLI response to optimized format with enhanced metadata
|
|
902
|
-
*/
|
|
903
|
-
function transformToOptimizedFormat$1(items, params) {
|
|
904
|
-
const searchEfficiency = calculateSearchEfficiency(params);
|
|
905
|
-
// Extract repository info if single repo search
|
|
906
|
-
const singleRepo = extractSingleRepository$1(items);
|
|
907
|
-
const optimizedItems = items.map(item => ({
|
|
908
|
-
path: item.path,
|
|
909
|
-
matches: item.textMatches.map(match => ({
|
|
910
|
-
context: optimizeTextMatch(match.fragment, 120), // Increased context for better understanding
|
|
911
|
-
positions: match.matches.map(m => m.indices),
|
|
912
|
-
})),
|
|
913
|
-
url: singleRepo ? item.path : simplifyGitHubUrl(item.url),
|
|
914
|
-
}));
|
|
915
|
-
const result = {
|
|
916
|
-
items: optimizedItems,
|
|
917
|
-
total_count: items.length,
|
|
918
|
-
};
|
|
919
|
-
// Add repository info if single repo
|
|
920
|
-
if (singleRepo) {
|
|
921
|
-
result.repository = {
|
|
922
|
-
name: singleRepo.nameWithOwner,
|
|
923
|
-
url: simplifyRepoUrl(singleRepo.url),
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
// Enhanced metadata with search efficiency and tips
|
|
927
|
-
result.metadata = {
|
|
928
|
-
has_filters: !!(params.language ||
|
|
929
|
-
params.owner ||
|
|
930
|
-
params.filename ||
|
|
931
|
-
params.extension),
|
|
932
|
-
search_scope: params.match
|
|
933
|
-
? Array.isArray(params.match)
|
|
934
|
-
? params.match.join(',')
|
|
935
|
-
: params.match
|
|
936
|
-
: 'file',
|
|
937
|
-
search_efficiency: searchEfficiency,
|
|
938
|
-
};
|
|
939
|
-
// Add performance tips for low-efficiency searches
|
|
940
|
-
if (searchEfficiency.score < 7) {
|
|
941
|
-
result.metadata.performance_tips = generatePerformanceTips(params);
|
|
942
|
-
}
|
|
943
|
-
return result;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Calculate search efficiency score and provide insights
|
|
947
|
-
*/
|
|
948
|
-
function calculateSearchEfficiency(params) {
|
|
949
|
-
let score = 5; // Base score
|
|
950
|
-
const factors = [];
|
|
951
|
-
const recommendations = [];
|
|
952
|
-
// Language filter is huge efficiency boost
|
|
953
|
-
if (params.language) {
|
|
954
|
-
score += 3;
|
|
955
|
-
factors.push('Language filter (+3)');
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
recommendations.push('Add language filter for 90% performance boost');
|
|
959
|
-
}
|
|
960
|
-
// Owner filter provides good targeting
|
|
961
|
-
if (params.owner) {
|
|
962
|
-
score += 2;
|
|
963
|
-
factors.push('Owner filter (+2)');
|
|
964
|
-
}
|
|
965
|
-
// Filename filter is very efficient
|
|
966
|
-
if (params.filename) {
|
|
967
|
-
score += 2;
|
|
968
|
-
factors.push('Filename filter (+2)');
|
|
969
|
-
}
|
|
970
|
-
// Extension filter adds precision
|
|
971
|
-
if (params.extension) {
|
|
972
|
-
score += 1;
|
|
973
|
-
factors.push('Extension filter (+1)');
|
|
974
|
-
}
|
|
975
|
-
// Repository filter is highly targeted
|
|
976
|
-
if (params.repo) {
|
|
977
|
-
score += 2;
|
|
978
|
-
factors.push('Repository filter (+2)');
|
|
979
|
-
}
|
|
980
|
-
// Size filter helps avoid huge files
|
|
981
|
-
if (params.size) {
|
|
982
|
-
score += 1;
|
|
983
|
-
factors.push('Size filter (+1)');
|
|
984
|
-
}
|
|
985
|
-
// Boolean operators are now properly supported
|
|
986
|
-
if (params.query.includes(' OR ')) {
|
|
987
|
-
// OR is powerful when used correctly
|
|
988
|
-
if (params.language || params.owner) {
|
|
989
|
-
score += 1;
|
|
990
|
-
factors.push('OR with filters (+1)');
|
|
991
|
-
}
|
|
992
|
-
else {
|
|
993
|
-
factors.push('OR operator (neutral)');
|
|
994
|
-
recommendations.push('Add language or owner filter with OR for better targeting');
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
if (params.query.includes(' AND ')) {
|
|
998
|
-
score += 1;
|
|
999
|
-
factors.push('AND operator (+1)');
|
|
1000
|
-
}
|
|
1001
|
-
if (params.query.includes(' NOT ')) {
|
|
1002
|
-
// NOT can be useful but less efficient
|
|
1003
|
-
factors.push('NOT operator (neutral)');
|
|
1004
|
-
recommendations.push('Consider positive filters alongside NOT for better results');
|
|
1005
|
-
}
|
|
1006
|
-
if (params.query.length > 100) {
|
|
1007
|
-
score -= 1;
|
|
1008
|
-
factors.push('Long query (-1)');
|
|
1009
|
-
recommendations.push('Simplify query for better results');
|
|
1010
|
-
}
|
|
1011
|
-
return {
|
|
1012
|
-
score: Math.max(1, Math.min(10, score)),
|
|
1013
|
-
factors,
|
|
1014
|
-
recommendations,
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
|
-
* Generate performance tips for inefficient searches
|
|
1019
|
-
*/
|
|
1020
|
-
function generatePerformanceTips(params) {
|
|
1021
|
-
const tips = [];
|
|
1022
|
-
if (!params.language) {
|
|
1023
|
-
tips.push('Add language filter - single biggest performance boost');
|
|
1024
|
-
}
|
|
1025
|
-
if (!params.owner && !params.repo) {
|
|
1026
|
-
tips.push('Add owner filter to target specific organizations');
|
|
1027
|
-
}
|
|
1028
|
-
if (params.query.includes(' OR ') && !params.language && !params.owner) {
|
|
1029
|
-
tips.push('Add language or owner filter with OR for better targeting');
|
|
1030
|
-
}
|
|
1031
|
-
if (params.query.includes(' NOT ')) {
|
|
1032
|
-
tips.push('Combine NOT with positive filters (language, filename, etc.) for best results');
|
|
1033
|
-
}
|
|
1034
|
-
if (!params.filename && !params.extension) {
|
|
1035
|
-
tips.push('Add filename or extension filter for file-type precision');
|
|
1036
|
-
}
|
|
1037
|
-
return tips.slice(0, 3);
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Extract single repository if all results are from same repo
|
|
1041
|
-
*/
|
|
1042
|
-
function extractSingleRepository$1(items) {
|
|
1043
|
-
if (items.length === 0)
|
|
1044
|
-
return null;
|
|
1045
|
-
const firstRepo = items[0].repository;
|
|
1046
|
-
const allSameRepo = items.every(item => item.repository.nameWithOwner === firstRepo.nameWithOwner);
|
|
1047
|
-
return allSameRepo ? firstRepo : null;
|
|
1048
|
-
}
|
|
1049
|
-
/**
|
|
1050
|
-
* Build command line arguments for GitHub CLI with improved parameter handling
|
|
1051
|
-
* Ensures exact string search capability with proper quote and escape handling
|
|
1052
|
-
*/
|
|
1053
|
-
function buildGitHubCliArgs(params) {
|
|
1054
|
-
const args = ['code'];
|
|
1055
|
-
// Handle exact string search - preserve quotes and special characters
|
|
1056
|
-
const searchQuery = params.query;
|
|
1057
|
-
// For exact string searches (quoted strings), preserve the quotes
|
|
1058
|
-
// For special characters and escape sequences, pass them through
|
|
1059
|
-
// GitHub CLI will handle the proper escaping to the GitHub API
|
|
1060
|
-
if (searchQuery.includes('"') || searchQuery.includes("'")) {
|
|
1061
|
-
// Already has quotes - exact string search
|
|
1062
|
-
args.push(searchQuery);
|
|
1063
|
-
}
|
|
1064
|
-
else if (searchQuery.includes('\\') ||
|
|
1065
|
-
/[<>{}[\]()&|;$`!*?~]/.test(searchQuery)) {
|
|
1066
|
-
// Contains special characters that might need exact matching
|
|
1067
|
-
// Let GitHub CLI handle the escaping - don't double-escape
|
|
1068
|
-
args.push(searchQuery);
|
|
1069
|
-
}
|
|
1070
|
-
else {
|
|
1071
|
-
// Regular search query
|
|
1072
|
-
args.push(searchQuery);
|
|
1073
|
-
}
|
|
1074
|
-
// Add filters in order of effectiveness for better CLI performance
|
|
1075
|
-
if (params.language) {
|
|
1076
|
-
args.push(`--language=${params.language}`);
|
|
1077
|
-
}
|
|
1078
|
-
if (params.filename) {
|
|
1079
|
-
args.push(`--filename=${params.filename}`);
|
|
1080
|
-
}
|
|
1081
|
-
if (params.extension) {
|
|
1082
|
-
args.push(`--extension=${params.extension}`);
|
|
1083
|
-
}
|
|
1084
|
-
if (params.size) {
|
|
1085
|
-
args.push(`--size=${params.size}`);
|
|
1086
|
-
}
|
|
1087
|
-
// Always add limit
|
|
1088
|
-
if (params.limit) {
|
|
1089
|
-
args.push(`--limit=${params.limit}`);
|
|
1090
|
-
}
|
|
1091
|
-
// Handle match parameter
|
|
1092
|
-
if (params.match) {
|
|
1093
|
-
const matchValues = Array.isArray(params.match)
|
|
1094
|
-
? params.match
|
|
1095
|
-
: [params.match];
|
|
1096
|
-
// Use the first match type when multiple are provided
|
|
1097
|
-
const matchValue = matchValues[0];
|
|
1098
|
-
args.push(`--match=${matchValue}`);
|
|
1099
|
-
}
|
|
1100
|
-
// Handle owner parameter - can be string or array
|
|
1101
|
-
if (params.owner && !params.repo) {
|
|
1102
|
-
const ownerValues = Array.isArray(params.owner)
|
|
1103
|
-
? params.owner
|
|
1104
|
-
: [params.owner];
|
|
1105
|
-
ownerValues.forEach(owner => args.push(`--owner=${owner}`));
|
|
1106
|
-
}
|
|
1107
|
-
// Handle repository filters with improved validation
|
|
1108
|
-
if (params.repo) {
|
|
1109
|
-
const repos = Array.isArray(params.repo) ? params.repo : [params.repo];
|
|
1110
|
-
if (params.owner) {
|
|
1111
|
-
const owners = Array.isArray(params.owner)
|
|
1112
|
-
? params.owner
|
|
1113
|
-
: [params.owner];
|
|
1114
|
-
// Create repo filters for each owner/repo combination
|
|
1115
|
-
owners.forEach(owner => {
|
|
1116
|
-
repos.forEach(repo => {
|
|
1117
|
-
// Handle both "owner/repo" format and just "repo" format
|
|
1118
|
-
if (repo.includes('/')) {
|
|
1119
|
-
args.push(`--repo=${repo}`);
|
|
1120
|
-
}
|
|
1121
|
-
else {
|
|
1122
|
-
args.push(`--repo=${owner}/${repo}`);
|
|
1123
|
-
}
|
|
1124
|
-
});
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
else {
|
|
1128
|
-
// Handle repo without owner (must be in owner/repo format)
|
|
1129
|
-
repos.forEach(repo => {
|
|
1130
|
-
args.push(`--repo=${repo}`);
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
// JSON output with all available fields
|
|
1135
|
-
args.push('--json=repository,path,textMatches,sha,url');
|
|
1136
|
-
return args;
|
|
1137
|
-
}
|
|
1138
|
-
async function searchGitHubCode(params) {
|
|
1139
|
-
const cacheKey = generateCacheKey('gh-code', params);
|
|
1140
|
-
return withCache(cacheKey, async () => {
|
|
1141
|
-
try {
|
|
1142
|
-
const args = buildGitHubCliArgs(params);
|
|
1143
|
-
const result = await executeGitHubCommand('search', args, {
|
|
1144
|
-
cache: false,
|
|
1145
|
-
});
|
|
1146
|
-
return result;
|
|
1147
|
-
}
|
|
1148
|
-
catch (error) {
|
|
1149
|
-
const errorMessage = error.message || '';
|
|
1150
|
-
// Parse specific GitHub CLI error types
|
|
1151
|
-
if (errorMessage.includes('authentication')) {
|
|
1152
|
-
return createResult({
|
|
1153
|
-
error: 'GitHub authentication required - run api_status_check tool',
|
|
1154
|
-
});
|
|
1155
|
-
}
|
|
1156
|
-
if (errorMessage.includes('rate limit')) {
|
|
1157
|
-
return createResult({
|
|
1158
|
-
error: 'GitHub rate limit exceeded - use more specific filters or wait',
|
|
1159
|
-
suggestions: [
|
|
1160
|
-
'Add language filter to reduce search scope',
|
|
1161
|
-
'Use owner filter to target specific organizations',
|
|
1162
|
-
'Add filename filter for precision targeting',
|
|
1163
|
-
'Wait a few minutes and try again',
|
|
1164
|
-
],
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
if (errorMessage.includes('validation failed') ||
|
|
1168
|
-
errorMessage.includes('Invalid query')) {
|
|
1169
|
-
return createResult({
|
|
1170
|
-
error: 'Invalid query syntax. GitHub legacy search has limitations.',
|
|
1171
|
-
suggestions: [
|
|
1172
|
-
'Remove NOT operators (use positive filters instead)',
|
|
1173
|
-
'Simplify OR logic to separate searches',
|
|
1174
|
-
'Use quotes for exact phrases: "error handling"',
|
|
1175
|
-
'Try: language:python + "function definition" instead of complex boolean',
|
|
1176
|
-
],
|
|
1177
|
-
});
|
|
1178
|
-
}
|
|
1179
|
-
if (errorMessage.includes('repository not found') ||
|
|
1180
|
-
errorMessage.includes('owner not found')) {
|
|
1181
|
-
return createResult({
|
|
1182
|
-
error: 'Repository or owner not found',
|
|
1183
|
-
suggestions: [
|
|
1184
|
-
'Verify exact owner/repository names (case-sensitive)',
|
|
1185
|
-
'Check if repository is private and you have access',
|
|
1186
|
-
'Remove repo filter to search across all repositories',
|
|
1187
|
-
'Use owner filter instead of specific repo for broader search',
|
|
1188
|
-
],
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
if (errorMessage.includes('timeout')) {
|
|
1192
|
-
return createResult({
|
|
1193
|
-
error: 'Search timeout - query too broad',
|
|
1194
|
-
suggestions: [
|
|
1195
|
-
'Add language filter to narrow scope',
|
|
1196
|
-
'Use owner or repo filters for targeting',
|
|
1197
|
-
'Simplify query terms',
|
|
1198
|
-
'Add filename filter for specific file types',
|
|
1199
|
-
],
|
|
1200
|
-
});
|
|
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);
|
|
1201
902
|
}
|
|
1202
|
-
// Generic fallback with helpful guidance
|
|
1203
|
-
return createResult({
|
|
1204
|
-
error: 'Code search failed',
|
|
1205
|
-
suggestions: [
|
|
1206
|
-
'Check GitHub CLI authentication with: gh auth status',
|
|
1207
|
-
'Simplify query and add language filter',
|
|
1208
|
-
'Try owner filter for targeted search',
|
|
1209
|
-
'Use api_status_check tool to verify setup',
|
|
1210
|
-
],
|
|
1211
|
-
});
|
|
1212
903
|
}
|
|
1213
|
-
});
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Enhanced validation with helpful suggestions
|
|
1217
|
-
* Supports exact string search with quotes and special characters
|
|
1218
|
-
*/
|
|
1219
|
-
function validateSearchParameters(params) {
|
|
1220
|
-
// Query validation
|
|
1221
|
-
if (!params.query.trim()) {
|
|
1222
|
-
return 'Empty query. Try: "useState", "authentication", "docker setup", or use filters like language:python';
|
|
1223
904
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
905
|
+
catch {
|
|
906
|
+
// If we can't get repo info, proceed with standard fallbacks
|
|
1226
907
|
}
|
|
1227
|
-
//
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
return 'Repository format error. Use "owner/repo" format like "facebook/react" or provide both owner and repo parameters.';
|
|
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);
|
|
1233
913
|
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Remove overly restrictive validation for escape characters
|
|
1237
|
-
// GitHub CLI and API can handle special characters properly
|
|
1238
|
-
// Boolean operator validation with suggestions
|
|
1239
|
-
const invalidBooleans = params.query.match(/\b(and|or|not)\b/g);
|
|
1240
|
-
if (invalidBooleans) {
|
|
1241
|
-
const corrected = invalidBooleans.map(op => op.toUpperCase()).join(', ');
|
|
1242
|
-
return `Boolean operators must be uppercase: ${corrected}. Example: "react OR vue" not "react or vue"`;
|
|
1243
|
-
}
|
|
1244
|
-
return null; // No validation errors
|
|
914
|
+
});
|
|
915
|
+
return branches;
|
|
1245
916
|
}
|
|
1246
917
|
|
|
1247
|
-
const
|
|
1248
|
-
const DESCRIPTION$7 = `
|
|
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).`;
|
|
1249
920
|
function registerFetchGitHubFileContentTool(server) {
|
|
1250
|
-
server.registerTool(
|
|
921
|
+
server.registerTool(GITHUB_GET_FILE_CONTENT_TOOL_NAME, {
|
|
1251
922
|
description: DESCRIPTION$7,
|
|
1252
923
|
inputSchema: {
|
|
1253
924
|
owner: z
|
|
@@ -1255,26 +926,26 @@ function registerFetchGitHubFileContentTool(server) {
|
|
|
1255
926
|
.min(1)
|
|
1256
927
|
.max(100)
|
|
1257
928
|
.regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/)
|
|
1258
|
-
.describe(`Repository owner/
|
|
929
|
+
.describe(`Repository owner/org name (e.g., 'microsoft', 'google', NOT 'microsoft/vscode')`),
|
|
1259
930
|
repo: z
|
|
1260
931
|
.string()
|
|
1261
932
|
.min(1)
|
|
1262
933
|
.max(100)
|
|
1263
934
|
.regex(/^[a-zA-Z0-9._-]+$/)
|
|
1264
|
-
.describe(`Repository name.
|
|
935
|
+
.describe(`Repository name only (e.g., 'vscode', 'react', NOT 'microsoft/vscode')`),
|
|
1265
936
|
branch: z
|
|
1266
937
|
.string()
|
|
1267
938
|
.min(1)
|
|
1268
939
|
.max(255)
|
|
1269
940
|
.regex(/^[^\s]+$/)
|
|
1270
|
-
.describe(`Branch name.
|
|
941
|
+
.describe(`Branch name. Falls back to main/master if not found`),
|
|
1271
942
|
filePath: z
|
|
1272
943
|
.string()
|
|
1273
944
|
.min(1)
|
|
1274
|
-
.describe(`
|
|
945
|
+
.describe(`Exact file path from repo root (e.g., src/index.js, README.md)`),
|
|
1275
946
|
},
|
|
1276
947
|
annotations: {
|
|
1277
|
-
title: 'GitHub File Content
|
|
948
|
+
title: 'GitHub File Content - Direct Access',
|
|
1278
949
|
readOnlyHint: true,
|
|
1279
950
|
destructiveHint: false,
|
|
1280
951
|
idempotentHint: true,
|
|
@@ -1287,7 +958,7 @@ function registerFetchGitHubFileContentTool(server) {
|
|
|
1287
958
|
}
|
|
1288
959
|
catch (error) {
|
|
1289
960
|
return createResult({
|
|
1290
|
-
error: '
|
|
961
|
+
error: 'Failed to fetch file. Verify path with github_get_contents first',
|
|
1291
962
|
});
|
|
1292
963
|
}
|
|
1293
964
|
});
|
|
@@ -1322,27 +993,23 @@ async function fetchGitHubFileContent(params) {
|
|
|
1322
993
|
// Handle common errors
|
|
1323
994
|
if (errorMsg.includes('404')) {
|
|
1324
995
|
return createResult({
|
|
1325
|
-
error: 'File not found
|
|
1326
|
-
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
996
|
+
error: 'File not found. Use github_view_repo_structure to explore repository structure',
|
|
1327
997
|
});
|
|
1328
998
|
}
|
|
1329
999
|
else if (errorMsg.includes('403')) {
|
|
1330
1000
|
return createResult({
|
|
1331
|
-
error: 'Access denied
|
|
1332
|
-
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1001
|
+
error: 'Access denied. Repository may be private - use apiStatusCheck to verify',
|
|
1333
1002
|
});
|
|
1334
1003
|
}
|
|
1335
1004
|
else if (errorMsg.includes('maxBuffer') ||
|
|
1336
1005
|
errorMsg.includes('stdout maxBuffer length exceeded')) {
|
|
1337
1006
|
return createResult({
|
|
1338
|
-
error: 'File too large (>300KB)
|
|
1339
|
-
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1007
|
+
error: 'File too large (>300KB). Use githubSearchCode to search for patterns within the file',
|
|
1340
1008
|
});
|
|
1341
1009
|
}
|
|
1342
1010
|
else {
|
|
1343
1011
|
return createResult({
|
|
1344
|
-
error: '
|
|
1345
|
-
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1012
|
+
error: 'Failed to fetch file. Verify repository name and file path',
|
|
1346
1013
|
});
|
|
1347
1014
|
}
|
|
1348
1015
|
}
|
|
@@ -1354,11 +1021,11 @@ async function fetchGitHubFileContent(params) {
|
|
|
1354
1021
|
if (errorMessage.includes('maxBuffer') ||
|
|
1355
1022
|
errorMessage.includes('stdout maxBuffer length exceeded')) {
|
|
1356
1023
|
return createResult({
|
|
1357
|
-
error: 'File too large (>300KB)
|
|
1024
|
+
error: 'File too large (>300KB). Use github_search_code to search for patterns within the file',
|
|
1358
1025
|
});
|
|
1359
1026
|
}
|
|
1360
1027
|
return createResult({
|
|
1361
|
-
error: 'Unexpected error
|
|
1028
|
+
error: 'Unexpected error. Check network connection and try again',
|
|
1362
1029
|
});
|
|
1363
1030
|
}
|
|
1364
1031
|
});
|
|
@@ -1366,28 +1033,33 @@ async function fetchGitHubFileContent(params) {
|
|
|
1366
1033
|
async function processFileContent(result, owner, repo, branch, filePath) {
|
|
1367
1034
|
// Extract the actual content from the exec result
|
|
1368
1035
|
const execResult = JSON.parse(result.content[0].text);
|
|
1369
|
-
const fileData =
|
|
1036
|
+
const fileData = execResult.result;
|
|
1370
1037
|
// Check if it's a directory
|
|
1371
1038
|
if (Array.isArray(fileData)) {
|
|
1372
1039
|
return createResult({
|
|
1373
|
-
error: 'Path is directory
|
|
1040
|
+
error: 'Path is a directory. Use github_view_repo_structure to list directory contents',
|
|
1374
1041
|
});
|
|
1375
1042
|
}
|
|
1376
|
-
const fileSize = fileData.size
|
|
1043
|
+
const fileSize = typeof fileData.size === 'number' ? fileData.size : 0;
|
|
1377
1044
|
const MAX_FILE_SIZE = 300 * 1024; // 300KB limit for better performance and reliability
|
|
1378
1045
|
// Check file size with helpful message
|
|
1379
1046
|
if (fileSize > MAX_FILE_SIZE) {
|
|
1380
1047
|
const fileSizeKB = Math.round(fileSize / 1024);
|
|
1381
1048
|
const maxSizeKB = Math.round(MAX_FILE_SIZE / 1024);
|
|
1382
1049
|
return createResult({
|
|
1383
|
-
error: `File too large (${fileSizeKB}KB > ${maxSizeKB}KB)
|
|
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',
|
|
1384
1057
|
});
|
|
1385
1058
|
}
|
|
1386
|
-
//
|
|
1387
|
-
const base64Content = fileData.content?.replace(/\s/g, ''); // Remove all whitespace
|
|
1059
|
+
const base64Content = fileData.content.replace(/\s/g, ''); // Remove all whitespace
|
|
1388
1060
|
if (!base64Content) {
|
|
1389
1061
|
return createResult({
|
|
1390
|
-
error: '
|
|
1062
|
+
error: 'File is empty - no content to display',
|
|
1391
1063
|
});
|
|
1392
1064
|
}
|
|
1393
1065
|
let decodedContent;
|
|
@@ -1396,215 +1068,61 @@ async function processFileContent(result, owner, repo, branch, filePath) {
|
|
|
1396
1068
|
// Simple binary check - look for null bytes
|
|
1397
1069
|
if (buffer.indexOf(0) !== -1) {
|
|
1398
1070
|
return createResult({
|
|
1399
|
-
error: 'Binary file detected
|
|
1071
|
+
error: 'Binary file detected. Cannot display as text - download directly from GitHub',
|
|
1400
1072
|
});
|
|
1401
1073
|
}
|
|
1402
1074
|
decodedContent = buffer.toString('utf-8');
|
|
1403
1075
|
}
|
|
1404
1076
|
catch (decodeError) {
|
|
1405
1077
|
return createResult({
|
|
1406
|
-
error: '
|
|
1078
|
+
error: 'Failed to decode file. Encoding may not be supported (expected UTF-8)',
|
|
1407
1079
|
});
|
|
1408
1080
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
metadata: {
|
|
1417
|
-
size: fileSize,
|
|
1418
|
-
lines: decodedContent.split('\n').length,
|
|
1419
|
-
encoding: 'utf-8',
|
|
1081
|
+
return createResult({
|
|
1082
|
+
data: {
|
|
1083
|
+
filePath,
|
|
1084
|
+
owner,
|
|
1085
|
+
repo,
|
|
1086
|
+
branch,
|
|
1087
|
+
content: decodedContent,
|
|
1420
1088
|
},
|
|
1421
|
-
};
|
|
1422
|
-
return createResult({ data: response });
|
|
1089
|
+
});
|
|
1423
1090
|
}
|
|
1424
1091
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
*
|
|
1430
|
-
* 1. Quality Discovery:
|
|
1431
|
-
* { topic: ["react", "typescript"], stars: "1000..5000", limit: 10 }
|
|
1432
|
-
*
|
|
1433
|
-
* 2. Organization Research:
|
|
1434
|
-
* { owner: ["microsoft", "google"], language: "python", limit: 10 }
|
|
1435
|
-
*
|
|
1436
|
-
* 3. Beginner Projects:
|
|
1437
|
-
* { goodFirstIssues: ">=5", stars: "100..5000", limit: 10 }
|
|
1438
|
-
*
|
|
1439
|
-
* 4. Recent Quality:
|
|
1440
|
-
* { stars: ">1000", created: ">2023-01-01", limit: 10 }
|
|
1441
|
-
*
|
|
1442
|
-
* AVOID: OR queries + language filter, 5+ filters, multi-word OR
|
|
1443
|
-
* TIP: Use limit parameter instead of adding more filters
|
|
1444
|
-
*/
|
|
1445
|
-
const TOOL_NAME$6 = 'github_search_repositories';
|
|
1446
|
-
const DESCRIPTION$6 = `Search GitHub repositories with powerful GitHub search syntax and advanced filtering.
|
|
1447
|
-
|
|
1448
|
-
EMBEDDED QUALIFIERS (MOST POWERFUL):
|
|
1449
|
-
- "vue OR react stars:>1000 language:javascript" - OR logic with constraints
|
|
1450
|
-
- "typescript AND framework stars:100..5000" - AND logic with star range
|
|
1451
|
-
- "repo:facebook/react OR repo:vuejs/vue" - Multiple specific repositories
|
|
1452
|
-
- "org:microsoft OR org:google language:typescript" - Multiple organizations
|
|
1453
|
-
- "topic:react topic:typescript stars:>500" - Multiple topics with quality filter
|
|
1454
|
-
|
|
1455
|
-
TRADITIONAL FILTERS (ALSO SUPPORTED):
|
|
1456
|
-
- owner: ["microsoft", "google"] - Multiple owners as array
|
|
1457
|
-
- topic: ["react", "typescript"] - Multiple topics as array
|
|
1458
|
-
- stars: "1000..5000" - Range or threshold filtering
|
|
1459
|
-
|
|
1460
|
-
BEST PRACTICES:
|
|
1461
|
-
- Use embedded qualifiers for complex queries with OR/AND logic
|
|
1462
|
-
- Use traditional filters for simple, clean parameter-based searches
|
|
1463
|
-
- Combine both approaches: "vue OR react" + language:"javascript" + stars:">1000"`;
|
|
1464
|
-
/**
|
|
1465
|
-
* Extract owner/repo information from various query formats
|
|
1466
|
-
*/
|
|
1467
|
-
function extractOwnerRepoFromQuery(query) {
|
|
1468
|
-
let cleanedQuery = query;
|
|
1469
|
-
let extractedOwner;
|
|
1470
|
-
let extractedRepo;
|
|
1471
|
-
// Pattern 1: GitHub URLs (https://github.com/owner/repo)
|
|
1472
|
-
const githubUrlMatch = query.match(/github\.com\/([^\\s]+)\/([^\\s]+)/i);
|
|
1473
|
-
if (githubUrlMatch) {
|
|
1474
|
-
extractedOwner = githubUrlMatch[1];
|
|
1475
|
-
extractedRepo = githubUrlMatch[2];
|
|
1476
|
-
cleanedQuery = query
|
|
1477
|
-
.replace(/https?:\/\/github\.com\/[^\\s]+\/[^\\s]+/gi, '')
|
|
1478
|
-
.trim();
|
|
1479
|
-
}
|
|
1480
|
-
// Pattern 2: owner/repo format in query
|
|
1481
|
-
const ownerRepoMatch = query.match(/\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/);
|
|
1482
|
-
if (ownerRepoMatch && !extractedOwner) {
|
|
1483
|
-
extractedOwner = ownerRepoMatch[1];
|
|
1484
|
-
extractedRepo = ownerRepoMatch[2];
|
|
1485
|
-
cleanedQuery = query.replace(ownerRepoMatch[0], '').trim();
|
|
1486
|
-
}
|
|
1487
|
-
// Pattern 3: NPM package-like references (@scope/package)
|
|
1488
|
-
const scopedPackageMatch = query.match(/@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])/);
|
|
1489
|
-
if (scopedPackageMatch && !extractedOwner) {
|
|
1490
|
-
extractedOwner = scopedPackageMatch[1];
|
|
1491
|
-
extractedRepo = scopedPackageMatch[2];
|
|
1492
|
-
cleanedQuery = query.replace(scopedPackageMatch[0], '').trim();
|
|
1493
|
-
}
|
|
1494
|
-
return {
|
|
1495
|
-
extractedOwner,
|
|
1496
|
-
extractedRepo,
|
|
1497
|
-
cleanedQuery: cleanedQuery || query,
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
function registerSearchGitHubReposTool(server) {
|
|
1501
|
-
server.registerTool(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, {
|
|
1502
1096
|
description: DESCRIPTION$6,
|
|
1503
1097
|
inputSchema: {
|
|
1504
1098
|
query: z
|
|
1505
1099
|
.string()
|
|
1506
|
-
.
|
|
1507
|
-
.describe('Search
|
|
1508
|
-
// CORE FILTERS (GitHub CLI flags)
|
|
1509
|
-
owner: z
|
|
1510
|
-
.union([z.string(), z.array(z.string())])
|
|
1511
|
-
.optional()
|
|
1512
|
-
.describe('Repository owner/organization. HIGHLY EFFECTIVE as array ["microsoft", "google"]. FIXED: Now supports multiple owners with comma separation. Best for targeted research.'),
|
|
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.'),
|
|
1513
1102
|
language: z
|
|
1514
1103
|
.string()
|
|
1515
1104
|
.optional()
|
|
1516
|
-
.describe('
|
|
1517
|
-
|
|
1518
|
-
.union([
|
|
1519
|
-
z.number().int().min(0),
|
|
1520
|
-
z
|
|
1521
|
-
.string()
|
|
1522
|
-
.regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid stars format. Use: number, ">100", ">=50", "<200", "<=100", or "10..100"'),
|
|
1523
|
-
])
|
|
1524
|
-
.optional()
|
|
1525
|
-
.describe('Stars filter. MOST EFFECTIVE: ranges "1000..5000", thresholds ">1000". Excellent for quality filtering.'),
|
|
1526
|
-
topic: z
|
|
1527
|
-
.union([z.string(), z.array(z.string())])
|
|
1528
|
-
.optional()
|
|
1529
|
-
.describe('Topics filter. BEST PATTERN: arrays ["react", "typescript"]. FIXED: Now supports comma-separated topics. Preferred over OR queries. Combines well with stars.'),
|
|
1530
|
-
forks: z.number().optional().describe('Number of forks filter.'),
|
|
1531
|
-
// UPDATED: Match CLI parameter name exactly
|
|
1532
|
-
numberOfTopics: z
|
|
1533
|
-
.number()
|
|
1534
|
-
.optional()
|
|
1535
|
-
.describe('Filter by number of topics (indicates documentation quality).'),
|
|
1536
|
-
// QUALITY & STATE FILTERS
|
|
1537
|
-
license: z
|
|
1105
|
+
.describe('Language filter (javascript, python, etc). Use only when needed.'),
|
|
1106
|
+
owner: z
|
|
1538
1107
|
.union([z.string(), z.array(z.string())])
|
|
1539
1108
|
.optional()
|
|
1540
|
-
.describe('
|
|
1541
|
-
|
|
1542
|
-
.
|
|
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()
|
|
1543
1112
|
.optional()
|
|
1544
|
-
.describe('
|
|
1545
|
-
|
|
1546
|
-
.
|
|
1113
|
+
.describe('Specific filename to search. Use for targeted searches.'),
|
|
1114
|
+
extension: z
|
|
1115
|
+
.string()
|
|
1547
1116
|
.optional()
|
|
1548
|
-
.describe('
|
|
1549
|
-
|
|
1550
|
-
.enum(['
|
|
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']))])
|
|
1551
1120
|
.optional()
|
|
1552
|
-
.describe('
|
|
1553
|
-
|
|
1554
|
-
created: z
|
|
1121
|
+
.describe('Search scope: "file" for content, "path" for filenames. Default: file content.'),
|
|
1122
|
+
size: z
|
|
1555
1123
|
.string()
|
|
1556
1124
|
.optional()
|
|
1557
|
-
.describe('
|
|
1558
|
-
updated: z
|
|
1559
|
-
.string()
|
|
1560
|
-
.optional()
|
|
1561
|
-
.describe('Updated date filter. Good for finding active projects.'),
|
|
1562
|
-
size: z
|
|
1563
|
-
.string()
|
|
1564
|
-
.optional()
|
|
1565
|
-
.describe('Repository size filter in KB. Format: ">1000", "<500".'),
|
|
1566
|
-
// COMMUNITY FILTERS - Match CLI parameter names exactly
|
|
1567
|
-
goodFirstIssues: z
|
|
1568
|
-
.union([
|
|
1569
|
-
z.number().int().min(0),
|
|
1570
|
-
z
|
|
1571
|
-
.string()
|
|
1572
|
-
.regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
|
|
1573
|
-
])
|
|
1574
|
-
.optional()
|
|
1575
|
-
.describe('Good first issues count. WORKING: Filter for beginner-friendly projects. EXCELLENT when combined with stars "100..5000" for quality beginner projects.'),
|
|
1576
|
-
helpWantedIssues: z
|
|
1577
|
-
.union([
|
|
1578
|
-
z.number().int().min(0),
|
|
1579
|
-
z
|
|
1580
|
-
.string()
|
|
1581
|
-
.regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
|
|
1582
|
-
])
|
|
1583
|
-
.optional()
|
|
1584
|
-
.describe('Help wanted issues count. Good for finding projects needing contributors.'),
|
|
1585
|
-
followers: z.number().optional().describe('Followers count filter.'),
|
|
1586
|
-
// SEARCH SCOPE
|
|
1587
|
-
match: z
|
|
1588
|
-
.enum(['name', 'description', 'readme'])
|
|
1589
|
-
.optional()
|
|
1590
|
-
.describe('Search scope: name, description, or readme content.'),
|
|
1591
|
-
// SORTING & LIMITS - Match CLI defaults exactly
|
|
1592
|
-
sort: z
|
|
1593
|
-
.enum([
|
|
1594
|
-
'forks',
|
|
1595
|
-
'help-wanted-issues',
|
|
1596
|
-
'stars',
|
|
1597
|
-
'updated',
|
|
1598
|
-
'best-match',
|
|
1599
|
-
])
|
|
1600
|
-
.optional()
|
|
1601
|
-
.default('best-match')
|
|
1602
|
-
.describe('Sort by: stars, updated, forks, help-wanted-issues, best-match.'),
|
|
1603
|
-
order: z
|
|
1604
|
-
.enum(['asc', 'desc'])
|
|
1605
|
-
.optional()
|
|
1606
|
-
.default('desc')
|
|
1607
|
-
.describe('Sort order: asc or desc.'),
|
|
1125
|
+
.describe('File size in KB. Format: >10, <100, or 10..50'),
|
|
1608
1126
|
limit: z
|
|
1609
1127
|
.number()
|
|
1610
1128
|
.int()
|
|
@@ -1612,10 +1130,10 @@ function registerSearchGitHubReposTool(server) {
|
|
|
1612
1130
|
.max(100)
|
|
1613
1131
|
.optional()
|
|
1614
1132
|
.default(30)
|
|
1615
|
-
.describe('Results limit (1-100).
|
|
1133
|
+
.describe('Results limit (1-100). Default: 30'),
|
|
1616
1134
|
},
|
|
1617
1135
|
annotations: {
|
|
1618
|
-
title: 'GitHub
|
|
1136
|
+
title: 'GitHub Code Search - Smart & Efficient',
|
|
1619
1137
|
readOnlyHint: true,
|
|
1620
1138
|
destructiveHint: false,
|
|
1621
1139
|
idempotentHint: true,
|
|
@@ -1623,325 +1141,307 @@ function registerSearchGitHubReposTool(server) {
|
|
|
1623
1141
|
},
|
|
1624
1142
|
}, async (args) => {
|
|
1625
1143
|
try {
|
|
1626
|
-
//
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
:
|
|
1630
|
-
cleanedQuery: '',
|
|
1631
|
-
extractedOwner: undefined,
|
|
1632
|
-
extractedRepo: undefined,
|
|
1633
|
-
};
|
|
1634
|
-
// Merge extracted owner with explicit owner parameter
|
|
1635
|
-
let finalOwner = args.owner;
|
|
1636
|
-
if (queryInfo.extractedOwner && !finalOwner) {
|
|
1637
|
-
finalOwner = queryInfo.extractedOwner;
|
|
1144
|
+
// Validate parameter combinations
|
|
1145
|
+
const validationError = validateSearchParameters(args);
|
|
1146
|
+
if (validationError) {
|
|
1147
|
+
return createResult({ error: validationError });
|
|
1638
1148
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
query: queryInfo.cleanedQuery || args.query,
|
|
1643
|
-
owner: finalOwner,
|
|
1644
|
-
};
|
|
1645
|
-
// Enhanced validation logic for primary filters
|
|
1646
|
-
const hasPrimaryFilter = enhancedArgs.query?.trim() ||
|
|
1647
|
-
enhancedArgs.owner ||
|
|
1648
|
-
enhancedArgs.language ||
|
|
1649
|
-
enhancedArgs.topic ||
|
|
1650
|
-
enhancedArgs.stars ||
|
|
1651
|
-
enhancedArgs.forks;
|
|
1652
|
-
if (!hasPrimaryFilter) {
|
|
1653
|
-
return createResult({
|
|
1654
|
-
error: 'Requires query or primary filter (owner, language, stars, topic, forks). You can also use owner/repo format like "microsoft/vscode" in the query.',
|
|
1655
|
-
});
|
|
1149
|
+
const result = await searchGitHubCode(args);
|
|
1150
|
+
if (result.isError) {
|
|
1151
|
+
return result;
|
|
1656
1152
|
}
|
|
1657
|
-
|
|
1658
|
-
const
|
|
1659
|
-
//
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
visibility: 'private',
|
|
1669
|
-
};
|
|
1670
|
-
const privateResult = await searchGitHubRepos(privateSearchArgs);
|
|
1671
|
-
if (!privateResult.isError) {
|
|
1672
|
-
const privateData = JSON.parse(privateResult.content[0].text);
|
|
1673
|
-
if (privateData.total > 0) {
|
|
1674
|
-
// Return private results with note
|
|
1675
|
-
return createResult({
|
|
1676
|
-
data: {
|
|
1677
|
-
...privateData,
|
|
1678
|
-
note: 'Found results in private repositories within the specified organization.',
|
|
1679
|
-
},
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1153
|
+
const execResult = JSON.parse(result.content[0].text);
|
|
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;
|
|
1683
1164
|
}
|
|
1165
|
+
return createResult({
|
|
1166
|
+
error: getErrorWithSuggestion({
|
|
1167
|
+
baseError: createNoResultsError('code'),
|
|
1168
|
+
suggestion: specificSuggestion,
|
|
1169
|
+
}),
|
|
1170
|
+
});
|
|
1684
1171
|
}
|
|
1685
|
-
|
|
1172
|
+
// Transform to optimized format
|
|
1173
|
+
const optimizedResult = transformToOptimizedFormat$1(items);
|
|
1174
|
+
return createResult({ data: optimizedResult });
|
|
1686
1175
|
}
|
|
1687
1176
|
catch (error) {
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
});
|
|
1177
|
+
const errorMessage = error.message || '';
|
|
1178
|
+
return handleSearchError(errorMessage);
|
|
1691
1179
|
}
|
|
1692
1180
|
});
|
|
1693
1181
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
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);
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
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) {
|
|
1319
|
+
args.push(`--limit=${params.limit}`);
|
|
1320
|
+
}
|
|
1321
|
+
// Add JSON output format
|
|
1322
|
+
args.push('--json=repository,path,textMatches,sha,url');
|
|
1323
|
+
return args;
|
|
1324
|
+
}
|
|
1325
|
+
async function searchGitHubCode(params) {
|
|
1326
|
+
const cacheKey = generateCacheKey('gh-code', params);
|
|
1696
1327
|
return withCache(cacheKey, async () => {
|
|
1697
1328
|
try {
|
|
1698
|
-
const
|
|
1699
|
-
const result = await executeGitHubCommand(
|
|
1329
|
+
const args = buildGitHubCliArgs(params);
|
|
1330
|
+
const result = await executeGitHubCommand('search', args, {
|
|
1700
1331
|
cache: false,
|
|
1701
1332
|
});
|
|
1702
|
-
|
|
1703
|
-
return result;
|
|
1704
|
-
}
|
|
1705
|
-
// Extract the actual content from the exec result
|
|
1706
|
-
const execResult = JSON.parse(result.content[0].text);
|
|
1707
|
-
const rawContent = execResult.result;
|
|
1708
|
-
// Parse JSON results and provide structured analysis
|
|
1709
|
-
let repositories = [];
|
|
1710
|
-
const analysis = {
|
|
1711
|
-
totalFound: 0,
|
|
1712
|
-
languages: new Set(),
|
|
1713
|
-
avgStars: 0,
|
|
1714
|
-
recentlyUpdated: 0,
|
|
1715
|
-
topStarred: [],
|
|
1716
|
-
};
|
|
1717
|
-
// Parse JSON response from GitHub CLI
|
|
1718
|
-
repositories = JSON.parse(rawContent);
|
|
1719
|
-
if (Array.isArray(repositories) && repositories.length > 0) {
|
|
1720
|
-
analysis.totalFound = repositories.length;
|
|
1721
|
-
// Analyze repository data
|
|
1722
|
-
let totalStars = 0;
|
|
1723
|
-
const now = new Date();
|
|
1724
|
-
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
1725
|
-
repositories.forEach(repo => {
|
|
1726
|
-
// Collect languages
|
|
1727
|
-
if (repo.language) {
|
|
1728
|
-
analysis.languages.add(repo.language);
|
|
1729
|
-
}
|
|
1730
|
-
// Calculate average stars (use correct field name)
|
|
1731
|
-
if (repo.stargazersCount) {
|
|
1732
|
-
totalStars += repo.stargazersCount;
|
|
1733
|
-
}
|
|
1734
|
-
// Count recently updated repositories (use correct field name)
|
|
1735
|
-
if (repo.updatedAt) {
|
|
1736
|
-
const updatedDate = new Date(repo.updatedAt);
|
|
1737
|
-
if (updatedDate > thirtyDaysAgo) {
|
|
1738
|
-
analysis.recentlyUpdated++;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
});
|
|
1742
|
-
analysis.avgStars =
|
|
1743
|
-
repositories.length > 0
|
|
1744
|
-
? Math.round(totalStars / repositories.length)
|
|
1745
|
-
: 0;
|
|
1746
|
-
// Get all repositories with comprehensive data
|
|
1747
|
-
analysis.topStarred = repositories.map(repo => ({
|
|
1748
|
-
name: repo.fullName || repo.name,
|
|
1749
|
-
stars: repo.stargazersCount || 0,
|
|
1750
|
-
description: repo.description || 'No description',
|
|
1751
|
-
language: repo.language || 'Unknown',
|
|
1752
|
-
url: repo.url,
|
|
1753
|
-
forks: repo.forksCount || 0,
|
|
1754
|
-
isPrivate: repo.isPrivate || false,
|
|
1755
|
-
isArchived: repo.isArchived || false,
|
|
1756
|
-
isFork: repo.isFork || false,
|
|
1757
|
-
topics: [], // GitHub CLI search repos doesn't provide topics in JSON output
|
|
1758
|
-
license: repo.license?.name || null,
|
|
1759
|
-
hasIssues: repo.hasIssues || false,
|
|
1760
|
-
openIssuesCount: repo.openIssuesCount || 0,
|
|
1761
|
-
createdAt: toDDMMYYYY(repo.createdAt),
|
|
1762
|
-
updatedAt: toDDMMYYYY(repo.updatedAt),
|
|
1763
|
-
visibility: repo.visibility || 'public',
|
|
1764
|
-
owner: repo.owner?.login || repo.owner,
|
|
1765
|
-
}));
|
|
1766
|
-
}
|
|
1767
|
-
return createResult({
|
|
1768
|
-
data: {
|
|
1769
|
-
total_count: analysis.totalFound,
|
|
1770
|
-
...(analysis.totalFound > 0
|
|
1771
|
-
? {
|
|
1772
|
-
repositories: analysis.topStarred,
|
|
1773
|
-
summary: {
|
|
1774
|
-
languages: Array.from(analysis.languages).slice(0, 10),
|
|
1775
|
-
avgStars: analysis.avgStars,
|
|
1776
|
-
recentlyUpdated: analysis.recentlyUpdated,
|
|
1777
|
-
},
|
|
1778
|
-
}
|
|
1779
|
-
: {
|
|
1780
|
-
repositories: [],
|
|
1781
|
-
cli_command: execResult.command, // Only on no results
|
|
1782
|
-
}),
|
|
1783
|
-
},
|
|
1784
|
-
});
|
|
1333
|
+
return result;
|
|
1785
1334
|
}
|
|
1786
1335
|
catch (error) {
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
});
|
|
1336
|
+
const errorMessage = error.message || '';
|
|
1337
|
+
return handleSearchError(errorMessage); // Delegating error handling
|
|
1790
1338
|
}
|
|
1791
1339
|
});
|
|
1792
1340
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
//
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
// Handle exact string search - preserve quotes and special characters
|
|
1801
|
-
if (query) {
|
|
1802
|
-
// For exact repository name searches (quoted strings), preserve the quotes
|
|
1803
|
-
// For special characters, pass them through - GitHub CLI handles escaping
|
|
1804
|
-
// Support searches like "microsoft/vscode", "@types/node", etc.
|
|
1805
|
-
args.push(query);
|
|
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;
|
|
1806
1348
|
}
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
//
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
if (params.forks !== undefined && !hasEmbeddedQualifiers)
|
|
1821
|
-
args.push(`--forks=${params.forks}`);
|
|
1822
|
-
// Handle topic as string or array - GitHub CLI expects comma-separated topics
|
|
1823
|
-
if (params.topic && !hasEmbeddedQualifiers) {
|
|
1824
|
-
const topics = Array.isArray(params.topic) ? params.topic : [params.topic];
|
|
1825
|
-
args.push(`--topic=${topics.join(',')}`);
|
|
1826
|
-
}
|
|
1827
|
-
if (params.numberOfTopics !== undefined)
|
|
1828
|
-
args.push(`--number-topics=${params.numberOfTopics}`);
|
|
1829
|
-
// Handle stars as number or string - avoid conflicts with embedded qualifiers
|
|
1830
|
-
if (params.stars !== undefined && !hasEmbeddedQualifiers) {
|
|
1831
|
-
const starsValue = typeof params.stars === 'number'
|
|
1832
|
-
? params.stars.toString()
|
|
1833
|
-
: params.stars.trim();
|
|
1834
|
-
// Validate numeric patterns for string values
|
|
1835
|
-
if (typeof params.stars === 'number' ||
|
|
1836
|
-
/^(\d+|>\d+|<\d+|\d+\.\.\d+|>=\d+|<=\d+)$/.test(starsValue)) {
|
|
1837
|
-
args.push(`--stars=${starsValue}`);
|
|
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.';
|
|
1838
1362
|
}
|
|
1839
1363
|
}
|
|
1840
|
-
//
|
|
1841
|
-
if (params.
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
if (params.visibility)
|
|
1846
|
-
args.push(`--visibility=${params.visibility}`);
|
|
1847
|
-
// Handle license as string or array
|
|
1848
|
-
if (params.license) {
|
|
1849
|
-
const licenses = Array.isArray(params.license)
|
|
1850
|
-
? params.license
|
|
1851
|
-
: [params.license];
|
|
1852
|
-
args.push(`--license=${licenses.join(',')}`);
|
|
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
|
+
}
|
|
1853
1369
|
}
|
|
1854
|
-
//
|
|
1855
|
-
if (params.
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
// COMMUNITY FILTERS - handle both number and string
|
|
1862
|
-
if (params.goodFirstIssues) {
|
|
1863
|
-
const value = typeof params.goodFirstIssues === 'number'
|
|
1864
|
-
? params.goodFirstIssues.toString()
|
|
1865
|
-
: params.goodFirstIssues;
|
|
1866
|
-
args.push(`--good-first-issues=${value}`);
|
|
1867
|
-
}
|
|
1868
|
-
if (params.helpWantedIssues) {
|
|
1869
|
-
const value = typeof params.helpWantedIssues === 'number'
|
|
1870
|
-
? params.helpWantedIssues.toString()
|
|
1871
|
-
: params.helpWantedIssues;
|
|
1872
|
-
args.push(`--help-wanted-issues=${value}`);
|
|
1873
|
-
}
|
|
1874
|
-
if (params.followers !== undefined)
|
|
1875
|
-
args.push(`--followers=${params.followers}`);
|
|
1876
|
-
// SEARCH SCOPE
|
|
1877
|
-
if (params.match)
|
|
1878
|
-
args.push(`--match=${params.match}`);
|
|
1879
|
-
// SORTING AND LIMITS
|
|
1880
|
-
if (params.limit)
|
|
1881
|
-
args.push(`--limit=${params.limit}`);
|
|
1882
|
-
if (params.order)
|
|
1883
|
-
args.push(`--order=${params.order}`);
|
|
1884
|
-
// Use best-match as default, only specify sort if different from default
|
|
1885
|
-
const sortBy = params.sort || 'best-match';
|
|
1886
|
-
if (sortBy !== 'best-match') {
|
|
1887
|
-
args.push(`--sort=${sortBy}`);
|
|
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
|
+
}
|
|
1888
1377
|
}
|
|
1889
|
-
|
|
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
|
|
1890
1384
|
}
|
|
1891
1385
|
|
|
1892
|
-
const
|
|
1893
|
-
const DESCRIPTION$5 = `Search commit history
|
|
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).`;
|
|
1894
1388
|
function registerGitHubSearchCommitsTool(server) {
|
|
1895
|
-
server.registerTool(
|
|
1389
|
+
server.registerTool(GITHUB_SEARCH_COMMITS_TOOL_NAME, {
|
|
1896
1390
|
description: DESCRIPTION$5,
|
|
1897
1391
|
inputSchema: {
|
|
1898
1392
|
query: z
|
|
1899
1393
|
.string()
|
|
1900
1394
|
.optional()
|
|
1901
|
-
.describe('Search
|
|
1395
|
+
.describe('Search terms. Start simple: "bug fix", "refactor". Use quotes for exact phrases.'),
|
|
1902
1396
|
// Repository filters
|
|
1903
1397
|
owner: z
|
|
1904
1398
|
.string()
|
|
1905
1399
|
.optional()
|
|
1906
|
-
.describe('Repository owner/
|
|
1400
|
+
.describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
|
|
1907
1401
|
repo: z
|
|
1908
1402
|
.string()
|
|
1909
1403
|
.optional()
|
|
1910
|
-
.describe('Repository name.
|
|
1404
|
+
.describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
|
|
1911
1405
|
// Author filters
|
|
1912
|
-
author: z
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
committerName: z
|
|
1406
|
+
author: z
|
|
1407
|
+
.string()
|
|
1408
|
+
.optional()
|
|
1409
|
+
.describe('GitHub username of commit author'),
|
|
1410
|
+
authorName: z
|
|
1918
1411
|
.string()
|
|
1919
1412
|
.optional()
|
|
1920
|
-
.describe('
|
|
1921
|
-
|
|
1413
|
+
.describe('Full name of commit author'),
|
|
1414
|
+
authorEmail: z.string().optional().describe('Email of commit author'),
|
|
1415
|
+
// Committer filters
|
|
1416
|
+
committer: z
|
|
1922
1417
|
.string()
|
|
1923
1418
|
.optional()
|
|
1924
|
-
.describe('
|
|
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'),
|
|
1925
1422
|
// Date filters
|
|
1926
1423
|
authorDate: z
|
|
1927
1424
|
.string()
|
|
1928
1425
|
.optional()
|
|
1929
|
-
.describe('
|
|
1426
|
+
.describe('When authored. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
|
|
1930
1427
|
committerDate: z
|
|
1931
1428
|
.string()
|
|
1932
1429
|
.optional()
|
|
1933
|
-
.describe('
|
|
1430
|
+
.describe('When committed. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
|
|
1934
1431
|
// Hash filters
|
|
1935
|
-
hash: z.string().optional().describe('
|
|
1936
|
-
parent: z.string().optional().describe('
|
|
1937
|
-
tree: z.string().optional().describe('
|
|
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'),
|
|
1938
1435
|
// State filters
|
|
1939
|
-
merge: z
|
|
1436
|
+
merge: z
|
|
1437
|
+
.boolean()
|
|
1438
|
+
.optional()
|
|
1439
|
+
.describe('Only merge commits (true) or exclude them (false)'),
|
|
1940
1440
|
// Visibility
|
|
1941
1441
|
visibility: z
|
|
1942
1442
|
.enum(['public', 'private', 'internal'])
|
|
1943
1443
|
.optional()
|
|
1944
|
-
.describe('
|
|
1444
|
+
.describe('Repository visibility filter'),
|
|
1945
1445
|
// Pagination and sorting
|
|
1946
1446
|
limit: z
|
|
1947
1447
|
.number()
|
|
@@ -1950,19 +1450,19 @@ function registerGitHubSearchCommitsTool(server) {
|
|
|
1950
1450
|
.max(50)
|
|
1951
1451
|
.optional()
|
|
1952
1452
|
.default(25)
|
|
1953
|
-
.describe('
|
|
1453
|
+
.describe('Results limit (1-50). Default: 25'),
|
|
1954
1454
|
sort: z
|
|
1955
1455
|
.enum(['author-date', 'committer-date'])
|
|
1956
1456
|
.optional()
|
|
1957
|
-
.describe('Sort
|
|
1457
|
+
.describe('Sort by date. Default: best match'),
|
|
1958
1458
|
order: z
|
|
1959
1459
|
.enum(['asc', 'desc'])
|
|
1960
1460
|
.optional()
|
|
1961
1461
|
.default('desc')
|
|
1962
|
-
.describe('
|
|
1462
|
+
.describe('Sort order. Default: desc'),
|
|
1963
1463
|
},
|
|
1964
1464
|
annotations: {
|
|
1965
|
-
title: 'GitHub Commit Search',
|
|
1465
|
+
title: 'GitHub Commit Search - Smart History Analysis',
|
|
1966
1466
|
readOnlyHint: true,
|
|
1967
1467
|
destructiveHint: false,
|
|
1968
1468
|
idempotentHint: true,
|
|
@@ -1975,28 +1475,13 @@ function registerGitHubSearchCommitsTool(server) {
|
|
|
1975
1475
|
return result;
|
|
1976
1476
|
}
|
|
1977
1477
|
const execResult = JSON.parse(result.content[0].text);
|
|
1978
|
-
const commits =
|
|
1478
|
+
const commits = execResult.result;
|
|
1979
1479
|
// GitHub CLI returns a direct array
|
|
1980
1480
|
const items = Array.isArray(commits) ? commits : [];
|
|
1981
|
-
//
|
|
1481
|
+
// Smart handling for no results - provide actionable suggestions
|
|
1982
1482
|
if (items.length === 0) {
|
|
1983
1483
|
return createResult({
|
|
1984
|
-
|
|
1985
|
-
commits: [],
|
|
1986
|
-
total_count: 0,
|
|
1987
|
-
cli_command: execResult.command,
|
|
1988
|
-
suggestions: {
|
|
1989
|
-
message: 'No commits found. GitHub commit search is limited compared to code/issue search.',
|
|
1990
|
-
fallback_strategies: [
|
|
1991
|
-
'Try simpler, shorter queries (single keywords work better)',
|
|
1992
|
-
"Use broader terms like 'fix' instead of 'fix useState bug'",
|
|
1993
|
-
'Search by author: add author filter for specific contributors',
|
|
1994
|
-
'Use date ranges: add authorDate or committerDate filters',
|
|
1995
|
-
'Try github_search_code tool for finding code patterns instead',
|
|
1996
|
-
],
|
|
1997
|
-
alternative_queries: generateCommitSearchAlternatives(args.query),
|
|
1998
|
-
},
|
|
1999
|
-
},
|
|
1484
|
+
error: createNoResultsError('commits'),
|
|
2000
1485
|
});
|
|
2001
1486
|
}
|
|
2002
1487
|
// Transform to optimized format
|
|
@@ -2007,21 +1492,16 @@ function registerGitHubSearchCommitsTool(server) {
|
|
|
2007
1492
|
const errorMessage = error.message || '';
|
|
2008
1493
|
if (errorMessage.includes('authentication')) {
|
|
2009
1494
|
return createResult({
|
|
2010
|
-
error:
|
|
1495
|
+
error: createAuthenticationError(),
|
|
2011
1496
|
});
|
|
2012
1497
|
}
|
|
2013
1498
|
if (errorMessage.includes('rate limit')) {
|
|
2014
1499
|
return createResult({
|
|
2015
|
-
error:
|
|
1500
|
+
error: createRateLimitError(false),
|
|
2016
1501
|
});
|
|
2017
1502
|
}
|
|
2018
1503
|
return createResult({
|
|
2019
|
-
error: '
|
|
2020
|
-
suggestions: [
|
|
2021
|
-
'Check authentication with api_status_check',
|
|
2022
|
-
'Use more specific date ranges or author filters',
|
|
2023
|
-
'Try simpler boolean queries',
|
|
2024
|
-
],
|
|
1504
|
+
error: createSearchFailedError('commits'),
|
|
2025
1505
|
});
|
|
2026
1506
|
}
|
|
2027
1507
|
});
|
|
@@ -2032,14 +1512,12 @@ function registerGitHubSearchCommitsTool(server) {
|
|
|
2032
1512
|
function transformCommitsToOptimizedFormat(items, _params) {
|
|
2033
1513
|
// Extract repository info if single repo search
|
|
2034
1514
|
const singleRepo = extractSingleRepository(items);
|
|
2035
|
-
// Get unique authors for metadata
|
|
2036
|
-
const uniqueAuthors = new Set(items.map(item => item.commit?.author?.name || item.author?.login || 'Unknown')).size;
|
|
2037
1515
|
const optimizedCommits = items
|
|
2038
1516
|
.map(item => ({
|
|
2039
1517
|
sha: item.sha,
|
|
2040
|
-
message: getCommitTitle(item.commit?.message
|
|
2041
|
-
author: item.commit?.author?.name
|
|
2042
|
-
date: toDDMMYYYY(item.commit?.author?.date
|
|
1518
|
+
message: getCommitTitle(item.commit?.message ?? ''),
|
|
1519
|
+
author: item.commit?.author?.name ?? item.author?.login ?? 'Unknown',
|
|
1520
|
+
date: toDDMMYYYY(item.commit?.author?.date ?? ''),
|
|
2043
1521
|
repository: singleRepo
|
|
2044
1522
|
? undefined
|
|
2045
1523
|
: simplifyRepoUrl(item.repository?.url || ''),
|
|
@@ -2068,13 +1546,6 @@ function transformCommitsToOptimizedFormat(items, _params) {
|
|
|
2068
1546
|
description: singleRepo.description,
|
|
2069
1547
|
};
|
|
2070
1548
|
}
|
|
2071
|
-
// Add metadata for insights
|
|
2072
|
-
if (items.length > 1) {
|
|
2073
|
-
result.metadata = {
|
|
2074
|
-
timeframe: getTimeframe(items),
|
|
2075
|
-
unique_authors: uniqueAuthors,
|
|
2076
|
-
};
|
|
2077
|
-
}
|
|
2078
1549
|
return result;
|
|
2079
1550
|
}
|
|
2080
1551
|
/**
|
|
@@ -2087,58 +1558,6 @@ function extractSingleRepository(items) {
|
|
|
2087
1558
|
const allSameRepo = items.every(item => item.repository.fullName === firstRepo.fullName);
|
|
2088
1559
|
return allSameRepo ? firstRepo : null;
|
|
2089
1560
|
}
|
|
2090
|
-
/**
|
|
2091
|
-
* Calculate timeframe of commits
|
|
2092
|
-
*/
|
|
2093
|
-
function getTimeframe(items) {
|
|
2094
|
-
if (items.length === 0)
|
|
2095
|
-
return '';
|
|
2096
|
-
const dates = items.map(item => new Date(item.commit?.author?.date || ''));
|
|
2097
|
-
const oldest = new Date(Math.min(...dates.map(d => d.getTime())));
|
|
2098
|
-
const newest = new Date(Math.max(...dates.map(d => d.getTime())));
|
|
2099
|
-
const diffMs = newest.getTime() - oldest.getTime();
|
|
2100
|
-
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
2101
|
-
if (diffDays === 0)
|
|
2102
|
-
return 'same day';
|
|
2103
|
-
if (diffDays < 7)
|
|
2104
|
-
return `${diffDays} days`;
|
|
2105
|
-
if (diffDays < 30)
|
|
2106
|
-
return `${Math.floor(diffDays / 7)} weeks`;
|
|
2107
|
-
if (diffDays < 365)
|
|
2108
|
-
return `${Math.floor(diffDays / 30)} months`;
|
|
2109
|
-
return `${Math.floor(diffDays / 365)} years`;
|
|
2110
|
-
}
|
|
2111
|
-
/**
|
|
2112
|
-
* Generate alternative commit search queries when original query fails
|
|
2113
|
-
*/
|
|
2114
|
-
function generateCommitSearchAlternatives(originalQuery) {
|
|
2115
|
-
if (!originalQuery) {
|
|
2116
|
-
return [
|
|
2117
|
-
{ query: 'fix', reason: 'Search for general fixes' },
|
|
2118
|
-
{ query: 'bug', reason: 'Search for bug-related commits' },
|
|
2119
|
-
{ query: 'refactor', reason: 'Search for refactoring commits' },
|
|
2120
|
-
];
|
|
2121
|
-
}
|
|
2122
|
-
const alternatives = [];
|
|
2123
|
-
const query = originalQuery.toLowerCase();
|
|
2124
|
-
// Extract key terms and create simpler alternatives
|
|
2125
|
-
if (query.includes('fix') && query.includes('bug')) {
|
|
2126
|
-
alternatives.push({ query: 'fix', reason: 'Broader search for all fixes' }, { query: 'bug', reason: 'Search for bug-related commits' });
|
|
2127
|
-
}
|
|
2128
|
-
else if (query.includes(' ')) {
|
|
2129
|
-
// Multi-word query - suggest individual terms
|
|
2130
|
-
const words = query.split(' ').filter(w => w.length > 2);
|
|
2131
|
-
words.slice(0, 2).forEach(word => {
|
|
2132
|
-
alternatives.push({
|
|
2133
|
-
query: word,
|
|
2134
|
-
reason: `Single keyword search for '${word}'`,
|
|
2135
|
-
});
|
|
2136
|
-
});
|
|
2137
|
-
}
|
|
2138
|
-
// Always suggest some common commit patterns
|
|
2139
|
-
alternatives.push({ query: 'feat', reason: 'Search for feature commits' }, { query: 'docs', reason: 'Search for documentation updates' });
|
|
2140
|
-
return alternatives.slice(0, 4); // Limit to 4 suggestions
|
|
2141
|
-
}
|
|
2142
1561
|
async function searchGitHubCommits(params) {
|
|
2143
1562
|
const cacheKey = generateCacheKey('gh-commits', params);
|
|
2144
1563
|
return withCache(cacheKey, async () => {
|
|
@@ -2153,16 +1572,16 @@ async function searchGitHubCommits(params) {
|
|
|
2153
1572
|
const errorMessage = error.message || '';
|
|
2154
1573
|
if (errorMessage.includes('authentication')) {
|
|
2155
1574
|
return createResult({
|
|
2156
|
-
error:
|
|
1575
|
+
error: createAuthenticationError(),
|
|
2157
1576
|
});
|
|
2158
1577
|
}
|
|
2159
1578
|
if (errorMessage.includes('rate limit')) {
|
|
2160
1579
|
return createResult({
|
|
2161
|
-
error:
|
|
1580
|
+
error: createRateLimitError(false),
|
|
2162
1581
|
});
|
|
2163
1582
|
}
|
|
2164
1583
|
return createResult({
|
|
2165
|
-
error: '
|
|
1584
|
+
error: createSearchFailedError('commits'),
|
|
2166
1585
|
});
|
|
2167
1586
|
}
|
|
2168
1587
|
});
|
|
@@ -2225,85 +1644,133 @@ function buildGitHubCommitCliArgs(params) {
|
|
|
2225
1644
|
return args;
|
|
2226
1645
|
}
|
|
2227
1646
|
|
|
2228
|
-
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
server.registerTool(TOOL_NAME$4, {
|
|
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, {
|
|
2233
1651
|
description: DESCRIPTION$4,
|
|
2234
1652
|
inputSchema: {
|
|
2235
1653
|
query: z
|
|
2236
1654
|
.string()
|
|
2237
1655
|
.min(1, 'Search query is required and cannot be empty')
|
|
2238
|
-
.describe('Search
|
|
2239
|
-
owner: z
|
|
2240
|
-
repo: z.string().optional().describe('Repository name'),
|
|
2241
|
-
author: z.string().optional().describe('Filter by pull request author'),
|
|
2242
|
-
assignee: z.string().optional().describe('Filter by assignee'),
|
|
2243
|
-
mentions: z.string().optional().describe('Filter by user mentions'),
|
|
2244
|
-
commenter: z.string().optional().describe('Filter by comments by user'),
|
|
2245
|
-
involves: z.string().optional().describe('Filter by user involvement'),
|
|
2246
|
-
reviewedBy: z
|
|
1656
|
+
.describe('Search terms. Start simple: "error", "crash". Use quotes for exact phrases.'),
|
|
1657
|
+
owner: z
|
|
2247
1658
|
.string()
|
|
1659
|
+
.min(1)
|
|
2248
1660
|
.optional()
|
|
2249
|
-
.describe('
|
|
2250
|
-
|
|
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
|
|
2251
1663
|
.string()
|
|
2252
1664
|
.optional()
|
|
2253
|
-
.describe('
|
|
2254
|
-
|
|
2255
|
-
.
|
|
1665
|
+
.describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
|
|
1666
|
+
app: z
|
|
1667
|
+
.string()
|
|
2256
1668
|
.optional()
|
|
2257
|
-
.describe('
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
language: z.string().optional().describe('Filter by coding language'),
|
|
2261
|
-
created: z.string().optional().describe('Filter by created date'),
|
|
2262
|
-
updated: z.string().optional().describe('Filter by last updated date'),
|
|
2263
|
-
mergedAt: z.string().optional().describe('Filter by merged date'),
|
|
2264
|
-
closed: z.string().optional().describe('Filter by closed date'),
|
|
2265
|
-
draft: z.boolean().optional().describe('Filter by draft state'),
|
|
2266
|
-
checks: z
|
|
2267
|
-
.enum(['pending', 'success', 'failure'])
|
|
1669
|
+
.describe('GitHub App that created the issue'),
|
|
1670
|
+
archived: z
|
|
1671
|
+
.boolean()
|
|
2268
1672
|
.optional()
|
|
2269
|
-
.describe('
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
.
|
|
1673
|
+
.describe('Include archived repositories'),
|
|
1674
|
+
assignee: z.string().optional().describe('GitHub username of assignee'),
|
|
1675
|
+
author: z
|
|
1676
|
+
.string()
|
|
2273
1677
|
.optional()
|
|
2274
|
-
.describe('
|
|
2275
|
-
|
|
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
|
|
2276
1688
|
.number()
|
|
2277
|
-
.int()
|
|
2278
|
-
.min(1)
|
|
2279
|
-
.max(50)
|
|
2280
1689
|
.optional()
|
|
2281
|
-
.
|
|
2282
|
-
|
|
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'),
|
|
2283
1741
|
sort: z
|
|
2284
1742
|
.enum([
|
|
2285
1743
|
'comments',
|
|
1744
|
+
'created',
|
|
1745
|
+
'interactions',
|
|
2286
1746
|
'reactions',
|
|
2287
1747
|
'reactions-+1',
|
|
2288
1748
|
'reactions--1',
|
|
2289
|
-
'reactions-smile',
|
|
2290
|
-
'reactions-thinking_face',
|
|
2291
1749
|
'reactions-heart',
|
|
1750
|
+
'reactions-smile',
|
|
2292
1751
|
'reactions-tada',
|
|
2293
|
-
'
|
|
2294
|
-
'created',
|
|
1752
|
+
'reactions-thinking_face',
|
|
2295
1753
|
'updated',
|
|
1754
|
+
'best-match',
|
|
2296
1755
|
])
|
|
2297
1756
|
.optional()
|
|
2298
|
-
.describe('Sort
|
|
1757
|
+
.describe('Sort by activity or reactions. Default: best match'),
|
|
2299
1758
|
order: z
|
|
2300
1759
|
.enum(['asc', 'desc'])
|
|
2301
1760
|
.optional()
|
|
2302
1761
|
.default('desc')
|
|
2303
|
-
.describe('
|
|
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'),
|
|
2304
1771
|
},
|
|
2305
1772
|
annotations: {
|
|
2306
|
-
title: 'GitHub
|
|
1773
|
+
title: 'GitHub Issues Search - Bug & Feature Discovery',
|
|
2307
1774
|
readOnlyHint: true,
|
|
2308
1775
|
destructiveHint: false,
|
|
2309
1776
|
idempotentHint: true,
|
|
@@ -2312,750 +1779,912 @@ function registerSearchGitHubPullRequestsTool(server) {
|
|
|
2312
1779
|
}, async (args) => {
|
|
2313
1780
|
if (!args.query?.trim()) {
|
|
2314
1781
|
return createResult({
|
|
2315
|
-
error:
|
|
1782
|
+
error: `${ERROR_MESSAGES.QUERY_REQUIRED} ${SUGGESTIONS.PROVIDE_KEYWORDS}`,
|
|
2316
1783
|
});
|
|
2317
1784
|
}
|
|
2318
1785
|
if (args.query.length > 256) {
|
|
2319
1786
|
return createResult({
|
|
2320
|
-
error:
|
|
1787
|
+
error: ERROR_MESSAGES.QUERY_TOO_LONG,
|
|
2321
1788
|
});
|
|
2322
1789
|
}
|
|
2323
1790
|
try {
|
|
2324
|
-
return await
|
|
1791
|
+
return await searchGitHubIssues(args);
|
|
2325
1792
|
}
|
|
2326
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
|
|
2327
1806
|
return createResult({
|
|
2328
|
-
error: '
|
|
1807
|
+
error: createSearchFailedError('issues'),
|
|
2329
1808
|
});
|
|
2330
1809
|
}
|
|
2331
1810
|
});
|
|
2332
1811
|
}
|
|
2333
|
-
async function
|
|
2334
|
-
const cacheKey = generateCacheKey('gh-
|
|
1812
|
+
async function searchGitHubIssues(params) {
|
|
1813
|
+
const cacheKey = generateCacheKey('gh-issues', params);
|
|
2335
1814
|
return withCache(cacheKey, async () => {
|
|
2336
|
-
const { command, args } =
|
|
1815
|
+
const { command, args } = buildGitHubIssuesAPICommand(params);
|
|
2337
1816
|
const result = await executeGitHubCommand(command, args, { cache: false });
|
|
2338
1817
|
if (result.isError) {
|
|
2339
1818
|
return result;
|
|
2340
1819
|
}
|
|
2341
1820
|
const execResult = JSON.parse(result.content[0].text);
|
|
2342
|
-
const apiResponse =
|
|
2343
|
-
const
|
|
2344
|
-
const
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
draft: pr.draft,
|
|
2358
|
-
};
|
|
2359
|
-
// Only include optional fields if they have values
|
|
2360
|
-
if (pr.merged_at)
|
|
2361
|
-
result.merged_at = pr.merged_at;
|
|
2362
|
-
if (pr.closed_at)
|
|
2363
|
-
result.closed_at = toDDMMYYYY(pr.closed_at);
|
|
2364
|
-
if (pr.head?.ref)
|
|
2365
|
-
result.head = pr.head.ref;
|
|
2366
|
-
if (pr.base?.ref)
|
|
2367
|
-
result.base = pr.base.ref;
|
|
2368
|
-
return result;
|
|
2369
|
-
});
|
|
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
|
+
}));
|
|
2370
1836
|
const searchResult = {
|
|
2371
|
-
results:
|
|
2372
|
-
total_count: apiResponse.total_count || cleanPRs.length,
|
|
2373
|
-
metadata: {
|
|
2374
|
-
incomplete_results: apiResponse.incomplete_results || false,
|
|
2375
|
-
},
|
|
1837
|
+
results: cleanIssues,
|
|
2376
1838
|
};
|
|
2377
1839
|
return createResult({ data: searchResult });
|
|
2378
1840
|
});
|
|
2379
1841
|
}
|
|
2380
|
-
function
|
|
2381
|
-
const queryParts = [
|
|
2382
|
-
//
|
|
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);
|
|
1866
|
+
}
|
|
1867
|
+
// Repository/organization qualifiers - prioritize function params over query
|
|
2383
1868
|
if (params.owner && params.repo) {
|
|
2384
1869
|
queryParts.push(`repo:${params.owner}/${params.repo}`);
|
|
2385
1870
|
}
|
|
2386
1871
|
else if (params.owner) {
|
|
2387
1872
|
queryParts.push(`org:${params.owner}`);
|
|
2388
1873
|
}
|
|
2389
|
-
// Build search qualifiers from
|
|
1874
|
+
// Build search qualifiers from function parameters (these take precedence)
|
|
2390
1875
|
const qualifiers = {
|
|
2391
1876
|
author: params.author,
|
|
2392
1877
|
assignee: params.assignee,
|
|
2393
1878
|
mentions: params.mentions,
|
|
2394
1879
|
commenter: params.commenter,
|
|
2395
1880
|
involves: params.involves,
|
|
1881
|
+
language: params.language,
|
|
2396
1882
|
state: params.state,
|
|
2397
1883
|
created: params.created,
|
|
2398
1884
|
updated: params.updated,
|
|
2399
1885
|
closed: params.closed,
|
|
2400
|
-
language: params.language,
|
|
2401
1886
|
};
|
|
2402
1887
|
Object.entries(qualifiers).forEach(([key, value]) => {
|
|
2403
1888
|
if (value)
|
|
2404
1889
|
queryParts.push(`${key}:${value}`);
|
|
2405
1890
|
});
|
|
2406
|
-
// Special qualifiers
|
|
2407
|
-
if (params.
|
|
2408
|
-
queryParts.push(`
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
|
|
2430
|
-
if (params.sort)
|
|
2431
|
-
apiPath += `&sort=${params.sort}`;
|
|
2432
|
-
if (params.order)
|
|
2433
|
-
apiPath += `&order=${params.order}`;
|
|
2434
|
-
return { command: 'api', args: [apiPath] };
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
const TOOL_NAME$3 = 'npm_package_search';
|
|
2438
|
-
const DESCRIPTION$3 = `Search npm packages by keywords using fuzzy matching. Use space-separated keywords like "react hooks" or "cli typescript". No boolean operators supported.`;
|
|
2439
|
-
const MAX_DESCRIPTION_LENGTH = 100;
|
|
2440
|
-
const MAX_KEYWORDS = 10;
|
|
2441
|
-
function registerNpmSearchTool(server) {
|
|
2442
|
-
server.registerTool(TOOL_NAME$3, {
|
|
2443
|
-
description: DESCRIPTION$3,
|
|
2444
|
-
inputSchema: {
|
|
2445
|
-
queries: z
|
|
2446
|
-
.union([z.string(), z.array(z.string())])
|
|
2447
|
-
.describe('Package names or keywords to search for. Use simple space-separated keywords like "react hooks" or "typescript cli" for fuzzy matching.'),
|
|
2448
|
-
searchlimit: z
|
|
2449
|
-
.number()
|
|
2450
|
-
.int()
|
|
2451
|
-
.min(1)
|
|
2452
|
-
.max(50)
|
|
2453
|
-
.optional()
|
|
2454
|
-
.default(20)
|
|
2455
|
-
.describe('Max results per query (default: 20, max: 50)'),
|
|
2456
|
-
},
|
|
2457
|
-
annotations: {
|
|
2458
|
-
title: 'NPM Package Search',
|
|
2459
|
-
readOnlyHint: true,
|
|
2460
|
-
destructiveHint: false,
|
|
2461
|
-
idempotentHint: true,
|
|
2462
|
-
openWorldHint: true,
|
|
2463
|
-
},
|
|
2464
|
-
}, async (args) => {
|
|
2465
|
-
try {
|
|
2466
|
-
const queries = Array.isArray(args.queries)
|
|
2467
|
-
? args.queries
|
|
2468
|
-
: [args.queries];
|
|
2469
|
-
const searchLimit = args.searchlimit || 20;
|
|
2470
|
-
const allPackages = [];
|
|
2471
|
-
// Search for each query term
|
|
2472
|
-
for (const query of queries) {
|
|
2473
|
-
const result = await executeNpmCommand('search', [query, `--searchlimit=${searchLimit}`, '--json'], { cache: true });
|
|
2474
|
-
if (!result.isError && result.content?.[0]?.text) {
|
|
2475
|
-
const packages = parseNpmSearchOutput(result.content[0].text);
|
|
2476
|
-
allPackages.push(...packages);
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
const deduplicatedPackages = deduplicatePackages(allPackages);
|
|
2480
|
-
if (deduplicatedPackages.length > 0) {
|
|
2481
|
-
return createResult({
|
|
2482
|
-
data: {
|
|
2483
|
-
total_count: deduplicatedPackages.length,
|
|
2484
|
-
results: deduplicatedPackages,
|
|
2485
|
-
},
|
|
2486
|
-
});
|
|
2487
|
-
}
|
|
2488
|
-
return createResult({
|
|
2489
|
-
error: 'No packages found',
|
|
2490
|
-
cli_command: `npm search ${Array.isArray(args.queries) ? args.queries.join(' ') : args.queries}`,
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
catch (error) {
|
|
2494
|
-
return createResult({
|
|
2495
|
-
error: 'Package search failed - check terms or try different keywords',
|
|
2496
|
-
cli_command: `npm search ${Array.isArray(args.queries) ? args.queries.join(' ') : args.queries}`,
|
|
2497
|
-
});
|
|
2498
|
-
}
|
|
2499
|
-
});
|
|
2500
|
-
}
|
|
2501
|
-
function deduplicatePackages(packages) {
|
|
2502
|
-
const seen = new Set();
|
|
2503
|
-
return packages.filter(pkg => {
|
|
2504
|
-
if (seen.has(pkg.name))
|
|
2505
|
-
return false;
|
|
2506
|
-
seen.add(pkg.name);
|
|
2507
|
-
return true;
|
|
2508
|
-
});
|
|
2509
|
-
}
|
|
2510
|
-
function normalizePackage(pkg) {
|
|
2511
|
-
const description = pkg.description || null;
|
|
2512
|
-
const truncatedDescription = description && description.length > MAX_DESCRIPTION_LENGTH
|
|
2513
|
-
? description.substring(0, MAX_DESCRIPTION_LENGTH) + '...'
|
|
2514
|
-
: description;
|
|
2515
|
-
const keywords = pkg.keywords || [];
|
|
2516
|
-
const limitedKeywords = keywords.slice(0, MAX_KEYWORDS);
|
|
2517
|
-
return {
|
|
2518
|
-
name: pkg.name || '',
|
|
2519
|
-
version: pkg.version || '',
|
|
2520
|
-
description: truncatedDescription,
|
|
2521
|
-
keywords: limitedKeywords,
|
|
2522
|
-
repository: pkg.links?.repository || pkg.repository?.url || null,
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
function parseNpmSearchOutput(output) {
|
|
2526
|
-
try {
|
|
2527
|
-
const wrapper = JSON.parse(output);
|
|
2528
|
-
const commandResult = typeof wrapper.result === 'string'
|
|
2529
|
-
? JSON.parse(wrapper.result)
|
|
2530
|
-
: wrapper.result;
|
|
2531
|
-
let packages = [];
|
|
2532
|
-
// Handle different npm search output formats
|
|
2533
|
-
if (Array.isArray(commandResult)) {
|
|
2534
|
-
packages = commandResult;
|
|
2535
|
-
}
|
|
2536
|
-
else if (commandResult?.objects && Array.isArray(commandResult.objects)) {
|
|
2537
|
-
packages = commandResult.objects.map((obj) => obj.package || obj);
|
|
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()}`);
|
|
2538
1914
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
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}"`);
|
|
2541
1921
|
}
|
|
2542
|
-
return packages.map(normalizePackage);
|
|
2543
1922
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
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
|
+
}
|
|
2546
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}`;
|
|
1934
|
+
if (params.order)
|
|
1935
|
+
apiPath += `&order=${params.order}`;
|
|
1936
|
+
return { command: 'api', args: [apiPath] };
|
|
2547
1937
|
}
|
|
2548
1938
|
|
|
2549
|
-
|
|
2550
|
-
const
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
1939
|
+
// TODO: add PR commeents. e.g, gh pr view <PR_NUMBER_OR_URL_OR_BRANCH> --comments
|
|
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).`;
|
|
1942
|
+
function registerSearchGitHubPullRequestsTool(server) {
|
|
1943
|
+
server.registerTool(GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME, {
|
|
1944
|
+
description: DESCRIPTION$3,
|
|
2554
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.'),
|
|
2555
1950
|
owner: z
|
|
2556
1951
|
.string()
|
|
2557
|
-
.
|
|
2558
|
-
.
|
|
2559
|
-
.regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
|
|
2560
|
-
.describe(`Repository owner/organization.`),
|
|
1952
|
+
.optional()
|
|
1953
|
+
.describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
|
|
2561
1954
|
repo: z
|
|
2562
1955
|
.string()
|
|
2563
|
-
.
|
|
2564
|
-
.
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
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
|
|
2568
1965
|
.string()
|
|
2569
|
-
.
|
|
2570
|
-
.
|
|
2571
|
-
|
|
2572
|
-
.
|
|
2573
|
-
|
|
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
|
|
2574
1974
|
.string()
|
|
2575
1975
|
.optional()
|
|
2576
|
-
.
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
.
|
|
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'),
|
|
2580
2036
|
},
|
|
2581
2037
|
annotations: {
|
|
2582
|
-
title: 'GitHub
|
|
2038
|
+
title: 'GitHub PR Search - Implementation Discovery',
|
|
2583
2039
|
readOnlyHint: true,
|
|
2584
2040
|
destructiveHint: false,
|
|
2585
2041
|
idempotentHint: true,
|
|
2586
2042
|
openWorldHint: true,
|
|
2587
2043
|
},
|
|
2588
2044
|
}, async (args) => {
|
|
2045
|
+
if (!args.query?.trim()) {
|
|
2046
|
+
return createResult({
|
|
2047
|
+
error: `${ERROR_MESSAGES.QUERY_REQUIRED} ${SUGGESTIONS.PROVIDE_PR_KEYWORDS}`,
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
if (args.query.length > 256) {
|
|
2051
|
+
return createResult({
|
|
2052
|
+
error: ERROR_MESSAGES.QUERY_TOO_LONG,
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2589
2055
|
try {
|
|
2590
|
-
|
|
2591
|
-
return result;
|
|
2056
|
+
return await searchGitHubPullRequests(args);
|
|
2592
2057
|
}
|
|
2593
2058
|
catch (error) {
|
|
2594
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
2595
2059
|
return createResult({
|
|
2596
|
-
error:
|
|
2060
|
+
error: createSearchFailedError('pull_requests'),
|
|
2597
2061
|
});
|
|
2598
2062
|
}
|
|
2599
2063
|
});
|
|
2600
2064
|
}
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
*
|
|
2604
|
-
* Features:
|
|
2605
|
-
* - Smart branch detection: fetches repository default branch automatically
|
|
2606
|
-
* - Intelligent fallback: tries requested -> default -> common branches
|
|
2607
|
-
* - Input validation: prevents path traversal and validates GitHub naming
|
|
2608
|
-
* - Clear error context: provides descriptive error messages
|
|
2609
|
-
* - Efficient caching: avoids redundant API calls
|
|
2610
|
-
* - Rich metadata: includes all GitHub API fields (sha, urls, links, etc.)
|
|
2611
|
-
*/
|
|
2612
|
-
async function viewRepositoryStructure(params) {
|
|
2613
|
-
const cacheKey = generateCacheKey('gh-repo-structure', params);
|
|
2065
|
+
async function searchGitHubPullRequests(params) {
|
|
2066
|
+
const cacheKey = generateCacheKey('gh-prs', params);
|
|
2614
2067
|
return withCache(cacheKey, async () => {
|
|
2615
|
-
const {
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
// Try the requested branch first, then fallback to main/master
|
|
2620
|
-
const branchesToTry = await getSmartBranchFallback(owner, repo, branch);
|
|
2621
|
-
let items = [];
|
|
2622
|
-
let usedBranch = branch;
|
|
2623
|
-
let lastError = null;
|
|
2624
|
-
for (const tryBranch of branchesToTry) {
|
|
2625
|
-
try {
|
|
2626
|
-
const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${tryBranch}`;
|
|
2627
|
-
const result = await executeGitHubCommand('api', [apiPath], {
|
|
2628
|
-
cache: false,
|
|
2629
|
-
});
|
|
2630
|
-
if (!result.isError) {
|
|
2631
|
-
const execResult = JSON.parse(result.content[0].text);
|
|
2632
|
-
const apiItems = JSON.parse(execResult.result);
|
|
2633
|
-
items = Array.isArray(apiItems) ? apiItems : [apiItems];
|
|
2634
|
-
usedBranch = tryBranch;
|
|
2635
|
-
break;
|
|
2636
|
-
}
|
|
2637
|
-
else {
|
|
2638
|
-
lastError = new Error(result.content[0].text);
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
catch (error) {
|
|
2642
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2643
|
-
// Try next branch
|
|
2644
|
-
continue;
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
if (items.length === 0) {
|
|
2648
|
-
// Use the most descriptive error message
|
|
2649
|
-
const errorMsg = lastError?.message || 'Unknown error';
|
|
2650
|
-
if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
|
|
2651
|
-
if (path) {
|
|
2652
|
-
return createResult({
|
|
2653
|
-
error: `Path "${path}" not found - verify path or use code search`,
|
|
2654
|
-
});
|
|
2655
|
-
}
|
|
2656
|
-
else {
|
|
2657
|
-
return createResult({
|
|
2658
|
-
error: `Repository not found: ${owner}/${repo} - verify names`,
|
|
2659
|
-
});
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
|
|
2663
|
-
return createResult({
|
|
2664
|
-
error: `Access denied to ${owner}/${repo} - check permissions`,
|
|
2665
|
-
});
|
|
2666
|
-
}
|
|
2667
|
-
else {
|
|
2668
|
-
return createResult({
|
|
2669
|
-
error: `Access failed: ${owner}/${repo} - check connection`,
|
|
2670
|
-
});
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
// Limit total items to 100 for efficiency
|
|
2674
|
-
const limitedItems = items.slice(0, 100);
|
|
2675
|
-
// Sort: directories first, then alphabetically
|
|
2676
|
-
limitedItems.sort((a, b) => {
|
|
2677
|
-
if (a.type !== b.type) {
|
|
2678
|
-
return a.type === 'dir' ? -1 : 1;
|
|
2679
|
-
}
|
|
2680
|
-
return a.name.localeCompare(b.name);
|
|
2681
|
-
});
|
|
2682
|
-
// Create simplified, token-efficient structure
|
|
2683
|
-
const files = limitedItems
|
|
2684
|
-
.filter(item => item.type === 'file')
|
|
2685
|
-
.map(item => ({
|
|
2686
|
-
name: item.name,
|
|
2687
|
-
size: item.size,
|
|
2688
|
-
url: item.path, // Use path for fetching
|
|
2689
|
-
}));
|
|
2690
|
-
const folders = limitedItems
|
|
2691
|
-
.filter(item => item.type === 'dir')
|
|
2692
|
-
.map(item => ({
|
|
2693
|
-
name: item.name,
|
|
2694
|
-
url: item.path, // Use path for browsing
|
|
2695
|
-
}));
|
|
2696
|
-
// Simplified result structure - token efficient
|
|
2697
|
-
const result = {
|
|
2698
|
-
repository: `${owner}/${repo}`,
|
|
2699
|
-
branch: usedBranch,
|
|
2700
|
-
path: cleanPath || '/',
|
|
2701
|
-
githubBasePath: `https://api.github.com/repos/${owner}/${repo}/contents/`,
|
|
2702
|
-
files: {
|
|
2703
|
-
count: files.length,
|
|
2704
|
-
files: files,
|
|
2705
|
-
},
|
|
2706
|
-
folders: {
|
|
2707
|
-
count: folders.length,
|
|
2708
|
-
folders: folders,
|
|
2709
|
-
},
|
|
2710
|
-
...((usedBranch !== branch || limitedItems.length === 100) && {
|
|
2711
|
-
metadata: {
|
|
2712
|
-
...(usedBranch !== branch && {
|
|
2713
|
-
branchFallback: {
|
|
2714
|
-
requested: branch,
|
|
2715
|
-
used: usedBranch,
|
|
2716
|
-
},
|
|
2717
|
-
}),
|
|
2718
|
-
...(limitedItems.length === 100 && {
|
|
2719
|
-
truncated: true,
|
|
2720
|
-
}),
|
|
2721
|
-
},
|
|
2722
|
-
}),
|
|
2723
|
-
};
|
|
2724
|
-
return createResult({ data: result });
|
|
2068
|
+
const { command, args } = buildGitHubPullRequestsAPICommand(params);
|
|
2069
|
+
const result = await executeGitHubCommand(command, args, { cache: false });
|
|
2070
|
+
if (result.isError) {
|
|
2071
|
+
return result;
|
|
2725
2072
|
}
|
|
2726
|
-
|
|
2073
|
+
const execResult = JSON.parse(result.content[0].text);
|
|
2074
|
+
const apiResponse = execResult.result;
|
|
2075
|
+
const pullRequests = apiResponse.items || [];
|
|
2076
|
+
if (pullRequests.length === 0) {
|
|
2727
2077
|
return createResult({
|
|
2728
|
-
error:
|
|
2078
|
+
error: createNoResultsError('pull_requests'),
|
|
2729
2079
|
});
|
|
2730
2080
|
}
|
|
2081
|
+
const cleanPRs = pullRequests.map((pr) => {
|
|
2082
|
+
const result = {
|
|
2083
|
+
number: pr.number,
|
|
2084
|
+
title: pr.title,
|
|
2085
|
+
state: pr.state,
|
|
2086
|
+
author: pr.user?.login || '',
|
|
2087
|
+
repository: pr.repository_url?.split('/').slice(-2).join('/') || 'unknown',
|
|
2088
|
+
labels: pr.labels?.map(l => l.name) || [],
|
|
2089
|
+
created_at: toDDMMYYYY(pr.created_at),
|
|
2090
|
+
updated_at: toDDMMYYYY(pr.updated_at),
|
|
2091
|
+
url: pr.html_url,
|
|
2092
|
+
comments: pr.comments,
|
|
2093
|
+
reactions: pr.reactions?.total_count || 0,
|
|
2094
|
+
draft: pr.draft,
|
|
2095
|
+
};
|
|
2096
|
+
// Only include optional fields if they have values
|
|
2097
|
+
if (pr.merged_at)
|
|
2098
|
+
result.merged_at = pr.merged_at;
|
|
2099
|
+
if (pr.closed_at)
|
|
2100
|
+
result.closed_at = toDDMMYYYY(pr.closed_at);
|
|
2101
|
+
if (pr.head?.ref)
|
|
2102
|
+
result.head = pr.head.ref;
|
|
2103
|
+
if (pr.base?.ref)
|
|
2104
|
+
result.base = pr.base.ref;
|
|
2105
|
+
return result;
|
|
2106
|
+
});
|
|
2107
|
+
const searchResult = {
|
|
2108
|
+
results: cleanPRs,
|
|
2109
|
+
total_count: apiResponse.total_count || cleanPRs.length,
|
|
2110
|
+
};
|
|
2111
|
+
return createResult({ data: searchResult });
|
|
2731
2112
|
});
|
|
2732
2113
|
}
|
|
2114
|
+
function buildGitHubPullRequestsAPICommand(params) {
|
|
2115
|
+
const queryParts = [params.query?.trim() || ''];
|
|
2116
|
+
// Repository/organization qualifiers
|
|
2117
|
+
if (params.owner && params.repo) {
|
|
2118
|
+
queryParts.push(`repo:${params.owner}/${params.repo}`);
|
|
2119
|
+
}
|
|
2120
|
+
else if (params.owner) {
|
|
2121
|
+
queryParts.push(`org:${params.owner}`);
|
|
2122
|
+
}
|
|
2123
|
+
// Build search qualifiers from params
|
|
2124
|
+
const qualifiers = {
|
|
2125
|
+
author: params.author,
|
|
2126
|
+
assignee: params.assignee,
|
|
2127
|
+
mentions: params.mentions,
|
|
2128
|
+
commenter: params.commenter,
|
|
2129
|
+
involves: params.involves,
|
|
2130
|
+
state: params.state,
|
|
2131
|
+
created: params.created,
|
|
2132
|
+
updated: params.updated,
|
|
2133
|
+
closed: params.closed,
|
|
2134
|
+
language: params.language,
|
|
2135
|
+
};
|
|
2136
|
+
Object.entries(qualifiers).forEach(([key, value]) => {
|
|
2137
|
+
if (value)
|
|
2138
|
+
queryParts.push(`${key}:${value}`);
|
|
2139
|
+
});
|
|
2140
|
+
// Special qualifiers
|
|
2141
|
+
if (params.reviewedBy)
|
|
2142
|
+
queryParts.push(`reviewed-by:${params.reviewedBy}`);
|
|
2143
|
+
if (params.reviewRequested)
|
|
2144
|
+
queryParts.push(`review-requested:${params.reviewRequested}`);
|
|
2145
|
+
if (params.head)
|
|
2146
|
+
queryParts.push(`head:${params.head}`);
|
|
2147
|
+
if (params.base)
|
|
2148
|
+
queryParts.push(`base:${params.base}`);
|
|
2149
|
+
if (params.mergedAt)
|
|
2150
|
+
queryParts.push(`merged:${params.mergedAt}`);
|
|
2151
|
+
if (params.draft !== undefined)
|
|
2152
|
+
queryParts.push(`draft:${params.draft}`);
|
|
2153
|
+
if (params.checks)
|
|
2154
|
+
queryParts.push(`status:${params.checks}`);
|
|
2155
|
+
if (params.merged !== undefined)
|
|
2156
|
+
queryParts.push(`is:${params.merged ? 'merged' : 'unmerged'}`);
|
|
2157
|
+
if (params.review)
|
|
2158
|
+
queryParts.push(`review:${params.review}`);
|
|
2159
|
+
// Add type qualifier to search only pull requests
|
|
2160
|
+
queryParts.push('type:pr');
|
|
2161
|
+
const query = queryParts.filter(Boolean).join(' ');
|
|
2162
|
+
const limit = Math.min(params.limit || 25, 100);
|
|
2163
|
+
let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
|
|
2164
|
+
if (params.sort)
|
|
2165
|
+
apiPath += `&sort=${params.sort}`;
|
|
2166
|
+
if (params.order)
|
|
2167
|
+
apiPath += `&order=${params.order}`;
|
|
2168
|
+
return { command: 'api', args: [apiPath] };
|
|
2169
|
+
}
|
|
2170
|
+
|
|
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).`;
|
|
2733
2193
|
/**
|
|
2734
|
-
*
|
|
2735
|
-
* Attempts to fetch the default branch first, then falls back to common branches.
|
|
2194
|
+
* Extract owner/repo information from various query formats
|
|
2736
2195
|
*/
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
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
|
|
2750
2215
|
}
|
|
2751
2216
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
commonBranches.forEach(branch => {
|
|
2758
|
-
if (!branches.includes(branch)) {
|
|
2759
|
-
branches.push(branch);
|
|
2760
|
-
}
|
|
2761
|
-
});
|
|
2762
|
-
return branches;
|
|
2217
|
+
return {
|
|
2218
|
+
extractedOwner,
|
|
2219
|
+
extractedRepo,
|
|
2220
|
+
cleanedQuery: cleanedQuery || query, // Ensure original query is returned if cleaned is empty
|
|
2221
|
+
};
|
|
2763
2222
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
function registerSearchGitHubIssuesTool(server) {
|
|
2768
|
-
server.registerTool(TOOL_NAME$1, {
|
|
2769
|
-
description: DESCRIPTION$1,
|
|
2223
|
+
function registerSearchGitHubReposTool(server) {
|
|
2224
|
+
server.registerTool(GITHUB_SEARCH_REPOSITORIES_TOOL_NAME, {
|
|
2225
|
+
description: DESCRIPTION$2,
|
|
2770
2226
|
inputSchema: {
|
|
2771
2227
|
query: z
|
|
2772
2228
|
.string()
|
|
2773
|
-
.
|
|
2774
|
-
.describe('Search query
|
|
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)
|
|
2775
2232
|
owner: z
|
|
2776
|
-
.string()
|
|
2777
|
-
.min(1)
|
|
2233
|
+
.union([z.string(), z.array(z.string())])
|
|
2778
2234
|
.optional()
|
|
2779
|
-
.describe('Repository owner
|
|
2780
|
-
|
|
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
|
|
2781
2237
|
.string()
|
|
2782
2238
|
.optional()
|
|
2783
|
-
.describe('
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
.
|
|
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
|
+
])
|
|
2787
2245
|
.optional()
|
|
2788
|
-
.describe('
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
closed: z.string().optional().describe('Filter by closed date'),
|
|
2792
|
-
commenter: z
|
|
2793
|
-
.string()
|
|
2246
|
+
.describe('Stars filter. Supports ranges and thresholds.'),
|
|
2247
|
+
topic: z
|
|
2248
|
+
.union([z.string(), z.array(z.string())])
|
|
2794
2249
|
.optional()
|
|
2795
|
-
.describe('
|
|
2796
|
-
|
|
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
|
|
2797
2254
|
.number()
|
|
2798
2255
|
.optional()
|
|
2799
|
-
.describe('Filter by number of
|
|
2800
|
-
|
|
2801
|
-
|
|
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
|
|
2802
2263
|
.boolean()
|
|
2803
2264
|
.optional()
|
|
2804
|
-
.describe('
|
|
2805
|
-
|
|
2806
|
-
.
|
|
2265
|
+
.describe('Filter archived repositories (true/false).'),
|
|
2266
|
+
includeForks: z
|
|
2267
|
+
.enum(['false', 'true', 'only'])
|
|
2807
2268
|
.optional()
|
|
2808
|
-
.describe('
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
language: z.string().optional().describe('Filter by coding language'),
|
|
2812
|
-
locked: z
|
|
2813
|
-
.boolean()
|
|
2269
|
+
.describe('Include forks: false (exclude), true (include), only (forks only).'),
|
|
2270
|
+
visibility: z
|
|
2271
|
+
.enum(['public', 'private', 'internal'])
|
|
2814
2272
|
.optional()
|
|
2815
|
-
.describe('
|
|
2816
|
-
|
|
2817
|
-
|
|
2273
|
+
.describe('Repository visibility filter.'),
|
|
2274
|
+
// DATE & SIZE FILTERS
|
|
2275
|
+
created: z
|
|
2276
|
+
.string()
|
|
2818
2277
|
.optional()
|
|
2819
|
-
.describe('
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
noAssignee: z
|
|
2823
|
-
.boolean()
|
|
2278
|
+
.describe('Created date filter. Format: ">2020-01-01", "<2023-12-31".'),
|
|
2279
|
+
updated: z
|
|
2280
|
+
.string()
|
|
2824
2281
|
.optional()
|
|
2825
|
-
.describe('
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
.boolean()
|
|
2282
|
+
.describe('Updated date filter. Good for finding active projects.'),
|
|
2283
|
+
size: z
|
|
2284
|
+
.string()
|
|
2829
2285
|
.optional()
|
|
2830
|
-
.describe('
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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
|
+
])
|
|
2836
2295
|
.optional()
|
|
2837
|
-
.describe('Filter
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
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'])
|
|
2842
2310
|
.optional()
|
|
2843
|
-
.describe('
|
|
2311
|
+
.describe('Search scope: name, description, or readme content.'),
|
|
2312
|
+
// SORTING & LIMITS - Match CLI defaults exactly
|
|
2844
2313
|
sort: z
|
|
2845
2314
|
.enum([
|
|
2846
|
-
'
|
|
2847
|
-
'
|
|
2848
|
-
'
|
|
2849
|
-
'reactions',
|
|
2850
|
-
'reactions-+1',
|
|
2851
|
-
'reactions--1',
|
|
2852
|
-
'reactions-heart',
|
|
2853
|
-
'reactions-smile',
|
|
2854
|
-
'reactions-tada',
|
|
2855
|
-
'reactions-thinking_face',
|
|
2315
|
+
'forks',
|
|
2316
|
+
'help-wanted-issues',
|
|
2317
|
+
'stars',
|
|
2856
2318
|
'updated',
|
|
2857
2319
|
'best-match',
|
|
2858
2320
|
])
|
|
2859
2321
|
.optional()
|
|
2860
|
-
.
|
|
2322
|
+
.default('best-match')
|
|
2323
|
+
.describe('Sort criteria for results.'),
|
|
2861
2324
|
order: z
|
|
2862
2325
|
.enum(['asc', 'desc'])
|
|
2863
2326
|
.optional()
|
|
2864
2327
|
.default('desc')
|
|
2865
|
-
.describe('
|
|
2328
|
+
.describe('Sort order direction.'),
|
|
2866
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
|
+
},
|
|
2345
|
+
}, async (args) => {
|
|
2346
|
+
try {
|
|
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;
|
|
2359
|
+
}
|
|
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) {
|
|
2374
|
+
return createResult({
|
|
2375
|
+
error: SUGGESTIONS.REPO_SEARCH_PRIMARY_FILTER,
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
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;
|
|
2407
|
+
}
|
|
2408
|
+
catch (error) {
|
|
2409
|
+
return createResult({
|
|
2410
|
+
error: createSearchFailedError('repositories'),
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
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
|
|
2867
2578
|
.number()
|
|
2868
2579
|
.int()
|
|
2869
2580
|
.min(1)
|
|
2870
2581
|
.max(50)
|
|
2871
2582
|
.optional()
|
|
2872
|
-
.default(
|
|
2873
|
-
.describe('
|
|
2583
|
+
.default(20)
|
|
2584
|
+
.describe('Results limit per query (1-50). Default: 20'),
|
|
2874
2585
|
},
|
|
2875
2586
|
annotations: {
|
|
2876
|
-
title: '
|
|
2587
|
+
title: 'NPM Package Search',
|
|
2877
2588
|
readOnlyHint: true,
|
|
2878
2589
|
destructiveHint: false,
|
|
2879
2590
|
idempotentHint: true,
|
|
2880
2591
|
openWorldHint: true,
|
|
2881
2592
|
},
|
|
2882
2593
|
}, async (args) => {
|
|
2883
|
-
if (!args.query?.trim()) {
|
|
2884
|
-
return createResult({
|
|
2885
|
-
error: 'Search query is required and cannot be empty - provide keywords to search for issues',
|
|
2886
|
-
});
|
|
2887
|
-
}
|
|
2888
|
-
if (args.query.length > 256) {
|
|
2889
|
-
return createResult({
|
|
2890
|
-
error: 'Search query is too long. Please limit to 256 characters or less - simplify your search terms',
|
|
2891
|
-
});
|
|
2892
|
-
}
|
|
2893
2594
|
try {
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
const
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
});
|
|
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
|
+
}
|
|
2902
2607
|
}
|
|
2903
|
-
|
|
2608
|
+
const deduplicatedPackages = deduplicatePackages(allPackages);
|
|
2609
|
+
if (deduplicatedPackages.length > 0) {
|
|
2904
2610
|
return createResult({
|
|
2905
|
-
|
|
2611
|
+
data: {
|
|
2612
|
+
total_count: deduplicatedPackages.length,
|
|
2613
|
+
results: deduplicatedPackages,
|
|
2614
|
+
},
|
|
2906
2615
|
});
|
|
2907
2616
|
}
|
|
2908
|
-
// Generic fallback
|
|
2909
2617
|
return createResult({
|
|
2910
|
-
error: '
|
|
2618
|
+
error: createNoResultsError('packages'),
|
|
2911
2619
|
});
|
|
2912
2620
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
return withCache(cacheKey, async () => {
|
|
2918
|
-
const { command, args } = buildGitHubIssuesAPICommand(params);
|
|
2919
|
-
const result = await executeGitHubCommand(command, args, { cache: false });
|
|
2920
|
-
if (result.isError) {
|
|
2921
|
-
return result;
|
|
2621
|
+
catch (error) {
|
|
2622
|
+
return createResult({
|
|
2623
|
+
error: createSearchFailedError('packages'),
|
|
2624
|
+
});
|
|
2922
2625
|
}
|
|
2923
|
-
const execResult = JSON.parse(result.content[0].text);
|
|
2924
|
-
const apiResponse = JSON.parse(execResult.result);
|
|
2925
|
-
const issues = apiResponse.items || [];
|
|
2926
|
-
const cleanIssues = issues.map((issue) => ({
|
|
2927
|
-
number: issue.number,
|
|
2928
|
-
title: issue.title,
|
|
2929
|
-
state: issue.state,
|
|
2930
|
-
author: issue.user?.login || '',
|
|
2931
|
-
repository: issue.repository_url?.split('/').slice(-2).join('/') || 'unknown',
|
|
2932
|
-
labels: issue.labels?.map(l => l.name) || [],
|
|
2933
|
-
created_at: toDDMMYYYY(issue.created_at),
|
|
2934
|
-
updated_at: toDDMMYYYY(issue.updated_at),
|
|
2935
|
-
url: issue.html_url,
|
|
2936
|
-
comments: issue.comments,
|
|
2937
|
-
reactions: issue.reactions?.total_count || 0,
|
|
2938
|
-
}));
|
|
2939
|
-
const searchResult = {
|
|
2940
|
-
results: cleanIssues,
|
|
2941
|
-
total_count: apiResponse.total_count || cleanIssues.length,
|
|
2942
|
-
metadata: {
|
|
2943
|
-
incomplete_results: apiResponse.incomplete_results || false,
|
|
2944
|
-
},
|
|
2945
|
-
};
|
|
2946
|
-
return createResult({ data: searchResult });
|
|
2947
2626
|
});
|
|
2948
2627
|
}
|
|
2949
|
-
function
|
|
2950
|
-
const
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
/\blabel:("[^"]*"|[^\s]+)/gi,
|
|
2957
|
-
/\bcreated:([^\s]+)/gi,
|
|
2958
|
-
/\bupdated:([^\s]+)/gi,
|
|
2959
|
-
/\bauthor:([^\s]+)/gi,
|
|
2960
|
-
/\bassignee:([^\s]+)/gi,
|
|
2961
|
-
/\bstate:(open|closed)/gi,
|
|
2962
|
-
/\brepo:([^\s]+)/gi,
|
|
2963
|
-
/\borg:([^\s]+)/gi,
|
|
2964
|
-
];
|
|
2965
|
-
// Remove extracted qualifiers from base query
|
|
2966
|
-
let cleanQuery = baseQuery;
|
|
2967
|
-
qualifierPatterns.forEach(pattern => {
|
|
2968
|
-
cleanQuery = cleanQuery.replace(pattern, '').trim();
|
|
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;
|
|
2969
2635
|
});
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
assignee: params.assignee,
|
|
2985
|
-
mentions: params.mentions,
|
|
2986
|
-
commenter: params.commenter,
|
|
2987
|
-
involves: params.involves,
|
|
2988
|
-
language: params.language,
|
|
2989
|
-
state: params.state,
|
|
2990
|
-
created: params.created,
|
|
2991
|
-
updated: params.updated,
|
|
2992
|
-
closed: params.closed,
|
|
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;
|
|
2642
|
+
const keywords = pkg.keywords || [];
|
|
2643
|
+
const limitedKeywords = keywords.slice(0, MAX_KEYWORDS);
|
|
2644
|
+
return {
|
|
2645
|
+
name: pkg.name || '',
|
|
2646
|
+
version: pkg.version || '',
|
|
2647
|
+
description: truncatedDescription,
|
|
2648
|
+
keywords: limitedKeywords,
|
|
2649
|
+
repository: pkg.links?.repository || pkg.repository?.url || null,
|
|
2993
2650
|
};
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
queryParts.push(`milestone:"${params.milestone}"`);
|
|
3004
|
-
if (params.noAssignee)
|
|
3005
|
-
queryParts.push('no:assignee');
|
|
3006
|
-
if (params.noLabel)
|
|
3007
|
-
queryParts.push('no:label');
|
|
3008
|
-
if (params.noMilestone)
|
|
3009
|
-
queryParts.push('no:milestone');
|
|
3010
|
-
if (params.archived !== undefined)
|
|
3011
|
-
queryParts.push(`archived:${params.archived}`);
|
|
3012
|
-
if (params.locked)
|
|
3013
|
-
queryParts.push('is:locked');
|
|
3014
|
-
if (params.visibility)
|
|
3015
|
-
queryParts.push(`is:${params.visibility}`);
|
|
3016
|
-
// Extract qualifiers from original query and add them if not already set by params
|
|
3017
|
-
if (baseQuery.includes('is:') && !params.state) {
|
|
3018
|
-
const isMatch = baseQuery.match(/\bis:(open|closed)\b/i);
|
|
3019
|
-
if (isMatch && !queryParts.some(part => part.startsWith('state:'))) {
|
|
3020
|
-
queryParts.push(`state:${isMatch[1].toLowerCase()}`);
|
|
2651
|
+
}
|
|
2652
|
+
function parseNpmSearchOutput(output) {
|
|
2653
|
+
try {
|
|
2654
|
+
const wrapper = JSON.parse(output);
|
|
2655
|
+
const commandResult = wrapper.result;
|
|
2656
|
+
let packages = [];
|
|
2657
|
+
// Handle different npm search output formats
|
|
2658
|
+
if (Array.isArray(commandResult)) {
|
|
2659
|
+
packages = commandResult;
|
|
3021
2660
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
const labelMatch = baseQuery.match(/\blabel:("[^"]*"|[^\s]+)/i);
|
|
3025
|
-
if (labelMatch) {
|
|
3026
|
-
const labelValue = labelMatch[1].replace(/"/g, '');
|
|
3027
|
-
queryParts.push(`label:"${labelValue}"`);
|
|
2661
|
+
else if (commandResult?.objects && Array.isArray(commandResult.objects)) {
|
|
2662
|
+
packages = commandResult.objects.map((obj) => obj.package || obj);
|
|
3028
2663
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
const createdMatch = baseQuery.match(/\bcreated:([^\s]+)/i);
|
|
3032
|
-
if (createdMatch) {
|
|
3033
|
-
queryParts.push(`created:${createdMatch[1]}`);
|
|
2664
|
+
else if (commandResult?.results && Array.isArray(commandResult.results)) {
|
|
2665
|
+
packages = commandResult.results;
|
|
3034
2666
|
}
|
|
2667
|
+
return packages.map(normalizePackage);
|
|
2668
|
+
}
|
|
2669
|
+
catch (error) {
|
|
2670
|
+
logger.warn('Failed to parse NPM search results:', error);
|
|
2671
|
+
return [];
|
|
3035
2672
|
}
|
|
3036
|
-
const query = queryParts.filter(Boolean).join(' ');
|
|
3037
|
-
const limit = Math.min(params.limit || 25, 100);
|
|
3038
|
-
let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
|
|
3039
|
-
if (params.sort)
|
|
3040
|
-
apiPath += `&sort=${params.sort}`;
|
|
3041
|
-
if (params.order)
|
|
3042
|
-
apiPath += `&order=${params.order}`;
|
|
3043
|
-
return { command: 'api', args: [apiPath] };
|
|
3044
2673
|
}
|
|
3045
2674
|
|
|
3046
|
-
const
|
|
3047
|
-
const DESCRIPTION = `
|
|
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).`;
|
|
3048
2677
|
function registerNpmViewPackageTool(server) {
|
|
3049
|
-
server.registerTool(
|
|
2678
|
+
server.registerTool(NPM_VIEW_PACKAGE_TOOL_NAME, {
|
|
3050
2679
|
description: DESCRIPTION,
|
|
3051
2680
|
inputSchema: {
|
|
3052
2681
|
packageName: z
|
|
3053
2682
|
.string()
|
|
3054
2683
|
.min(1)
|
|
3055
|
-
.describe('NPM package name
|
|
2684
|
+
.describe('NPM package name (e.g., "react", "express", "@types/node")'),
|
|
3056
2685
|
},
|
|
3057
2686
|
annotations: {
|
|
3058
|
-
title: 'NPM Package
|
|
2687
|
+
title: 'NPM Package Analyzer',
|
|
3059
2688
|
readOnlyHint: true,
|
|
3060
2689
|
destructiveHint: false,
|
|
3061
2690
|
idempotentHint: true,
|
|
@@ -3068,7 +2697,7 @@ function registerNpmViewPackageTool(server) {
|
|
|
3068
2697
|
return result;
|
|
3069
2698
|
}
|
|
3070
2699
|
const execResult = JSON.parse(result.content[0].text);
|
|
3071
|
-
const packageData =
|
|
2700
|
+
const packageData = execResult.result;
|
|
3072
2701
|
// Transform to optimized format
|
|
3073
2702
|
const optimizedResult = transformToOptimizedFormat(packageData);
|
|
3074
2703
|
return createResult({ data: optimizedResult });
|
|
@@ -3077,30 +2706,22 @@ function registerNpmViewPackageTool(server) {
|
|
|
3077
2706
|
const errorMessage = error.message || '';
|
|
3078
2707
|
if (errorMessage.includes('not found')) {
|
|
3079
2708
|
return createResult({
|
|
3080
|
-
error: 'Package not found
|
|
3081
|
-
cli_command: `npm view ${args.packageName} --json`,
|
|
2709
|
+
error: 'Package not found. Check spelling and use exact package name from npm',
|
|
3082
2710
|
});
|
|
3083
2711
|
}
|
|
3084
2712
|
if (errorMessage.includes('network')) {
|
|
3085
2713
|
return createResult({
|
|
3086
|
-
error: 'Network error
|
|
3087
|
-
cli_command: `npm view ${args.packageName} --json`,
|
|
2714
|
+
error: 'Network error. Check internet connection and try again',
|
|
3088
2715
|
});
|
|
3089
2716
|
}
|
|
3090
2717
|
return createResult({
|
|
3091
|
-
error: '
|
|
3092
|
-
cli_command: `npm view ${args.packageName} --json`,
|
|
3093
|
-
suggestions: [
|
|
3094
|
-
'Verify package name is correct',
|
|
3095
|
-
'Check if package exists on npmjs.com',
|
|
3096
|
-
'Try again in a moment',
|
|
3097
|
-
],
|
|
2718
|
+
error: 'Failed to fetch package information. Try again or check npm status',
|
|
3098
2719
|
});
|
|
3099
2720
|
}
|
|
3100
2721
|
});
|
|
3101
2722
|
}
|
|
3102
2723
|
/**
|
|
3103
|
-
* Transform NPM CLI response to optimized format
|
|
2724
|
+
* Transform NPM CLI response to optimized format for code analysis
|
|
3104
2725
|
*/
|
|
3105
2726
|
function transformToOptimizedFormat(packageData) {
|
|
3106
2727
|
// Extract repository URL and simplify
|
|
@@ -3112,7 +2733,9 @@ function transformToOptimizedFormat(packageData) {
|
|
|
3112
2733
|
: undefined;
|
|
3113
2734
|
// Get version timestamps from time object and limit to last 5
|
|
3114
2735
|
const timeData = packageData.time || {};
|
|
3115
|
-
const versionList = packageData.versions
|
|
2736
|
+
const versionList = Array.isArray(packageData.versions)
|
|
2737
|
+
? packageData.versions
|
|
2738
|
+
: [];
|
|
3116
2739
|
const recentVersions = versionList.slice(-5).map((version) => ({
|
|
3117
2740
|
version,
|
|
3118
2741
|
date: timeData[version] ? toDDMMYYYY(timeData[version]) : 'Unknown',
|
|
@@ -3139,7 +2762,7 @@ function transformToOptimizedFormat(packageData) {
|
|
|
3139
2762
|
return result;
|
|
3140
2763
|
}
|
|
3141
2764
|
/**
|
|
3142
|
-
* Simplify exports
|
|
2765
|
+
* Simplify exports to show only essential entry points for code navigation
|
|
3143
2766
|
*/
|
|
3144
2767
|
function simplifyExports(exports) {
|
|
3145
2768
|
if (typeof exports === 'string') {
|
|
@@ -3160,8 +2783,9 @@ function simplifyExports(exports) {
|
|
|
3160
2783
|
simplified.main = mainExport.import;
|
|
3161
2784
|
}
|
|
3162
2785
|
}
|
|
3163
|
-
// Extract types if available
|
|
3164
|
-
if (exports['./types'] ||
|
|
2786
|
+
// Extract types if available with safe property access
|
|
2787
|
+
if (exports['./types'] ||
|
|
2788
|
+
(exports['.'] && typeof exports['.'] === 'object' && exports['.'].types)) {
|
|
3165
2789
|
simplified.types = exports['./types'] || exports['.'].types;
|
|
3166
2790
|
}
|
|
3167
2791
|
// Add a few other important exports (max 3 total)
|
|
@@ -3192,93 +2816,257 @@ async function viewNpmPackage(packageName) {
|
|
|
3192
2816
|
const errorMessage = error.message || '';
|
|
3193
2817
|
if (errorMessage.includes('404')) {
|
|
3194
2818
|
return createResult({
|
|
3195
|
-
error: 'Package not found on NPM registry',
|
|
2819
|
+
error: 'Package not found on NPM registry. Verify the exact package name',
|
|
3196
2820
|
});
|
|
3197
2821
|
}
|
|
3198
2822
|
return createResult({
|
|
3199
|
-
error: 'NPM command
|
|
2823
|
+
error: 'Failed to execute NPM command. Check npm installation',
|
|
3200
2824
|
});
|
|
3201
2825
|
}
|
|
3202
2826
|
});
|
|
3203
2827
|
}
|
|
3204
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
|
+
|
|
3205
2963
|
const SERVER_CONFIG = {
|
|
3206
2964
|
name: 'octocode-mcp',
|
|
3207
2965
|
version: '1.0.0',
|
|
3208
|
-
description:
|
|
3209
|
-
Specialized in architectural analysis, algorithm explanations, and complete technical documentation.`,
|
|
2966
|
+
description: PROMPT_SYSTEM_PROMPT,
|
|
3210
2967
|
};
|
|
3211
2968
|
function registerAllTools(server) {
|
|
3212
2969
|
const toolRegistrations = [
|
|
3213
|
-
{ name:
|
|
3214
|
-
{ name:
|
|
2970
|
+
{ name: API_STATUS_CHECK_TOOL_NAME, fn: registerApiStatusCheckTool },
|
|
2971
|
+
{ name: GITHUB_SEARCH_CODE_TOOL_NAME, fn: registerGitHubSearchCodeTool },
|
|
3215
2972
|
{
|
|
3216
|
-
name:
|
|
2973
|
+
name: GITHUB_GET_FILE_CONTENT_TOOL_NAME,
|
|
3217
2974
|
fn: registerFetchGitHubFileContentTool,
|
|
3218
2975
|
},
|
|
3219
|
-
{ name: 'SearchGitHubRepos', fn: registerSearchGitHubReposTool },
|
|
3220
|
-
{ name: 'SearchGitHubCommits', fn: registerGitHubSearchCommitsTool },
|
|
3221
2976
|
{
|
|
3222
|
-
name:
|
|
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,
|
|
3223
2986
|
fn: registerSearchGitHubPullRequestsTool,
|
|
3224
2987
|
},
|
|
3225
|
-
{ name:
|
|
2988
|
+
{ name: NPM_PACKAGE_SEARCH_TOOL_NAME, fn: registerNpmSearchTool },
|
|
3226
2989
|
{
|
|
3227
|
-
name:
|
|
2990
|
+
name: GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME,
|
|
3228
2991
|
fn: registerViewRepositoryStructureTool,
|
|
3229
2992
|
},
|
|
3230
|
-
{
|
|
3231
|
-
|
|
2993
|
+
{
|
|
2994
|
+
name: GITHUB_SEARCH_ISSUES_TOOL_NAME,
|
|
2995
|
+
fn: registerSearchGitHubIssuesTool,
|
|
2996
|
+
},
|
|
2997
|
+
{ name: NPM_VIEW_PACKAGE_TOOL_NAME, fn: registerNpmViewPackageTool },
|
|
3232
2998
|
];
|
|
2999
|
+
logger.info(`Registering ${toolRegistrations.length} tools...`);
|
|
3000
|
+
let successCount = 0;
|
|
3233
3001
|
for (const tool of toolRegistrations) {
|
|
3234
3002
|
try {
|
|
3003
|
+
logger.debug(`Registering tool: ${tool.name}`);
|
|
3235
3004
|
tool.fn(server);
|
|
3005
|
+
successCount++;
|
|
3006
|
+
logger.info(`✓ Successfully registered: ${tool.name}`);
|
|
3236
3007
|
}
|
|
3237
3008
|
catch (error) {
|
|
3238
|
-
|
|
3009
|
+
logger.error(`✗ Failed to register ${tool.name}:`, error);
|
|
3010
|
+
// Continue with other tools instead of failing completely
|
|
3239
3011
|
}
|
|
3240
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`);
|
|
3241
3017
|
}
|
|
3242
3018
|
async function startServer() {
|
|
3243
3019
|
try {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
tools: {},
|
|
3247
|
-
resources: {},
|
|
3248
|
-
prompts: {},
|
|
3249
|
-
},
|
|
3250
|
-
instructions: `
|
|
3251
|
-
${PROMPT_SYSTEM_PROMPT}
|
|
3252
|
-
`,
|
|
3253
|
-
});
|
|
3020
|
+
logger.info('Creating MCP server...');
|
|
3021
|
+
const server = new McpServer(SERVER_CONFIG);
|
|
3254
3022
|
registerAllTools(server);
|
|
3255
3023
|
const transport = new StdioServerTransport();
|
|
3256
3024
|
await server.connect(transport);
|
|
3257
|
-
|
|
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) => {
|
|
3258
3030
|
try {
|
|
3259
|
-
|
|
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
|
+
]);
|
|
3260
3038
|
process.exit(0);
|
|
3261
3039
|
}
|
|
3262
3040
|
catch (error) {
|
|
3041
|
+
logger.error('Error during shutdown:', error);
|
|
3263
3042
|
process.exit(1);
|
|
3264
3043
|
}
|
|
3265
3044
|
};
|
|
3045
|
+
// Handle process signals
|
|
3266
3046
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3267
3047
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3048
|
+
// Handle stdin close (important for MCP)
|
|
3268
3049
|
process.stdin.on('close', async () => {
|
|
3269
3050
|
await gracefulShutdown('STDIN_CLOSE');
|
|
3270
3051
|
});
|
|
3271
|
-
|
|
3052
|
+
// Handle uncaught errors
|
|
3053
|
+
process.on('uncaughtException', error => {
|
|
3054
|
+
logger.error('Uncaught exception:', error);
|
|
3272
3055
|
gracefulShutdown('UNCAUGHT_EXCEPTION');
|
|
3273
3056
|
});
|
|
3274
|
-
process.on('unhandledRejection', () => {
|
|
3057
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
3058
|
+
logger.error('Unhandled rejection at:', promise, 'reason:', reason);
|
|
3275
3059
|
gracefulShutdown('UNHANDLED_REJECTION');
|
|
3276
3060
|
});
|
|
3061
|
+
// Keep process alive
|
|
3062
|
+
process.stdin.resume();
|
|
3277
3063
|
}
|
|
3278
3064
|
catch (error) {
|
|
3065
|
+
logger.error('Error details:', error);
|
|
3279
3066
|
process.exit(1);
|
|
3280
3067
|
}
|
|
3281
3068
|
}
|
|
3282
|
-
startServer().catch(
|
|
3069
|
+
startServer().catch(error => {
|
|
3070
|
+
logger.error('Error:', error);
|
|
3283
3071
|
process.exit(1);
|
|
3284
3072
|
});
|