octocode-mcp 2.3.2 → 2.3.5
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/README.md +50 -20
- package/build/index.js +2169 -1210
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -3,79 +3,44 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { exec } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
|
+
import { platform } from 'os';
|
|
6
7
|
import NodeCache from 'node-cache';
|
|
7
8
|
import crypto from 'crypto';
|
|
8
9
|
import z from 'zod';
|
|
9
10
|
|
|
10
|
-
const PROMPT_SYSTEM_PROMPT = `
|
|
11
|
-
You leverage powerful semantic search using GitHub (gh) and NPM CLI for code discovery.
|
|
11
|
+
const PROMPT_SYSTEM_PROMPT = `Expert code research assistant for GitHub/NPM ecosystems (public/private). Use powerful semantic search for efficient code discovery.
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Prioritize efficient, targeted searches with smart fallbacks
|
|
25
|
-
- Use strategic tool combinations for comprehensive results an
|
|
26
|
-
- Balance speed vs throughness based on query type
|
|
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
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- Always provide referenced code snippets and documentation
|
|
39
|
-
- Search docs (README.md) for quality information about code flow and architecture`;
|
|
39
|
+
CLI Help using CLI (use when needed to check failures)
|
|
40
|
+
gh <command> --help
|
|
41
|
+
npm <command> --help
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
const text = isError
|
|
43
|
-
? `${data}${''}`
|
|
44
|
-
: JSON.stringify(data, null, 2);
|
|
45
|
-
return {
|
|
46
|
-
content: [{ type: 'text', text }],
|
|
47
|
-
isError,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
// LEGACY SUPPORT - Remove these once all tools are updated
|
|
51
|
-
function createSuccessResult$1(data) {
|
|
52
|
-
return createResult(data, false);
|
|
53
|
-
}
|
|
54
|
-
function createErrorResult$1(message, error) {
|
|
55
|
-
return createResult(`${message}: ${error.message}`, true);
|
|
56
|
-
}
|
|
57
|
-
// ENHANCED PARSING UTILITY
|
|
58
|
-
function parseJsonResponse(responseText, fallback = null) {
|
|
59
|
-
try {
|
|
60
|
-
const data = JSON.parse(responseText);
|
|
61
|
-
return { data, parsed: true };
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return { data: (fallback || responseText), parsed: false };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Determines if a string needs quoting for GitHub search
|
|
69
|
-
*/
|
|
70
|
-
//TODO: move to util.ts
|
|
71
|
-
function needsQuoting(str) {
|
|
72
|
-
return (str.includes(' ') ||
|
|
73
|
-
str.includes('"') ||
|
|
74
|
-
str.includes('\t') ||
|
|
75
|
-
str.includes('\n') ||
|
|
76
|
-
str.includes('\r') ||
|
|
77
|
-
/[<>(){}[\]\\|&;]/.test(str));
|
|
78
|
-
}
|
|
43
|
+
Always provide code snippets and documentation references.`;
|
|
79
44
|
|
|
80
45
|
const VERSION = 'v1';
|
|
81
46
|
const cache = new NodeCache({
|
|
@@ -133,6 +98,113 @@ function isValidNpmCommand(command) {
|
|
|
133
98
|
function isValidGhCommand(command) {
|
|
134
99
|
return ALLOWED_GH_COMMANDS.includes(command);
|
|
135
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get platform-specific shell configuration with improved shell detection
|
|
103
|
+
*/
|
|
104
|
+
function getShellConfig(preferredWindowsShell) {
|
|
105
|
+
const isWindows = platform() === 'win32';
|
|
106
|
+
if (!isWindows) {
|
|
107
|
+
// Use user's actual shell instead of hardcoded /bin/sh to avoid alias/function conflicts
|
|
108
|
+
const userShell = process.env.SHELL || '/bin/sh';
|
|
109
|
+
return {
|
|
110
|
+
shell: userShell,
|
|
111
|
+
shellEnv: userShell,
|
|
112
|
+
type: 'unix',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Windows shell selection
|
|
116
|
+
const usesPowerShell = preferredWindowsShell === 'powershell';
|
|
117
|
+
if (usesPowerShell) {
|
|
118
|
+
return {
|
|
119
|
+
shell: 'powershell.exe',
|
|
120
|
+
shellEnv: 'powershell.exe',
|
|
121
|
+
type: 'powershell',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
shell: 'cmd.exe',
|
|
126
|
+
shellEnv: 'cmd.exe',
|
|
127
|
+
type: 'cmd',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Checks if a query contains GitHub search boolean operators
|
|
132
|
+
*/
|
|
133
|
+
function hasGitHubBooleanOperators(query) {
|
|
134
|
+
// Check for GitHub boolean operators (must be uppercase)
|
|
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) {
|
|
143
|
+
// Auto-detect shell type if not provided
|
|
144
|
+
if (!shellType) {
|
|
145
|
+
const isWindows = platform() === 'win32';
|
|
146
|
+
shellType = isWindows ? 'cmd' : 'unix';
|
|
147
|
+
}
|
|
148
|
+
switch (shellType) {
|
|
149
|
+
case 'powershell':
|
|
150
|
+
return escapePowerShellArg(arg);
|
|
151
|
+
case 'cmd':
|
|
152
|
+
return escapeWindowsCmdArg(arg);
|
|
153
|
+
case 'unix':
|
|
154
|
+
default:
|
|
155
|
+
return escapeUnixShellArg(arg, isGitHubQuery);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Escape arguments for PowerShell
|
|
160
|
+
* PowerShell uses single quotes for literal strings and has special escaping rules
|
|
161
|
+
*/
|
|
162
|
+
function escapePowerShellArg(arg) {
|
|
163
|
+
// PowerShell special characters that need escaping
|
|
164
|
+
if (/[\s&<>|;`$@"'()[\]{}]/.test(arg)) {
|
|
165
|
+
// Use single quotes for literal strings in PowerShell
|
|
166
|
+
// Escape single quotes by doubling them
|
|
167
|
+
return `'${arg.replace(/'/g, "''")}'`;
|
|
168
|
+
}
|
|
169
|
+
return arg;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Escape arguments for Windows CMD
|
|
173
|
+
*/
|
|
174
|
+
function escapeWindowsCmdArg(arg) {
|
|
175
|
+
// Windows CMD escaping
|
|
176
|
+
if (/[\s&<>|^"]/.test(arg)) {
|
|
177
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
178
|
+
}
|
|
179
|
+
return arg;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Escape arguments for Unix shells with special handling for GitHub CLI queries
|
|
183
|
+
*/
|
|
184
|
+
function escapeUnixShellArg(arg, isGitHubQuery) {
|
|
185
|
+
// Special handling for GitHub CLI search queries to preserve boolean logic
|
|
186
|
+
if (isGitHubQuery && hasGitHubBooleanOperators(arg)) {
|
|
187
|
+
// For boolean queries with OR/AND/NOT, use double quotes to preserve operators
|
|
188
|
+
// GitHub CLI handles boolean logic properly when quoted
|
|
189
|
+
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
190
|
+
}
|
|
191
|
+
// For GitHub queries with spaces but no boolean operators, quote them
|
|
192
|
+
if (isGitHubQuery &&
|
|
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
|
|
201
|
+
}
|
|
202
|
+
// Standard Unix shell escaping for other arguments
|
|
203
|
+
if (/[^\w\-._/:=@]/.test(arg)) {
|
|
204
|
+
return `'${arg.replace(/'/g, "'\"'\"'")}'`;
|
|
205
|
+
}
|
|
206
|
+
return arg;
|
|
207
|
+
}
|
|
136
208
|
/**
|
|
137
209
|
* Execute NPM commands safely by validating against allowed commands
|
|
138
210
|
* Security: Only executes commands that start with "npm {ALLOWED_COMMAND}"
|
|
@@ -142,74 +214,94 @@ async function executeNpmCommand(command, args = [], options = {}) {
|
|
|
142
214
|
if (!isValidNpmCommand(command)) {
|
|
143
215
|
return createErrorResult('Command not registered', new Error(`NPM command '${command}' is not in the allowed list`));
|
|
144
216
|
}
|
|
217
|
+
// Get shell configuration
|
|
218
|
+
const shellConfig = getShellConfig(options.windowsShell);
|
|
145
219
|
// Build command with validated prefix and properly escaped arguments
|
|
146
|
-
const escapedArgs = args.map(escapeShellArg);
|
|
220
|
+
const escapedArgs = args.map(arg => escapeShellArg(arg, shellConfig.type));
|
|
147
221
|
const fullCommand = `npm ${command} ${escapedArgs.join(' ')}`;
|
|
148
|
-
const executeNpmCommand = () => executeCommand(fullCommand, 'npm', options);
|
|
222
|
+
const executeNpmCommand = () => executeCommand(fullCommand, 'npm', options, shellConfig);
|
|
149
223
|
if (options.cache) {
|
|
150
|
-
const cacheKey = generateCacheKey('npm-exec', {
|
|
224
|
+
const cacheKey = generateCacheKey('npm-exec', {
|
|
225
|
+
command,
|
|
226
|
+
args,
|
|
227
|
+
shell: shellConfig.type,
|
|
228
|
+
});
|
|
151
229
|
return withCache(cacheKey, executeNpmCommand);
|
|
152
230
|
}
|
|
153
231
|
return executeNpmCommand();
|
|
154
232
|
}
|
|
155
233
|
/**
|
|
156
|
-
*
|
|
157
|
-
*/
|
|
158
|
-
function escapeShellArg(arg) {
|
|
159
|
-
// If the argument contains special characters, wrap it in single quotes
|
|
160
|
-
// and escape any single quotes within the argument
|
|
161
|
-
if (/[^\w\-._/:=@]/.test(arg)) {
|
|
162
|
-
return `'${arg.replace(/'/g, "'\"'\"'")}'`;
|
|
163
|
-
}
|
|
164
|
-
return arg;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Execute GitHub CLI commands safely by validating against allowed commands
|
|
168
|
-
* Security: Only executes commands that start with "gh {ALLOWED_COMMAND}"
|
|
234
|
+
* Execute GitHub CLI commands safely with improved boolean query handling
|
|
169
235
|
*/
|
|
170
236
|
async function executeGitHubCommand(command, args = [], options = {}) {
|
|
171
237
|
// Security check: only allow registered commands
|
|
172
238
|
if (!isValidGhCommand(command)) {
|
|
173
239
|
return createErrorResult('Command not registered', new Error(`GitHub command '${command}' is not in the allowed list`));
|
|
174
240
|
}
|
|
241
|
+
// Get shell configuration
|
|
242
|
+
const shellConfig = getShellConfig(options.windowsShell);
|
|
175
243
|
// Build command with validated prefix and properly escaped arguments
|
|
176
|
-
|
|
244
|
+
// First argument is typically the search query for GitHub CLI search commands
|
|
245
|
+
const escapedArgs = args.map((arg, index) => {
|
|
246
|
+
const isFirstArg = index === 0;
|
|
247
|
+
// Detect if this is a search query (first non-subcommand argument)
|
|
248
|
+
const isSearchQuery = command === 'search' && isFirstArg && !arg.startsWith('--');
|
|
249
|
+
return escapeShellArg(arg, shellConfig.type, isSearchQuery);
|
|
250
|
+
});
|
|
177
251
|
const fullCommand = `gh ${command} ${escapedArgs.join(' ')}`;
|
|
178
|
-
const executeGhCommand = () => executeCommand(fullCommand, 'github', options);
|
|
252
|
+
const executeGhCommand = () => executeCommand(fullCommand, 'github', options, shellConfig);
|
|
179
253
|
if (options.cache) {
|
|
180
|
-
const cacheKey = generateCacheKey('gh-exec', {
|
|
254
|
+
const cacheKey = generateCacheKey('gh-exec', {
|
|
255
|
+
command,
|
|
256
|
+
args,
|
|
257
|
+
shell: shellConfig.type,
|
|
258
|
+
});
|
|
181
259
|
return withCache(cacheKey, executeGhCommand);
|
|
182
260
|
}
|
|
183
261
|
return executeGhCommand();
|
|
184
262
|
}
|
|
185
263
|
/**
|
|
186
|
-
* Execute shell commands with
|
|
187
|
-
* Security: Should only be called with pre-validated command prefixes
|
|
264
|
+
* Execute shell commands with improved environment handling and error detection
|
|
188
265
|
*/
|
|
189
|
-
async function executeCommand(fullCommand, type, options = {}) {
|
|
266
|
+
async function executeCommand(fullCommand, type, options = {}, shellConfig) {
|
|
190
267
|
try {
|
|
191
268
|
const defaultTimeout = type === 'npm' ? 30000 : 60000;
|
|
269
|
+
const config = shellConfig || getShellConfig(options.windowsShell);
|
|
192
270
|
const execOptions = {
|
|
193
271
|
timeout: options.timeout || defaultTimeout,
|
|
272
|
+
maxBuffer: 5 * 1024 * 1024, // 5MB buffer limit (increased from default 1MB)
|
|
194
273
|
cwd: options.cwd,
|
|
195
274
|
env: {
|
|
196
275
|
...process.env,
|
|
197
276
|
...options.env,
|
|
198
|
-
//
|
|
199
|
-
SHELL:
|
|
277
|
+
// More conservative shell environment handling
|
|
278
|
+
SHELL: config.shellEnv,
|
|
200
279
|
PATH: process.env.PATH,
|
|
280
|
+
// Only disable problematic shell features, not all of them
|
|
281
|
+
...(config.type === 'unix' && {
|
|
282
|
+
// Only disable the most problematic shell configurations
|
|
283
|
+
BASH_ENV: '', // Prevent auto-sourcing of problematic configs
|
|
284
|
+
}),
|
|
201
285
|
},
|
|
202
286
|
encoding: 'utf-8',
|
|
203
|
-
shell:
|
|
287
|
+
shell: config.shell,
|
|
204
288
|
};
|
|
205
289
|
const { stdout, stderr } = await safeExecAsync(fullCommand, execOptions);
|
|
206
|
-
//
|
|
290
|
+
// Improved error detection that ignores shell configuration conflicts
|
|
207
291
|
const shouldTreatAsError = type === 'npm'
|
|
208
|
-
? stderr &&
|
|
292
|
+
? stderr &&
|
|
293
|
+
!stderr.includes('npm WARN') &&
|
|
294
|
+
!stderr.includes('npm notice')
|
|
209
295
|
: stderr &&
|
|
210
296
|
!stderr.includes('Warning:') &&
|
|
211
297
|
!stderr.includes('notice:') &&
|
|
212
|
-
|
|
298
|
+
// Ignore shell configuration conflicts - common in development environments
|
|
299
|
+
!stderr.includes('No such file or directory') &&
|
|
300
|
+
!stderr.includes('head: illegal option') &&
|
|
301
|
+
!stderr.includes('head: |: No such file or directory') &&
|
|
302
|
+
!stderr.includes('head: cat: No such file or directory') &&
|
|
303
|
+
!/^head:\s+/.test(stderr) && // Ignore all head command errors (shell conflicts)
|
|
304
|
+
!/^\s*head:\s+/.test(stderr) && // Ignore head errors with leading whitespace
|
|
213
305
|
stderr.trim() !== '';
|
|
214
306
|
if (shouldTreatAsError) {
|
|
215
307
|
const errorType = type === 'npm' ? 'NPM command error' : 'GitHub CLI command error';
|
|
@@ -220,6 +312,9 @@ async function executeCommand(fullCommand, type, options = {}) {
|
|
|
220
312
|
result: stdout,
|
|
221
313
|
timestamp: new Date().toISOString(),
|
|
222
314
|
type,
|
|
315
|
+
platform: platform(),
|
|
316
|
+
shell: config.shell,
|
|
317
|
+
shellType: config.type,
|
|
223
318
|
...(stderr && { warning: stderr }), // Include warnings but don't treat as error
|
|
224
319
|
});
|
|
225
320
|
}
|
|
@@ -231,17 +326,110 @@ async function executeCommand(fullCommand, type, options = {}) {
|
|
|
231
326
|
}
|
|
232
327
|
}
|
|
233
328
|
|
|
329
|
+
function createResult(options) {
|
|
330
|
+
const { data, error, suggestions, cli_command } = options;
|
|
331
|
+
if (error) {
|
|
332
|
+
const errorMessage = typeof error === 'string'
|
|
333
|
+
? error
|
|
334
|
+
: error.message || 'Unknown error';
|
|
335
|
+
// Build error response with optional CLI command
|
|
336
|
+
const errorResponse = { error: errorMessage };
|
|
337
|
+
if (suggestions) {
|
|
338
|
+
errorResponse.suggestions = suggestions;
|
|
339
|
+
}
|
|
340
|
+
if (cli_command) {
|
|
341
|
+
errorResponse.cli_command = cli_command;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: 'text', text: JSON.stringify(errorResponse, null, 2) }],
|
|
345
|
+
isError: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// For successful responses, include cli_command if provided (for debugging)
|
|
349
|
+
if (cli_command && data && typeof data === 'object') {
|
|
350
|
+
const dataWithCli = { ...data, cli_command };
|
|
351
|
+
return {
|
|
352
|
+
content: [{ type: 'text', text: JSON.stringify(dataWithCli, null, 2) }],
|
|
353
|
+
isError: false,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
358
|
+
isError: false,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Convert ISO timestamp to DDMMYYYY format
|
|
363
|
+
*/
|
|
364
|
+
function toDDMMYYYY(timestamp) {
|
|
365
|
+
const date = new Date(timestamp);
|
|
366
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
367
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
368
|
+
const year = date.getFullYear();
|
|
369
|
+
return `${day}/${month}/${year}`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Convert repository URL to owner/repo format
|
|
373
|
+
*/
|
|
374
|
+
function simplifyRepoUrl(url) {
|
|
375
|
+
const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
|
|
376
|
+
return match ? match[1] : url;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Extract first line of commit message
|
|
380
|
+
*/
|
|
381
|
+
function getCommitTitle(message) {
|
|
382
|
+
return message.split('\n')[0].trim();
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Convert bytes to human readable format
|
|
386
|
+
*/
|
|
387
|
+
function humanizeBytes(bytes) {
|
|
388
|
+
if (bytes === 0)
|
|
389
|
+
return '0 B';
|
|
390
|
+
const k = 1024;
|
|
391
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
392
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
393
|
+
return `${Math.round(bytes / Math.pow(k, i))} ${sizes[i]}`;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Simplify GitHub URL to relative path
|
|
397
|
+
*/
|
|
398
|
+
function simplifyGitHubUrl(url) {
|
|
399
|
+
const match = url.match(/github\.com\/[^/]+\/[^/]+\/(?:blob|commit)\/[^/]+\/(.+)$/);
|
|
400
|
+
return match ? match[1] : url;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Clean and optimize text match context
|
|
404
|
+
*/
|
|
405
|
+
function optimizeTextMatch(fragment, maxLength = 100) {
|
|
406
|
+
// Remove excessive whitespace and normalize
|
|
407
|
+
const cleaned = fragment.replace(/\s+/g, ' ').trim();
|
|
408
|
+
if (cleaned.length <= maxLength) {
|
|
409
|
+
return cleaned;
|
|
410
|
+
}
|
|
411
|
+
// Try to cut at word boundary
|
|
412
|
+
const truncated = cleaned.substring(0, maxLength);
|
|
413
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
414
|
+
if (lastSpace > maxLength * 0.7) {
|
|
415
|
+
return truncated.substring(0, lastSpace) + '…';
|
|
416
|
+
}
|
|
417
|
+
return truncated + '…';
|
|
418
|
+
}
|
|
419
|
+
|
|
234
420
|
const TOOL_NAME$9 = 'api_status_check';
|
|
235
|
-
const DESCRIPTION$9 = `
|
|
236
|
-
Use when user asks on specific implementaon in his organization (e.g. - I work at Wix, or search Wix code about X) or when cli is failing.`;
|
|
421
|
+
const DESCRIPTION$9 = `Get GitHub organizations list and check CLI authentication status. Use when searching private repos or when CLI tools fail.`;
|
|
237
422
|
function registerApiStatusCheckTool(server) {
|
|
238
|
-
server.
|
|
239
|
-
title: 'Check API Connections and Github Organizations',
|
|
423
|
+
server.registerTool(TOOL_NAME$9, {
|
|
240
424
|
description: DESCRIPTION$9,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
425
|
+
inputSchema: {},
|
|
426
|
+
annotations: {
|
|
427
|
+
title: 'Check API Connections and Github Organizations',
|
|
428
|
+
readOnlyHint: true,
|
|
429
|
+
destructiveHint: false,
|
|
430
|
+
idempotentHint: true,
|
|
431
|
+
openWorldHint: false,
|
|
432
|
+
},
|
|
245
433
|
}, async () => {
|
|
246
434
|
try {
|
|
247
435
|
let githubConnected = false;
|
|
@@ -341,111 +529,131 @@ function registerApiStatusCheckTool(server) {
|
|
|
341
529
|
}
|
|
342
530
|
npmConnected = false;
|
|
343
531
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
532
|
+
// Build structured response object
|
|
533
|
+
const loginStatus = {
|
|
534
|
+
login: {
|
|
535
|
+
github: {
|
|
536
|
+
connected: githubConnected,
|
|
537
|
+
user_organizations: organizations,
|
|
538
|
+
},
|
|
539
|
+
npm: {
|
|
540
|
+
connected: npmConnected,
|
|
541
|
+
registry: registry || 'https://registry.npmjs.org/',
|
|
542
|
+
},
|
|
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
|
+
],
|
|
352
546
|
},
|
|
353
|
-
}
|
|
547
|
+
};
|
|
548
|
+
return createResult({ data: loginStatus });
|
|
354
549
|
}
|
|
355
550
|
catch (error) {
|
|
356
|
-
return createResult(
|
|
551
|
+
return createResult({
|
|
552
|
+
error: `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
|
+
});
|
|
357
554
|
}
|
|
358
555
|
});
|
|
359
556
|
}
|
|
360
557
|
|
|
361
558
|
const TOOL_NAME$8 = 'github_search_code';
|
|
362
|
-
const DESCRIPTION$8 = `Search code across GitHub repositories
|
|
559
|
+
const DESCRIPTION$8 = `Search code across GitHub repositories with powerful GitHub search syntax and advanced filtering.
|
|
363
560
|
|
|
364
|
-
|
|
561
|
+
BOOLEAN LOGIC (MOST POWERFUL):
|
|
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
|
|
365
566
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
370
571
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
• Best for: Finding specific combinations, technology intersections
|
|
374
|
-
• Scope: RESTRICTIVE - only files containing ALL terms
|
|
572
|
+
TRADITIONAL FILTERS (ALSO SUPPORTED):
|
|
573
|
+
- language: "javascript", owner: "microsoft", filename: "package.json"
|
|
375
574
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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, {
|
|
579
|
+
description: DESCRIPTION$8,
|
|
580
|
+
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".
|
|
380
585
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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.
|
|
384
592
|
|
|
385
|
-
|
|
593
|
+
POPULAR: javascript, typescript, python, java, go, rust, php, ruby, swift, kotlin, dart
|
|
594
|
+
SYSTEMS: c, cpp, assembly, shell, dockerfile, yaml`),
|
|
595
|
+
owner: z
|
|
596
|
+
.union([z.string(), z.array(z.string())])
|
|
597
|
+
.optional()
|
|
598
|
+
.describe(`HIGH IMPACT - Reduces search space by 95%+
|
|
386
599
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}, {
|
|
439
|
-
title: TOOL_NAME$8,
|
|
440
|
-
description: DESCRIPTION$8,
|
|
441
|
-
readOnlyHint: true,
|
|
442
|
-
destructiveHint: false,
|
|
443
|
-
idempotentHint: true,
|
|
444
|
-
openWorldHint: true,
|
|
600
|
+
EXAMPLES: "microsoft", "google", "facebook" or ["microsoft", "google"]
|
|
601
|
+
POPULAR: microsoft, google, facebook, amazon, apache, hashicorp, kubernetes`),
|
|
602
|
+
filename: z
|
|
603
|
+
.string()
|
|
604
|
+
.optional()
|
|
605
|
+
.describe(`SURGICAL PRECISION for configs and special files
|
|
606
|
+
|
|
607
|
+
TARGETS: "package.json", "Dockerfile", "webpack.config.js", ".eslintrc", "README.md"
|
|
608
|
+
STRATEGY: filename:package.json + "react typescript"`),
|
|
609
|
+
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
|
+
.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
|
+
.min(1)
|
|
639
|
+
.max(100)
|
|
640
|
+
.optional()
|
|
641
|
+
.default(30)
|
|
642
|
+
.describe(`RESULTS: 10-20 (quick), 30 (default), 50-100 (comprehensive). Use filters over high limits.`),
|
|
643
|
+
},
|
|
644
|
+
annotations: {
|
|
645
|
+
title: 'GitHub Code Search - Smart & Efficient',
|
|
646
|
+
readOnlyHint: true,
|
|
647
|
+
destructiveHint: false,
|
|
648
|
+
idempotentHint: true,
|
|
649
|
+
openWorldHint: true,
|
|
650
|
+
},
|
|
445
651
|
}, async (args) => {
|
|
446
652
|
try {
|
|
447
|
-
|
|
448
|
-
|
|
653
|
+
// Validate parameter combinations
|
|
654
|
+
const validationError = validateSearchParameters(args);
|
|
655
|
+
if (validationError) {
|
|
656
|
+
return createResult({ error: validationError });
|
|
449
657
|
}
|
|
450
658
|
const result = await searchGitHubCode(args);
|
|
451
659
|
if (result.isError) {
|
|
@@ -455,131 +663,436 @@ function registerGitHubSearchCodeTool(server) {
|
|
|
455
663
|
const codeResults = JSON.parse(execResult.result);
|
|
456
664
|
// GitHub CLI returns a direct array, not an object with total_count and items
|
|
457
665
|
const items = Array.isArray(codeResults) ? codeResults : [];
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
has_complex_boolean_logic: hasComplexBooleanLogic(args.query),
|
|
466
|
-
escaped_args: buildGitHubCliArgs(args),
|
|
467
|
-
original_query: args.query,
|
|
468
|
-
},
|
|
469
|
-
});
|
|
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 });
|
|
470
673
|
}
|
|
471
674
|
catch (error) {
|
|
472
|
-
|
|
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
|
+
}
|
|
682
|
+
return createResult({
|
|
683
|
+
error: 'GitHub code search failed',
|
|
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
|
+
],
|
|
689
|
+
});
|
|
473
690
|
}
|
|
474
691
|
});
|
|
475
692
|
}
|
|
476
693
|
/**
|
|
477
|
-
*
|
|
694
|
+
* Smart handler for no results - provides actionable suggestions
|
|
478
695
|
*/
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
696
|
+
function handleNoResults(params, cliCommand) {
|
|
697
|
+
const suggestions = generateSmartSuggestions(params);
|
|
698
|
+
const fallbackQueries = generateFallbackQueries(params);
|
|
699
|
+
return createResult({
|
|
700
|
+
data: {
|
|
701
|
+
items: [],
|
|
702
|
+
total_count: 0,
|
|
703
|
+
smart_suggestions: {
|
|
704
|
+
message: "No results found. Here are smart strategies to find what you're looking for:",
|
|
705
|
+
suggestions,
|
|
706
|
+
fallback_queries: fallbackQueries,
|
|
707
|
+
next_steps: [
|
|
708
|
+
'Try one of the fallback queries above',
|
|
709
|
+
'Remove some filters to broaden the search',
|
|
710
|
+
'Use different keywords or synonyms',
|
|
711
|
+
'Check spelling and try variations',
|
|
712
|
+
],
|
|
713
|
+
},
|
|
714
|
+
metadata: {
|
|
715
|
+
has_filters: !!(params.language ||
|
|
716
|
+
params.owner ||
|
|
717
|
+
params.filename ||
|
|
718
|
+
params.extension),
|
|
719
|
+
search_scope: params.match
|
|
720
|
+
? Array.isArray(params.match)
|
|
721
|
+
? params.match.join(',')
|
|
722
|
+
: params.match
|
|
723
|
+
: 'file',
|
|
724
|
+
},
|
|
725
|
+
cli_command: cliCommand,
|
|
726
|
+
},
|
|
491
727
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Generate smart suggestions based on current search parameters
|
|
731
|
+
*/
|
|
732
|
+
function generateSmartSuggestions(params) {
|
|
733
|
+
const suggestions = [];
|
|
734
|
+
// Query-specific suggestions
|
|
735
|
+
if (params.query.includes('"')) {
|
|
736
|
+
suggestions.push('Remove quotes to search for individual terms instead of exact phrase');
|
|
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');
|
|
504
743
|
}
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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');
|
|
509
753
|
}
|
|
510
|
-
if (
|
|
511
|
-
|
|
754
|
+
if (params.filename) {
|
|
755
|
+
suggestions.push('Remove filename filter to search all file types');
|
|
512
756
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
+
});
|
|
518
801
|
}
|
|
519
|
-
|
|
520
|
-
|
|
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',
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return fallbacks.slice(0, 4);
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Extract key terms from search query
|
|
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;
|
|
521
896
|
}
|
|
522
|
-
|
|
523
|
-
|
|
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)');
|
|
524
991
|
}
|
|
525
|
-
|
|
526
|
-
|
|
992
|
+
else {
|
|
993
|
+
factors.push('OR operator (neutral)');
|
|
994
|
+
recommendations.push('Add language or owner filter with OR for better targeting');
|
|
527
995
|
}
|
|
528
996
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
997
|
+
if (params.query.includes(' AND ')) {
|
|
998
|
+
score += 1;
|
|
999
|
+
factors.push('AND operator (+1)');
|
|
532
1000
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
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
|
+
};
|
|
539
1016
|
}
|
|
540
1017
|
/**
|
|
541
|
-
*
|
|
1018
|
+
* Generate performance tips for inefficient searches
|
|
542
1019
|
*/
|
|
543
|
-
function
|
|
544
|
-
const
|
|
545
|
-
|
|
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;
|
|
546
1048
|
}
|
|
547
1049
|
/**
|
|
548
|
-
* Build command line arguments for GitHub CLI
|
|
1050
|
+
* Build command line arguments for GitHub CLI with improved parameter handling
|
|
1051
|
+
* Ensures exact string search capability with proper quote and escape handling
|
|
549
1052
|
*/
|
|
550
1053
|
function buildGitHubCliArgs(params) {
|
|
551
1054
|
const args = ['code'];
|
|
552
|
-
//
|
|
553
|
-
const searchQuery =
|
|
554
|
-
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
}
|
|
559
1070
|
else {
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
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}`);
|
|
573
1083
|
}
|
|
1084
|
+
if (params.size) {
|
|
1085
|
+
args.push(`--size=${params.size}`);
|
|
1086
|
+
}
|
|
1087
|
+
// Always add limit
|
|
574
1088
|
if (params.limit) {
|
|
575
1089
|
args.push(`--limit=${params.limit}`);
|
|
576
1090
|
}
|
|
577
|
-
// Handle match parameter
|
|
1091
|
+
// Handle match parameter
|
|
578
1092
|
if (params.match) {
|
|
579
1093
|
const matchValues = Array.isArray(params.match)
|
|
580
1094
|
? params.match
|
|
581
1095
|
: [params.match];
|
|
582
|
-
// GitHub API limitation: can't use both in:file and in:path in same query
|
|
583
1096
|
// Use the first match type when multiple are provided
|
|
584
1097
|
const matchValue = matchValues[0];
|
|
585
1098
|
args.push(`--match=${matchValue}`);
|
|
@@ -591,22 +1104,32 @@ function buildGitHubCliArgs(params) {
|
|
|
591
1104
|
: [params.owner];
|
|
592
1105
|
ownerValues.forEach(owner => args.push(`--owner=${owner}`));
|
|
593
1106
|
}
|
|
594
|
-
// Handle repository filters
|
|
595
|
-
if (params.
|
|
596
|
-
const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
|
|
1107
|
+
// Handle repository filters with improved validation
|
|
1108
|
+
if (params.repo) {
|
|
597
1109
|
const repos = Array.isArray(params.repo) ? params.repo : [params.repo];
|
|
598
|
-
|
|
599
|
-
|
|
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)
|
|
600
1129
|
repos.forEach(repo => {
|
|
601
|
-
|
|
602
|
-
if (repo.includes('/')) {
|
|
603
|
-
args.push(`--repo=${repo}`);
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
args.push(`--repo=${owner}/${repo}`);
|
|
607
|
-
}
|
|
1130
|
+
args.push(`--repo=${repo}`);
|
|
608
1131
|
});
|
|
609
|
-
}
|
|
1132
|
+
}
|
|
610
1133
|
}
|
|
611
1134
|
// JSON output with all available fields
|
|
612
1135
|
args.push('--json=repository,path,textMatches,sha,url');
|
|
@@ -623,75 +1146,149 @@ async function searchGitHubCode(params) {
|
|
|
623
1146
|
return result;
|
|
624
1147
|
}
|
|
625
1148
|
catch (error) {
|
|
626
|
-
|
|
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
|
+
});
|
|
1201
|
+
}
|
|
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
|
+
});
|
|
627
1212
|
}
|
|
628
1213
|
});
|
|
629
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
|
+
}
|
|
1224
|
+
if (params.query.length > 1000) {
|
|
1225
|
+
return 'Query too long (max 1000 chars). Simplify to key terms like "error handling" instead of full sentences.';
|
|
1226
|
+
}
|
|
1227
|
+
// Repository validation - allow owner/repo format in repo field
|
|
1228
|
+
if (params.repo && !params.owner) {
|
|
1229
|
+
const repoValues = Array.isArray(params.repo) ? params.repo : [params.repo];
|
|
1230
|
+
const hasOwnerFormat = repoValues.every(repo => repo.includes('/'));
|
|
1231
|
+
if (!hasOwnerFormat) {
|
|
1232
|
+
return 'Repository format error. Use "owner/repo" format like "facebook/react" or provide both owner and repo parameters.';
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// Support exact string searches with quotes and special characters
|
|
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
|
|
1245
|
+
}
|
|
630
1246
|
|
|
631
1247
|
const TOOL_NAME$7 = 'github_get_file_content';
|
|
632
|
-
const DESCRIPTION$7 = `Read file content
|
|
1248
|
+
const DESCRIPTION$7 = `Read file content with exact path verification. Smart branch fallbacks and size limits. Use github_get_contents first to verify file existence.`;
|
|
633
1249
|
function registerFetchGitHubFileContentTool(server) {
|
|
634
|
-
server.
|
|
635
|
-
owner: z
|
|
636
|
-
.string()
|
|
637
|
-
.min(1)
|
|
638
|
-
.max(100)
|
|
639
|
-
.regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/)
|
|
640
|
-
.describe(`Repository owner/organization (e.g., 'microsoft', 'facebook')`),
|
|
641
|
-
repo: z
|
|
642
|
-
.string()
|
|
643
|
-
.min(1)
|
|
644
|
-
.max(100)
|
|
645
|
-
.regex(/^[a-zA-Z0-9._-]+$/)
|
|
646
|
-
.describe(`Repository name (e.g., 'vscode', 'react'). Case-sensitive.`),
|
|
647
|
-
branch: z
|
|
648
|
-
.string()
|
|
649
|
-
.min(1)
|
|
650
|
-
.max(255)
|
|
651
|
-
.regex(/^[^\s]+$/)
|
|
652
|
-
.describe(`Branch name (e.g., 'main', 'master'). Auto-fallback to common branches if not found.`),
|
|
653
|
-
filePath: z
|
|
654
|
-
.string()
|
|
655
|
-
.min(1)
|
|
656
|
-
.describe(`File path from repository root (e.g., 'README.md', 'src/index.js'). Use github_get_contents to explore structure.`),
|
|
657
|
-
}, {
|
|
658
|
-
title: TOOL_NAME$7,
|
|
1250
|
+
server.registerTool(TOOL_NAME$7, {
|
|
659
1251
|
description: DESCRIPTION$7,
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1252
|
+
inputSchema: {
|
|
1253
|
+
owner: z
|
|
1254
|
+
.string()
|
|
1255
|
+
.min(1)
|
|
1256
|
+
.max(100)
|
|
1257
|
+
.regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/)
|
|
1258
|
+
.describe(`Repository owner/organization`),
|
|
1259
|
+
repo: z
|
|
1260
|
+
.string()
|
|
1261
|
+
.min(1)
|
|
1262
|
+
.max(100)
|
|
1263
|
+
.regex(/^[a-zA-Z0-9._-]+$/)
|
|
1264
|
+
.describe(`Repository name. Case-sensitive.`),
|
|
1265
|
+
branch: z
|
|
1266
|
+
.string()
|
|
1267
|
+
.min(1)
|
|
1268
|
+
.max(255)
|
|
1269
|
+
.regex(/^[^\s]+$/)
|
|
1270
|
+
.describe(`Branch name. Auto-fallback to common branches if not found.`),
|
|
1271
|
+
filePath: z
|
|
1272
|
+
.string()
|
|
1273
|
+
.min(1)
|
|
1274
|
+
.describe(`File path from repository root. Use github_get_contents to explore structure.`),
|
|
1275
|
+
},
|
|
1276
|
+
annotations: {
|
|
1277
|
+
title: 'GitHub File Content Reader',
|
|
1278
|
+
readOnlyHint: true,
|
|
1279
|
+
destructiveHint: false,
|
|
1280
|
+
idempotentHint: true,
|
|
1281
|
+
openWorldHint: true,
|
|
1282
|
+
},
|
|
664
1283
|
}, async (args) => {
|
|
665
1284
|
try {
|
|
666
1285
|
const result = await fetchGitHubFileContent(args);
|
|
667
|
-
if (result.content && result.content[0] && !result.isError) {
|
|
668
|
-
const { data, parsed } = parseJsonResponse(result.content[0].text);
|
|
669
|
-
if (parsed) {
|
|
670
|
-
return createResult({
|
|
671
|
-
file: `${args.owner}/${args.repo}/${args.filePath}`,
|
|
672
|
-
content: data.content || data,
|
|
673
|
-
metadata: {
|
|
674
|
-
branch: args.branch,
|
|
675
|
-
size: data.size,
|
|
676
|
-
encoding: data.encoding,
|
|
677
|
-
},
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
// Return raw file content
|
|
682
|
-
return createResult({
|
|
683
|
-
file: `${args.owner}/${args.repo}/${args.filePath}`,
|
|
684
|
-
content: data,
|
|
685
|
-
metadata: {
|
|
686
|
-
branch: args.branch,
|
|
687
|
-
},
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
1286
|
return result;
|
|
692
1287
|
}
|
|
693
1288
|
catch (error) {
|
|
694
|
-
return createResult(
|
|
1289
|
+
return createResult({
|
|
1290
|
+
error: 'File fetch failed - verify path or try github_get_contents first',
|
|
1291
|
+
});
|
|
695
1292
|
}
|
|
696
1293
|
});
|
|
697
1294
|
}
|
|
@@ -724,19 +1321,45 @@ async function fetchGitHubFileContent(params) {
|
|
|
724
1321
|
}
|
|
725
1322
|
// Handle common errors
|
|
726
1323
|
if (errorMsg.includes('404')) {
|
|
727
|
-
return
|
|
1324
|
+
return createResult({
|
|
1325
|
+
error: 'File not found - verify path with github_get_contents',
|
|
1326
|
+
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1327
|
+
});
|
|
728
1328
|
}
|
|
729
1329
|
else if (errorMsg.includes('403')) {
|
|
730
|
-
return
|
|
1330
|
+
return createResult({
|
|
1331
|
+
error: 'Access denied - repository may be private',
|
|
1332
|
+
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
else if (errorMsg.includes('maxBuffer') ||
|
|
1336
|
+
errorMsg.includes('stdout maxBuffer length exceeded')) {
|
|
1337
|
+
return createResult({
|
|
1338
|
+
error: 'File too large (>300KB) - use github_search_code for patterns instead',
|
|
1339
|
+
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1340
|
+
});
|
|
731
1341
|
}
|
|
732
1342
|
else {
|
|
733
|
-
return
|
|
1343
|
+
return createResult({
|
|
1344
|
+
error: 'Fetch failed - check repository and file path',
|
|
1345
|
+
cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
|
|
1346
|
+
});
|
|
734
1347
|
}
|
|
735
1348
|
}
|
|
736
1349
|
return await processFileContent(result, owner, repo, branch, filePath);
|
|
737
1350
|
}
|
|
738
1351
|
catch (error) {
|
|
739
|
-
|
|
1352
|
+
const errorMessage = error.message;
|
|
1353
|
+
// Handle maxBuffer errors that escape the main try-catch
|
|
1354
|
+
if (errorMessage.includes('maxBuffer') ||
|
|
1355
|
+
errorMessage.includes('stdout maxBuffer length exceeded')) {
|
|
1356
|
+
return createResult({
|
|
1357
|
+
error: 'File too large (>300KB) - use github_search_code for patterns instead',
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
return createResult({
|
|
1361
|
+
error: 'Unexpected error during file fetch - check connection',
|
|
1362
|
+
});
|
|
740
1363
|
}
|
|
741
1364
|
});
|
|
742
1365
|
}
|
|
@@ -746,30 +1369,42 @@ async function processFileContent(result, owner, repo, branch, filePath) {
|
|
|
746
1369
|
const fileData = JSON.parse(execResult.result);
|
|
747
1370
|
// Check if it's a directory
|
|
748
1371
|
if (Array.isArray(fileData)) {
|
|
749
|
-
return
|
|
1372
|
+
return createResult({
|
|
1373
|
+
error: 'Path is directory - use github_get_contents instead',
|
|
1374
|
+
});
|
|
750
1375
|
}
|
|
751
1376
|
const fileSize = fileData.size || 0;
|
|
752
|
-
const MAX_FILE_SIZE =
|
|
753
|
-
// Check file size
|
|
1377
|
+
const MAX_FILE_SIZE = 300 * 1024; // 300KB limit for better performance and reliability
|
|
1378
|
+
// Check file size with helpful message
|
|
754
1379
|
if (fileSize > MAX_FILE_SIZE) {
|
|
755
|
-
|
|
1380
|
+
const fileSizeKB = Math.round(fileSize / 1024);
|
|
1381
|
+
const maxSizeKB = Math.round(MAX_FILE_SIZE / 1024);
|
|
1382
|
+
return createResult({
|
|
1383
|
+
error: `File too large (${fileSizeKB}KB > ${maxSizeKB}KB) - use github_search_code for patterns`,
|
|
1384
|
+
});
|
|
756
1385
|
}
|
|
757
1386
|
// Get and decode content
|
|
758
1387
|
const base64Content = fileData.content?.replace(/\s/g, ''); // Remove all whitespace
|
|
759
1388
|
if (!base64Content) {
|
|
760
|
-
return
|
|
1389
|
+
return createResult({
|
|
1390
|
+
error: 'Empty file - no content to display',
|
|
1391
|
+
});
|
|
761
1392
|
}
|
|
762
1393
|
let decodedContent;
|
|
763
1394
|
try {
|
|
764
1395
|
const buffer = Buffer.from(base64Content, 'base64');
|
|
765
1396
|
// Simple binary check - look for null bytes
|
|
766
1397
|
if (buffer.indexOf(0) !== -1) {
|
|
767
|
-
return
|
|
1398
|
+
return createResult({
|
|
1399
|
+
error: 'Binary file detected - cannot display as text',
|
|
1400
|
+
});
|
|
768
1401
|
}
|
|
769
1402
|
decodedContent = buffer.toString('utf-8');
|
|
770
1403
|
}
|
|
771
1404
|
catch (decodeError) {
|
|
772
|
-
return
|
|
1405
|
+
return createResult({
|
|
1406
|
+
error: 'Decode failed - file encoding not supported',
|
|
1407
|
+
});
|
|
773
1408
|
}
|
|
774
1409
|
// Return simplified response
|
|
775
1410
|
const response = {
|
|
@@ -784,132 +1419,275 @@ async function processFileContent(result, owner, repo, branch, filePath) {
|
|
|
784
1419
|
encoding: 'utf-8',
|
|
785
1420
|
},
|
|
786
1421
|
};
|
|
787
|
-
return
|
|
1422
|
+
return createResult({ data: response });
|
|
788
1423
|
}
|
|
789
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* GitHub Repository Search Tool
|
|
1427
|
+
*
|
|
1428
|
+
* MOST EFFECTIVE PATTERNS (based on testing):
|
|
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
|
+
*/
|
|
790
1445
|
const TOOL_NAME$6 = 'github_search_repositories';
|
|
791
|
-
const DESCRIPTION$6 = `Search repositories
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
}
|
|
799
1500
|
function registerSearchGitHubReposTool(server) {
|
|
800
|
-
server.
|
|
801
|
-
query: z
|
|
802
|
-
.string()
|
|
803
|
-
.optional()
|
|
804
|
-
.describe('Search query with GitHub syntax: "cli shell" (AND), "vim plugin" (phrase), "language:Go OR language:Rust" (OR). Optional - can search with just primary filters.'),
|
|
805
|
-
// PRIMARY FILTERS (can work alone)
|
|
806
|
-
owner: z
|
|
807
|
-
.string()
|
|
808
|
-
.optional()
|
|
809
|
-
.describe('Repository owner/organization (e.g., "microsoft", "facebook").'),
|
|
810
|
-
language: z
|
|
811
|
-
.string()
|
|
812
|
-
.optional()
|
|
813
|
-
.describe('Programming language (e.g., "javascript", "python", "go").'),
|
|
814
|
-
stars: z
|
|
815
|
-
.string()
|
|
816
|
-
.optional()
|
|
817
|
-
.describe('Stars count with ranges: "100", ">500", "<50", "10..100", ">=1000". Use >100 for quality projects.'),
|
|
818
|
-
topic: z
|
|
819
|
-
.array(z.string())
|
|
820
|
-
.optional()
|
|
821
|
-
.describe('Filter by topics (e.g., ["cli", "typescript", "api"]).'),
|
|
822
|
-
forks: z.number().optional().describe('Exact forks count.'),
|
|
823
|
-
numberOfTopics: z
|
|
824
|
-
.number()
|
|
825
|
-
.optional()
|
|
826
|
-
.describe('Filter on number of topics.'),
|
|
827
|
-
// SECONDARY FILTERS (require query or primary filter)
|
|
828
|
-
license: z
|
|
829
|
-
.array(z.string())
|
|
830
|
-
.optional()
|
|
831
|
-
.describe('License types (e.g., ["mit", "apache-2.0"]).'),
|
|
832
|
-
match: z
|
|
833
|
-
.enum(['name', 'description', 'readme'])
|
|
834
|
-
.optional()
|
|
835
|
-
.describe('Search scope: "name", "description", or "readme".'),
|
|
836
|
-
visibility: z
|
|
837
|
-
.enum(['public', 'private', 'internal'])
|
|
838
|
-
.optional()
|
|
839
|
-
.describe('Repository visibility filter.'),
|
|
840
|
-
created: z
|
|
841
|
-
.string()
|
|
842
|
-
.optional()
|
|
843
|
-
.describe('Created date filter: ">2020-01-01", "<2023-12-31", "2022-01-01..2023-12-31".'),
|
|
844
|
-
updated: z
|
|
845
|
-
.string()
|
|
846
|
-
.optional()
|
|
847
|
-
.describe('Updated date filter (same format as created).'),
|
|
848
|
-
archived: z.boolean().optional().describe('Filter by archived state.'),
|
|
849
|
-
includeForks: z
|
|
850
|
-
.enum(['false', 'true', 'only'])
|
|
851
|
-
.optional()
|
|
852
|
-
.describe('Include forks: "false" (default), "true", or "only".'),
|
|
853
|
-
goodFirstIssues: z
|
|
854
|
-
.string()
|
|
855
|
-
.optional()
|
|
856
|
-
.describe('Filter by good first issues count (e.g., ">=10", ">5").'),
|
|
857
|
-
helpWantedIssues: z
|
|
858
|
-
.string()
|
|
859
|
-
.optional()
|
|
860
|
-
.describe('Filter by help wanted issues count (e.g., ">=5", ">10").'),
|
|
861
|
-
followers: z
|
|
862
|
-
.number()
|
|
863
|
-
.optional()
|
|
864
|
-
.describe('Filter by number of followers.'),
|
|
865
|
-
size: z
|
|
866
|
-
.string()
|
|
867
|
-
.optional()
|
|
868
|
-
.describe('Repository size filter in KB (e.g., ">100", "<50", "10..100").'),
|
|
869
|
-
// Sorting and limits
|
|
870
|
-
sort: z
|
|
871
|
-
.enum(['forks', 'help-wanted-issues', 'stars', 'updated', 'best-match'])
|
|
872
|
-
.optional()
|
|
873
|
-
.default('best-match')
|
|
874
|
-
.describe('Sort criteria (default: best-match)'),
|
|
875
|
-
order: z
|
|
876
|
-
.enum(['asc', 'desc'])
|
|
877
|
-
.optional()
|
|
878
|
-
.default('desc')
|
|
879
|
-
.describe('Result order (default: desc)'),
|
|
880
|
-
limit: z
|
|
881
|
-
.number()
|
|
882
|
-
.int()
|
|
883
|
-
.min(1)
|
|
884
|
-
.max(50)
|
|
885
|
-
.optional()
|
|
886
|
-
.default(25)
|
|
887
|
-
.describe('Maximum results (default: 25, max: 50)'),
|
|
888
|
-
}, {
|
|
889
|
-
title: TOOL_NAME$6,
|
|
1501
|
+
server.registerTool(TOOL_NAME$6, {
|
|
890
1502
|
description: DESCRIPTION$6,
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1503
|
+
inputSchema: {
|
|
1504
|
+
query: z
|
|
1505
|
+
.string()
|
|
1506
|
+
.optional()
|
|
1507
|
+
.describe('Search query with GitHub search syntax. POWERFUL EXAMPLES: "vue OR react stars:>1000", "typescript AND framework stars:100..5000", "repo:facebook/react OR repo:vuejs/vue", "org:microsoft language:typescript", "topic:react topic:typescript stars:>500". SUPPORTS: OR/AND logic, embedded qualifiers (stars:, language:, org:, repo:, topic:, etc.), exact repository targeting. COMBINES with traditional filters for maximum flexibility.'),
|
|
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.'),
|
|
1513
|
+
language: z
|
|
1514
|
+
.string()
|
|
1515
|
+
.optional()
|
|
1516
|
+
.describe('Programming language filter. CAUTION: Restrictive with other filters. Use alone or with stars/owner only.'),
|
|
1517
|
+
stars: z
|
|
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
|
|
1538
|
+
.union([z.string(), z.array(z.string())])
|
|
1539
|
+
.optional()
|
|
1540
|
+
.describe('License filter. Works well as array ["mit", "apache-2.0"].'),
|
|
1541
|
+
archived: z
|
|
1542
|
+
.boolean()
|
|
1543
|
+
.optional()
|
|
1544
|
+
.describe('Filter archived repositories (true/false).'),
|
|
1545
|
+
includeForks: z
|
|
1546
|
+
.enum(['false', 'true', 'only'])
|
|
1547
|
+
.optional()
|
|
1548
|
+
.describe('Include forks: false (exclude), true (include), only (forks only).'),
|
|
1549
|
+
visibility: z
|
|
1550
|
+
.enum(['public', 'private', 'internal'])
|
|
1551
|
+
.optional()
|
|
1552
|
+
.describe('Repository visibility filter.'),
|
|
1553
|
+
// DATE & SIZE FILTERS
|
|
1554
|
+
created: z
|
|
1555
|
+
.string()
|
|
1556
|
+
.optional()
|
|
1557
|
+
.describe('Created date filter. Format: ">2020-01-01", "<2023-12-31".'),
|
|
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.'),
|
|
1608
|
+
limit: z
|
|
1609
|
+
.number()
|
|
1610
|
+
.int()
|
|
1611
|
+
.min(1)
|
|
1612
|
+
.max(100)
|
|
1613
|
+
.optional()
|
|
1614
|
+
.default(30)
|
|
1615
|
+
.describe('Results limit (1-100). PREFER increasing limit over adding filters.'),
|
|
1616
|
+
},
|
|
1617
|
+
annotations: {
|
|
1618
|
+
title: 'GitHub Repository Search',
|
|
1619
|
+
readOnlyHint: true,
|
|
1620
|
+
destructiveHint: false,
|
|
1621
|
+
idempotentHint: true,
|
|
1622
|
+
openWorldHint: true,
|
|
1623
|
+
},
|
|
895
1624
|
}, async (args) => {
|
|
896
1625
|
try {
|
|
897
|
-
//
|
|
898
|
-
const
|
|
899
|
-
args.
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1626
|
+
// Extract owner/repo from query if present
|
|
1627
|
+
const queryInfo = args.query
|
|
1628
|
+
? extractOwnerRepoFromQuery(args.query)
|
|
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;
|
|
1638
|
+
}
|
|
1639
|
+
// Update parameters with extracted information
|
|
1640
|
+
const enhancedArgs = {
|
|
1641
|
+
...args,
|
|
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;
|
|
904
1652
|
if (!hasPrimaryFilter) {
|
|
905
|
-
return createResult(
|
|
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
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
// First attempt: Search with current parameters
|
|
1658
|
+
const result = await searchGitHubRepos(enhancedArgs);
|
|
1659
|
+
// Fallback for private repositories: If no results and owner is specified, try with private visibility
|
|
1660
|
+
if (!result.isError) {
|
|
1661
|
+
const resultData = JSON.parse(result.content[0].text);
|
|
1662
|
+
if (resultData.total === 0 &&
|
|
1663
|
+
enhancedArgs.owner &&
|
|
1664
|
+
!enhancedArgs.visibility) {
|
|
1665
|
+
// Try searching with private visibility for organization repos
|
|
1666
|
+
const privateSearchArgs = {
|
|
1667
|
+
...enhancedArgs,
|
|
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
|
+
}
|
|
1683
|
+
}
|
|
906
1684
|
}
|
|
907
|
-
// Search repositories using GitHub CLI
|
|
908
|
-
const result = await searchGitHubRepos(args);
|
|
909
1685
|
return result;
|
|
910
1686
|
}
|
|
911
1687
|
catch (error) {
|
|
912
|
-
return createResult(
|
|
1688
|
+
return createResult({
|
|
1689
|
+
error: 'Repository search failed - verify connection or simplify query',
|
|
1690
|
+
});
|
|
913
1691
|
}
|
|
914
1692
|
});
|
|
915
1693
|
}
|
|
@@ -980,129 +1758,124 @@ async function searchGitHubRepos(params) {
|
|
|
980
1758
|
license: repo.license?.name || null,
|
|
981
1759
|
hasIssues: repo.hasIssues || false,
|
|
982
1760
|
openIssuesCount: repo.openIssuesCount || 0,
|
|
983
|
-
createdAt: repo.createdAt,
|
|
984
|
-
updatedAt: repo.updatedAt,
|
|
1761
|
+
createdAt: toDDMMYYYY(repo.createdAt),
|
|
1762
|
+
updatedAt: toDDMMYYYY(repo.updatedAt),
|
|
985
1763
|
visibility: repo.visibility || 'public',
|
|
986
1764
|
owner: repo.owner?.login || repo.owner,
|
|
987
1765
|
}));
|
|
988
1766
|
}
|
|
989
|
-
return
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
+
},
|
|
1004
1784
|
});
|
|
1005
1785
|
}
|
|
1006
1786
|
catch (error) {
|
|
1007
|
-
return
|
|
1787
|
+
return createResult({
|
|
1788
|
+
error: 'Repository search failed - verify connection or simplify query',
|
|
1789
|
+
});
|
|
1008
1790
|
}
|
|
1009
1791
|
});
|
|
1010
1792
|
}
|
|
1011
1793
|
function buildGitHubReposSearchCommand(params) {
|
|
1012
|
-
// Build query following GitHub CLI patterns
|
|
1013
|
-
const query = params.query?.trim() || '';
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (query) {
|
|
1025
|
-
if (hasComplexSyntax) {
|
|
1026
|
-
// For complex queries with special syntax, we need to be more careful
|
|
1027
|
-
// Split by spaces but preserve quoted strings and handle special characters
|
|
1028
|
-
const queryParts = query.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
1029
|
-
queryParts.forEach(part => {
|
|
1030
|
-
// If part contains shell special characters, quote it
|
|
1031
|
-
if (/[><=&|$`(){}[\];\\]/.test(part) && !part.includes('"')) {
|
|
1032
|
-
args.push(`"${part}"`);
|
|
1033
|
-
}
|
|
1034
|
-
else {
|
|
1035
|
-
args.push(part);
|
|
1036
|
-
}
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
else {
|
|
1040
|
-
// For simple queries, split by spaces to match GitHub CLI examples
|
|
1041
|
-
// "cli shell" becomes separate args: cli shell
|
|
1042
|
-
const queryParts = query.split(/\s+/).filter(part => part.length > 0);
|
|
1043
|
-
queryParts.forEach(part => {
|
|
1044
|
-
// Only quote if the part contains special characters
|
|
1045
|
-
if (needsQuoting(part)) {
|
|
1046
|
-
args.push(`"${part}"`);
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
args.push(part);
|
|
1050
|
-
}
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1794
|
+
// Build query following GitHub CLI patterns
|
|
1795
|
+
const query = params.query?.trim() || '';
|
|
1796
|
+
const args = ['repos'];
|
|
1797
|
+
// Detect embedded qualifiers in query to avoid conflicts and optimize
|
|
1798
|
+
const hasEmbeddedQualifiers = query &&
|
|
1799
|
+
/\b(stars|language|org|repo|topic|user|created|updated|size|license|archived|fork|good-first-issues|help-wanted-issues):/i.test(query);
|
|
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);
|
|
1053
1806
|
}
|
|
1054
1807
|
// Add JSON output with specific fields for structured data parsing
|
|
1055
1808
|
// Note: 'topics' field is not available in GitHub CLI search repos JSON output
|
|
1056
|
-
args.push('--json
|
|
1057
|
-
//
|
|
1058
|
-
if
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1809
|
+
args.push('--json=name,fullName,description,language,stargazersCount,forksCount,updatedAt,createdAt,url,owner,isPrivate,license,hasIssues,openIssuesCount,isArchived,isFork,visibility');
|
|
1810
|
+
// SMART FILTER HANDLING - Avoid conflicts with embedded qualifiers
|
|
1811
|
+
// Only add CLI flags if not already specified in query to prevent conflicts
|
|
1812
|
+
// CORE FILTERS - Handle arrays properly
|
|
1813
|
+
if (params.owner && !hasEmbeddedQualifiers) {
|
|
1814
|
+
// GitHub CLI supports multiple owners with comma separation: --owner=owner1,owner2
|
|
1815
|
+
const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
|
|
1816
|
+
args.push(`--owner=${owners.join(',')}`);
|
|
1063
1817
|
}
|
|
1064
|
-
if (params.language)
|
|
1818
|
+
if (params.language && !hasEmbeddedQualifiers)
|
|
1065
1819
|
args.push(`--language=${params.language}`);
|
|
1066
|
-
if (params.forks !== undefined)
|
|
1820
|
+
if (params.forks !== undefined && !hasEmbeddedQualifiers)
|
|
1067
1821
|
args.push(`--forks=${params.forks}`);
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
+
}
|
|
1070
1827
|
if (params.numberOfTopics !== undefined)
|
|
1071
1828
|
args.push(`--number-topics=${params.numberOfTopics}`);
|
|
1072
|
-
//
|
|
1073
|
-
if (params.stars !== undefined &&
|
|
1074
|
-
params.stars
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
args.push(`--stars=${
|
|
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}`);
|
|
1081
1838
|
}
|
|
1082
1839
|
}
|
|
1083
|
-
//
|
|
1840
|
+
// QUALITY & STATE FILTERS
|
|
1084
1841
|
if (params.archived !== undefined)
|
|
1085
1842
|
args.push(`--archived=${params.archived}`);
|
|
1086
|
-
if (params.created)
|
|
1087
|
-
args.push(`--created="${params.created}"`);
|
|
1088
1843
|
if (params.includeForks)
|
|
1089
1844
|
args.push(`--include-forks=${params.includeForks}`);
|
|
1090
|
-
if (params.license && params.license.length > 0)
|
|
1091
|
-
args.push(`--license=${params.license.join(',')}`);
|
|
1092
|
-
if (params.match)
|
|
1093
|
-
args.push(`--match=${params.match}`);
|
|
1094
|
-
if (params.updated)
|
|
1095
|
-
args.push(`--updated="${params.updated}"`);
|
|
1096
1845
|
if (params.visibility)
|
|
1097
1846
|
args.push(`--visibility=${params.visibility}`);
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
args.push(`--
|
|
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(',')}`);
|
|
1853
|
+
}
|
|
1854
|
+
// DATE & SIZE FILTERS
|
|
1855
|
+
if (params.created)
|
|
1856
|
+
args.push(`--created=${params.created}`);
|
|
1857
|
+
if (params.updated)
|
|
1858
|
+
args.push(`--updated=${params.updated}`);
|
|
1104
1859
|
if (params.size)
|
|
1105
1860
|
args.push(`--size=${params.size}`);
|
|
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}`);
|
|
1106
1879
|
// SORTING AND LIMITS
|
|
1107
1880
|
if (params.limit)
|
|
1108
1881
|
args.push(`--limit=${params.limit}`);
|
|
@@ -1117,376 +1890,443 @@ function buildGitHubReposSearchCommand(params) {
|
|
|
1117
1890
|
}
|
|
1118
1891
|
|
|
1119
1892
|
const TOOL_NAME$5 = 'github_search_commits';
|
|
1120
|
-
const DESCRIPTION$5 = `Search commit history with
|
|
1121
|
-
function
|
|
1122
|
-
server.
|
|
1123
|
-
query: z
|
|
1124
|
-
.string()
|
|
1125
|
-
.optional()
|
|
1126
|
-
.describe('Search query with POWERFUL boolean logic and exact phrase matching. BOOLEAN OPERATORS: "fix AND bug" (both required), "fix OR update" (either term), "readme typo" (implicit AND). EXACT PHRASES: "initial commit" (precise phrase matching). ADVANCED SYNTAX: "author:john OR committer:jane" (user qualifiers), "-- -author:botuser" (exclusions). STRENGTH: Surgical precision for commit discovery across millions of repositories. Optional - can search with just filters.'),
|
|
1127
|
-
// Basic filters
|
|
1128
|
-
owner: z
|
|
1129
|
-
.string()
|
|
1130
|
-
.optional()
|
|
1131
|
-
.describe('Repository owner/organization. Leave empty for global search.'),
|
|
1132
|
-
repo: z
|
|
1133
|
-
.string()
|
|
1134
|
-
.optional()
|
|
1135
|
-
.describe('Repository name. Do exploratory search without repo filter first'),
|
|
1136
|
-
// Author filters
|
|
1137
|
-
author: z.string().optional().describe('Filter by commit author'),
|
|
1138
|
-
authorDate: z
|
|
1139
|
-
.string()
|
|
1140
|
-
.optional()
|
|
1141
|
-
.describe('Filter by authored date (format: >2020-01-01, <2023-12-31)'),
|
|
1142
|
-
authorEmail: z.string().optional().describe('Filter by author email'),
|
|
1143
|
-
authorName: z.string().optional().describe('Filter by author name'),
|
|
1144
|
-
// Committer filters
|
|
1145
|
-
committer: z.string().optional().describe('Filter by committer'),
|
|
1146
|
-
committerDate: z
|
|
1147
|
-
.string()
|
|
1148
|
-
.optional()
|
|
1149
|
-
.describe('Filter by committed date (format: >2020-01-01, <2023-12-31)'),
|
|
1150
|
-
committerEmail: z
|
|
1151
|
-
.string()
|
|
1152
|
-
.optional()
|
|
1153
|
-
.describe('Filter by committer email'),
|
|
1154
|
-
committerName: z.string().optional().describe('Filter by committer name'),
|
|
1155
|
-
// Hash filters
|
|
1156
|
-
hash: z.string().optional().describe('Filter by commit hash'),
|
|
1157
|
-
parent: z.string().optional().describe('Filter by parent hash'),
|
|
1158
|
-
tree: z.string().optional().describe('Filter by tree hash'),
|
|
1159
|
-
// Boolean filters
|
|
1160
|
-
merge: z.boolean().optional().describe('Filter merge commits'),
|
|
1161
|
-
visibility: z
|
|
1162
|
-
.enum(['public', 'private', 'internal'])
|
|
1163
|
-
.optional()
|
|
1164
|
-
.describe('Filter by repository visibility'),
|
|
1165
|
-
// Sorting and limits
|
|
1166
|
-
sort: z
|
|
1167
|
-
.enum(['author-date', 'committer-date', 'best-match'])
|
|
1168
|
-
.optional()
|
|
1169
|
-
.default('best-match')
|
|
1170
|
-
.describe('Sort criteria (default: best-match)'),
|
|
1171
|
-
order: z
|
|
1172
|
-
.enum(['asc', 'desc'])
|
|
1173
|
-
.optional()
|
|
1174
|
-
.default('desc')
|
|
1175
|
-
.describe('Order (default: desc)'),
|
|
1176
|
-
limit: z
|
|
1177
|
-
.number()
|
|
1178
|
-
.int()
|
|
1179
|
-
.min(1)
|
|
1180
|
-
.max(50)
|
|
1181
|
-
.optional()
|
|
1182
|
-
.default(25)
|
|
1183
|
-
.describe('Maximum results (default: 25, max: 50)'),
|
|
1184
|
-
}, {
|
|
1185
|
-
title: TOOL_NAME$5,
|
|
1893
|
+
const DESCRIPTION$5 = `Search commit history effectively with GitHub's commit search. Use simple, specific terms for best results. Complex boolean queries may return limited results - try individual keywords instead.`;
|
|
1894
|
+
function registerGitHubSearchCommitsTool(server) {
|
|
1895
|
+
server.registerTool(TOOL_NAME$5, {
|
|
1186
1896
|
description: DESCRIPTION$5,
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1897
|
+
inputSchema: {
|
|
1898
|
+
query: z
|
|
1899
|
+
.string()
|
|
1900
|
+
.optional()
|
|
1901
|
+
.describe('Search query with boolean logic. Boolean: "fix AND bug", exact phrases: "initial commit", advanced syntax: "author:john OR committer:jane".'),
|
|
1902
|
+
// Repository filters
|
|
1903
|
+
owner: z
|
|
1904
|
+
.string()
|
|
1905
|
+
.optional()
|
|
1906
|
+
.describe('Repository owner/organization. Leave empty for global search.'),
|
|
1907
|
+
repo: z
|
|
1908
|
+
.string()
|
|
1909
|
+
.optional()
|
|
1910
|
+
.describe('Repository name. Do exploratory search without repo filter first'),
|
|
1911
|
+
// Author filters
|
|
1912
|
+
author: z.string().optional().describe('Filter by commit author'),
|
|
1913
|
+
authorName: z.string().optional().describe('Filter by author name'),
|
|
1914
|
+
authorEmail: z.string().optional().describe('Filter by author email'),
|
|
1915
|
+
// Committer filters
|
|
1916
|
+
committer: z.string().optional().describe('Filter by committer'),
|
|
1917
|
+
committerName: z
|
|
1918
|
+
.string()
|
|
1919
|
+
.optional()
|
|
1920
|
+
.describe('Filter by committer name'),
|
|
1921
|
+
committerEmail: z
|
|
1922
|
+
.string()
|
|
1923
|
+
.optional()
|
|
1924
|
+
.describe('Filter by committer email'),
|
|
1925
|
+
// Date filters
|
|
1926
|
+
authorDate: z
|
|
1927
|
+
.string()
|
|
1928
|
+
.optional()
|
|
1929
|
+
.describe('Filter by authored date (format: >2020-01-01, <2023-12-31)'),
|
|
1930
|
+
committerDate: z
|
|
1931
|
+
.string()
|
|
1932
|
+
.optional()
|
|
1933
|
+
.describe('Filter by committed date (format: >2020-01-01, <2023-12-31)'),
|
|
1934
|
+
// Hash filters
|
|
1935
|
+
hash: z.string().optional().describe('Filter by commit hash'),
|
|
1936
|
+
parent: z.string().optional().describe('Filter by parent hash'),
|
|
1937
|
+
tree: z.string().optional().describe('Filter by tree hash'),
|
|
1938
|
+
// State filters
|
|
1939
|
+
merge: z.boolean().optional().describe('Filter merge commits'),
|
|
1940
|
+
// Visibility
|
|
1941
|
+
visibility: z
|
|
1942
|
+
.enum(['public', 'private', 'internal'])
|
|
1943
|
+
.optional()
|
|
1944
|
+
.describe('Filter by repository visibility'),
|
|
1945
|
+
// Pagination and sorting
|
|
1946
|
+
limit: z
|
|
1947
|
+
.number()
|
|
1948
|
+
.int()
|
|
1949
|
+
.min(1)
|
|
1950
|
+
.max(50)
|
|
1951
|
+
.optional()
|
|
1952
|
+
.default(25)
|
|
1953
|
+
.describe('Maximum results (default: 25, max: 50)'),
|
|
1954
|
+
sort: z
|
|
1955
|
+
.enum(['author-date', 'committer-date'])
|
|
1956
|
+
.optional()
|
|
1957
|
+
.describe('Sort criteria (default: relevance)'),
|
|
1958
|
+
order: z
|
|
1959
|
+
.enum(['asc', 'desc'])
|
|
1960
|
+
.optional()
|
|
1961
|
+
.default('desc')
|
|
1962
|
+
.describe('Order (default: desc)'),
|
|
1963
|
+
},
|
|
1964
|
+
annotations: {
|
|
1965
|
+
title: 'GitHub Commit Search',
|
|
1966
|
+
readOnlyHint: true,
|
|
1967
|
+
destructiveHint: false,
|
|
1968
|
+
idempotentHint: true,
|
|
1969
|
+
openWorldHint: true,
|
|
1970
|
+
},
|
|
1191
1971
|
}, async (args) => {
|
|
1192
1972
|
try {
|
|
1193
|
-
// Query is optional - can search with just filters
|
|
1194
|
-
if (!args.query?.trim() &&
|
|
1195
|
-
!args.owner &&
|
|
1196
|
-
!args.author &&
|
|
1197
|
-
!args.committer &&
|
|
1198
|
-
!args.repo) {
|
|
1199
|
-
return createResult('Either query or at least one filter is required', true);
|
|
1200
|
-
}
|
|
1201
1973
|
const result = await searchGitHubCommits(args);
|
|
1202
|
-
|
|
1974
|
+
if (result.isError) {
|
|
1975
|
+
return result;
|
|
1976
|
+
}
|
|
1977
|
+
const execResult = JSON.parse(result.content[0].text);
|
|
1978
|
+
const commits = JSON.parse(execResult.result);
|
|
1979
|
+
// GitHub CLI returns a direct array
|
|
1980
|
+
const items = Array.isArray(commits) ? commits : [];
|
|
1981
|
+
// Enhanced handling for no results - provide fallback suggestions
|
|
1982
|
+
if (items.length === 0) {
|
|
1983
|
+
return createResult({
|
|
1984
|
+
data: {
|
|
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
|
+
},
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
// Transform to optimized format
|
|
2003
|
+
const optimizedResult = transformCommitsToOptimizedFormat(items, args);
|
|
2004
|
+
return createResult({ data: optimizedResult });
|
|
1203
2005
|
}
|
|
1204
2006
|
catch (error) {
|
|
1205
|
-
|
|
2007
|
+
const errorMessage = error.message || '';
|
|
2008
|
+
if (errorMessage.includes('authentication')) {
|
|
2009
|
+
return createResult({
|
|
2010
|
+
error: 'GitHub authentication required - run api_status_check tool',
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
if (errorMessage.includes('rate limit')) {
|
|
2014
|
+
return createResult({
|
|
2015
|
+
error: 'GitHub rate limit exceeded - try more specific filters',
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
return createResult({
|
|
2019
|
+
error: 'Commit search failed',
|
|
2020
|
+
suggestions: [
|
|
2021
|
+
'Check authentication with api_status_check',
|
|
2022
|
+
'Use more specific date ranges or author filters',
|
|
2023
|
+
'Try simpler boolean queries',
|
|
2024
|
+
],
|
|
2025
|
+
});
|
|
1206
2026
|
}
|
|
1207
2027
|
});
|
|
1208
2028
|
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Transform GitHub CLI response to optimized format
|
|
2031
|
+
*/
|
|
2032
|
+
function transformCommitsToOptimizedFormat(items, _params) {
|
|
2033
|
+
// Extract repository info if single repo search
|
|
2034
|
+
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
|
+
const optimizedCommits = items
|
|
2038
|
+
.map(item => ({
|
|
2039
|
+
sha: item.sha,
|
|
2040
|
+
message: getCommitTitle(item.commit?.message || ''),
|
|
2041
|
+
author: item.commit?.author?.name || item.author?.login || 'Unknown',
|
|
2042
|
+
date: toDDMMYYYY(item.commit?.author?.date || ''),
|
|
2043
|
+
repository: singleRepo
|
|
2044
|
+
? undefined
|
|
2045
|
+
: simplifyRepoUrl(item.repository?.url || ''),
|
|
2046
|
+
url: singleRepo
|
|
2047
|
+
? item.sha
|
|
2048
|
+
: `${simplifyRepoUrl(item.repository?.url || '')}@${item.sha}`,
|
|
2049
|
+
}))
|
|
2050
|
+
.map(commit => {
|
|
2051
|
+
// Remove undefined fields
|
|
2052
|
+
const cleanCommit = {};
|
|
2053
|
+
Object.entries(commit).forEach(([key, value]) => {
|
|
2054
|
+
if (value !== undefined) {
|
|
2055
|
+
cleanCommit[key] = value;
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
return cleanCommit;
|
|
2059
|
+
});
|
|
2060
|
+
const result = {
|
|
2061
|
+
commits: optimizedCommits,
|
|
2062
|
+
total_count: items.length,
|
|
2063
|
+
};
|
|
2064
|
+
// Add repository info if single repo
|
|
2065
|
+
if (singleRepo) {
|
|
2066
|
+
result.repository = {
|
|
2067
|
+
name: singleRepo.fullName,
|
|
2068
|
+
description: singleRepo.description,
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
// Add metadata for insights
|
|
2072
|
+
if (items.length > 1) {
|
|
2073
|
+
result.metadata = {
|
|
2074
|
+
timeframe: getTimeframe(items),
|
|
2075
|
+
unique_authors: uniqueAuthors,
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
return result;
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Extract single repository if all results are from same repo
|
|
2082
|
+
*/
|
|
2083
|
+
function extractSingleRepository(items) {
|
|
2084
|
+
if (items.length === 0)
|
|
2085
|
+
return null;
|
|
2086
|
+
const firstRepo = items[0].repository;
|
|
2087
|
+
const allSameRepo = items.every(item => item.repository.fullName === firstRepo.fullName);
|
|
2088
|
+
return allSameRepo ? firstRepo : null;
|
|
2089
|
+
}
|
|
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
|
+
}
|
|
1209
2142
|
async function searchGitHubCommits(params) {
|
|
1210
2143
|
const cacheKey = generateCacheKey('gh-commits', params);
|
|
1211
2144
|
return withCache(cacheKey, async () => {
|
|
1212
2145
|
try {
|
|
1213
|
-
const
|
|
1214
|
-
const result = await executeGitHubCommand(
|
|
2146
|
+
const args = buildGitHubCommitCliArgs(params);
|
|
2147
|
+
const result = await executeGitHubCommand('search', args, {
|
|
1215
2148
|
cache: false,
|
|
1216
2149
|
});
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
let commits = [];
|
|
1225
|
-
const analysis = {
|
|
1226
|
-
totalFound: 0,
|
|
1227
|
-
recentCommits: 0,
|
|
1228
|
-
topAuthors: [],
|
|
1229
|
-
repositories: new Set(),
|
|
1230
|
-
};
|
|
1231
|
-
// Parse JSON response from GitHub CLI
|
|
1232
|
-
commits = JSON.parse(rawContent);
|
|
1233
|
-
if (Array.isArray(commits) && commits.length > 0) {
|
|
1234
|
-
analysis.totalFound = commits.length;
|
|
1235
|
-
// Simple analysis
|
|
1236
|
-
const now = new Date();
|
|
1237
|
-
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
1238
|
-
const authorCounts = {};
|
|
1239
|
-
commits.forEach(commit => {
|
|
1240
|
-
// Count recent commits
|
|
1241
|
-
const commitDate = commit.commit?.author?.date || commit.commit?.committer?.date;
|
|
1242
|
-
if (commitDate && new Date(commitDate) > thirtyDaysAgo) {
|
|
1243
|
-
analysis.recentCommits++;
|
|
1244
|
-
}
|
|
1245
|
-
// Count authors
|
|
1246
|
-
const authorName = commit.commit?.author?.name || commit.author?.login || 'Unknown';
|
|
1247
|
-
authorCounts[authorName] = (authorCounts[authorName] || 0) + 1;
|
|
1248
|
-
// Track repositories
|
|
1249
|
-
if (commit.repository?.fullName) {
|
|
1250
|
-
analysis.repositories.add(commit.repository.fullName);
|
|
1251
|
-
}
|
|
2150
|
+
return result;
|
|
2151
|
+
}
|
|
2152
|
+
catch (error) {
|
|
2153
|
+
const errorMessage = error.message || '';
|
|
2154
|
+
if (errorMessage.includes('authentication')) {
|
|
2155
|
+
return createResult({
|
|
2156
|
+
error: 'GitHub authentication required',
|
|
1252
2157
|
});
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
.map(([name, count]) => ({ name, commits: count }));
|
|
1258
|
-
// Format commits for output
|
|
1259
|
-
const formattedCommits = commits.map(commit => ({
|
|
1260
|
-
sha: commit.sha,
|
|
1261
|
-
message: commit.commit?.message || '',
|
|
1262
|
-
author: {
|
|
1263
|
-
name: commit.commit?.author?.name,
|
|
1264
|
-
email: commit.commit?.author?.email,
|
|
1265
|
-
date: commit.commit?.author?.date,
|
|
1266
|
-
login: commit.author?.login,
|
|
1267
|
-
},
|
|
1268
|
-
committer: {
|
|
1269
|
-
name: commit.commit?.committer?.name,
|
|
1270
|
-
email: commit.commit?.committer?.email,
|
|
1271
|
-
date: commit.commit?.committer?.date,
|
|
1272
|
-
login: commit.committer?.login,
|
|
1273
|
-
},
|
|
1274
|
-
repository: commit.repository
|
|
1275
|
-
? {
|
|
1276
|
-
name: commit.repository.name,
|
|
1277
|
-
fullName: commit.repository.fullName,
|
|
1278
|
-
url: commit.repository.url,
|
|
1279
|
-
description: commit.repository.description,
|
|
1280
|
-
}
|
|
1281
|
-
: null,
|
|
1282
|
-
url: commit.url,
|
|
1283
|
-
parents: commit.parents?.map((p) => p.sha) || [],
|
|
1284
|
-
}));
|
|
1285
|
-
return createSuccessResult$1({
|
|
1286
|
-
query: params.query,
|
|
1287
|
-
total: analysis.totalFound,
|
|
1288
|
-
commits: formattedCommits,
|
|
1289
|
-
summary: {
|
|
1290
|
-
recentCommits: analysis.recentCommits,
|
|
1291
|
-
topAuthors: analysis.topAuthors,
|
|
1292
|
-
repositories: Array.from(analysis.repositories),
|
|
1293
|
-
},
|
|
2158
|
+
}
|
|
2159
|
+
if (errorMessage.includes('rate limit')) {
|
|
2160
|
+
return createResult({
|
|
2161
|
+
error: 'GitHub rate limit exceeded',
|
|
1294
2162
|
});
|
|
1295
2163
|
}
|
|
1296
|
-
return
|
|
1297
|
-
|
|
1298
|
-
total: 0,
|
|
1299
|
-
commits: [],
|
|
2164
|
+
return createResult({
|
|
2165
|
+
error: 'Commit search execution failed',
|
|
1300
2166
|
});
|
|
1301
2167
|
}
|
|
1302
|
-
catch (error) {
|
|
1303
|
-
return createErrorResult$1('GitHub commit search failed - verify repository exists or try different filters', error);
|
|
1304
|
-
}
|
|
1305
2168
|
});
|
|
1306
2169
|
}
|
|
1307
|
-
function
|
|
1308
|
-
// Build query following GitHub CLI patterns
|
|
1309
|
-
const query = params.query?.trim() || '';
|
|
1310
|
-
// Handle complex queries (with qualifiers, operators, or --) differently
|
|
1311
|
-
const hasComplexSyntax = query.includes('--') ||
|
|
1312
|
-
query.includes(':') ||
|
|
1313
|
-
query.includes('OR') ||
|
|
1314
|
-
query.includes('AND') ||
|
|
1315
|
-
query.includes('(') ||
|
|
1316
|
-
query.includes(')') ||
|
|
1317
|
-
query.startsWith('-');
|
|
2170
|
+
function buildGitHubCommitCliArgs(params) {
|
|
1318
2171
|
const args = ['commits'];
|
|
1319
|
-
//
|
|
1320
|
-
if (query) {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
const queryParts = query.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
1324
|
-
queryParts.forEach(part => {
|
|
1325
|
-
// If part contains shell special characters, quote it
|
|
1326
|
-
if (/[><=&|$`(){}[\];\\]/.test(part) && !part.includes('"')) {
|
|
1327
|
-
args.push(`"${part}"`);
|
|
1328
|
-
}
|
|
1329
|
-
else {
|
|
1330
|
-
args.push(part);
|
|
1331
|
-
}
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
else {
|
|
1335
|
-
// For simple queries, use quoting logic
|
|
1336
|
-
const queryString = needsQuoting(query) ? `"${query}"` : query;
|
|
1337
|
-
args.push(queryString);
|
|
1338
|
-
}
|
|
2172
|
+
// Add query if provided - simplified approach for better results
|
|
2173
|
+
if (params.query) {
|
|
2174
|
+
// Simple, direct query handling - GitHub commit search works better with straightforward queries
|
|
2175
|
+
args.push(params.query.trim());
|
|
1339
2176
|
}
|
|
1340
|
-
//
|
|
1341
|
-
|
|
1342
|
-
|
|
2177
|
+
// Repository filters
|
|
2178
|
+
if (params.owner && params.repo) {
|
|
2179
|
+
args.push(`--repo=${params.owner}/${params.repo}`);
|
|
2180
|
+
}
|
|
2181
|
+
else if (params.owner) {
|
|
2182
|
+
args.push(`--owner=${params.owner}`);
|
|
2183
|
+
}
|
|
2184
|
+
// Author filters
|
|
1343
2185
|
if (params.author)
|
|
1344
2186
|
args.push(`--author=${params.author}`);
|
|
1345
|
-
if (params.
|
|
1346
|
-
args.push(`--author-
|
|
2187
|
+
if (params.authorName)
|
|
2188
|
+
args.push(`--author-name=${params.authorName}`);
|
|
1347
2189
|
if (params.authorEmail)
|
|
1348
2190
|
args.push(`--author-email=${params.authorEmail}`);
|
|
1349
|
-
|
|
1350
|
-
args.push(`--author-name="${params.authorName}"`);
|
|
2191
|
+
// Committer filters
|
|
1351
2192
|
if (params.committer)
|
|
1352
2193
|
args.push(`--committer=${params.committer}`);
|
|
1353
|
-
if (params.
|
|
1354
|
-
args.push(`--committer-
|
|
2194
|
+
if (params.committerName)
|
|
2195
|
+
args.push(`--committer-name=${params.committerName}`);
|
|
1355
2196
|
if (params.committerEmail)
|
|
1356
2197
|
args.push(`--committer-email=${params.committerEmail}`);
|
|
1357
|
-
|
|
1358
|
-
|
|
2198
|
+
// Date filters
|
|
2199
|
+
if (params.authorDate)
|
|
2200
|
+
args.push(`--author-date=${params.authorDate}`);
|
|
2201
|
+
if (params.committerDate)
|
|
2202
|
+
args.push(`--committer-date=${params.committerDate}`);
|
|
2203
|
+
// Hash filters
|
|
1359
2204
|
if (params.hash)
|
|
1360
2205
|
args.push(`--hash=${params.hash}`);
|
|
1361
2206
|
if (params.parent)
|
|
1362
2207
|
args.push(`--parent=${params.parent}`);
|
|
1363
2208
|
if (params.tree)
|
|
1364
2209
|
args.push(`--tree=${params.tree}`);
|
|
1365
|
-
|
|
1366
|
-
|
|
2210
|
+
// State filters
|
|
2211
|
+
if (params.merge !== undefined)
|
|
2212
|
+
args.push(`--merge=${params.merge}`);
|
|
2213
|
+
// Visibility
|
|
1367
2214
|
if (params.visibility)
|
|
1368
2215
|
args.push(`--visibility=${params.visibility}`);
|
|
1369
|
-
//
|
|
1370
|
-
if (params.
|
|
1371
|
-
args.push(`--
|
|
1372
|
-
}
|
|
1373
|
-
else if (params.repo) {
|
|
1374
|
-
args.push(`--repo=${params.repo}`);
|
|
1375
|
-
}
|
|
1376
|
-
else if (params.owner) {
|
|
1377
|
-
args.push(`--owner=${params.owner}`);
|
|
1378
|
-
}
|
|
1379
|
-
// Sorting
|
|
1380
|
-
const sortBy = params.sort || 'best-match';
|
|
1381
|
-
if (sortBy !== 'best-match') {
|
|
1382
|
-
args.push(`--sort=${sortBy}`);
|
|
1383
|
-
}
|
|
2216
|
+
// Sorting and pagination
|
|
2217
|
+
if (params.sort)
|
|
2218
|
+
args.push(`--sort=${params.sort}`);
|
|
1384
2219
|
if (params.order)
|
|
1385
2220
|
args.push(`--order=${params.order}`);
|
|
1386
|
-
// Limit
|
|
1387
2221
|
if (params.limit)
|
|
1388
2222
|
args.push(`--limit=${params.limit}`);
|
|
1389
|
-
|
|
2223
|
+
// JSON output
|
|
2224
|
+
args.push('--json=sha,commit,author,committer,repository,url,parents');
|
|
2225
|
+
return args;
|
|
1390
2226
|
}
|
|
1391
2227
|
|
|
1392
2228
|
// TODO: add PR commeents. e.g, gh pr view <PR_NUMBER_OR_URL_OR_BRANCH> --comments
|
|
1393
2229
|
const TOOL_NAME$4 = 'github_search_pull_requests';
|
|
1394
|
-
const DESCRIPTION$4 = `Find pull requests and implementations with detailed metadata. Discover
|
|
1395
|
-
|
|
1396
|
-
SEARCH PATTERNS SUPPORTED:
|
|
1397
|
-
• BOOLEAN OPERATORS: "fix AND bug" (both required), "refactor OR cleanup" (either term), "feature NOT draft" (excludes draft)
|
|
1398
|
-
• EXACT PHRASES: "initial commit" (precise phrase matching)
|
|
1399
|
-
• GITHUB QUALIFIERS: Built-in support for "is:merged", "review:approved", "base:main", etc.
|
|
1400
|
-
• COMBINABLE: Mix search terms with filters for targeted PR discovery
|
|
1401
|
-
|
|
1402
|
-
Filter by state, author, reviewer, or merge status for comprehensive development workflow analysis.`;
|
|
2230
|
+
const DESCRIPTION$4 = `Find pull requests and implementations with detailed metadata. Discover feature implementations, code review patterns, and development workflows.`;
|
|
1403
2231
|
function registerSearchGitHubPullRequestsTool(server) {
|
|
1404
|
-
server.
|
|
1405
|
-
query: z
|
|
1406
|
-
.string()
|
|
1407
|
-
.min(1, 'Search query is required and cannot be empty')
|
|
1408
|
-
.describe('Search query with GITHUB SEARCH SYNTAX support. BOOLEAN OPERATORS: "fix AND bug" (both required), "refactor OR cleanup" (either term), "feature NOT draft" (excludes). EXACT PHRASES: "initial commit" (precise matching). GITHUB QUALIFIERS: "is:merged review:approved base:main" (native GitHub syntax). COMBINED: Mix boolean logic with qualifiers for precise PR discovery.'),
|
|
1409
|
-
owner: z.string().optional().describe('Repository owner/organization'),
|
|
1410
|
-
repo: z.string().optional().describe('Repository name'),
|
|
1411
|
-
author: z.string().optional().describe('Filter by pull request author'),
|
|
1412
|
-
assignee: z.string().optional().describe('Filter by assignee'),
|
|
1413
|
-
mentions: z.string().optional().describe('Filter by user mentions'),
|
|
1414
|
-
commenter: z.string().optional().describe('Filter by comments by user'),
|
|
1415
|
-
involves: z.string().optional().describe('Filter by user involvement'),
|
|
1416
|
-
reviewedBy: z.string().optional().describe('Filter by user who reviewed'),
|
|
1417
|
-
reviewRequested: z
|
|
1418
|
-
.string()
|
|
1419
|
-
.optional()
|
|
1420
|
-
.describe('Filter by user or team requested to review'),
|
|
1421
|
-
state: z.enum(['open', 'closed']).optional().describe('Filter by state'),
|
|
1422
|
-
head: z.string().optional().describe('Filter by head branch name'),
|
|
1423
|
-
base: z.string().optional().describe('Filter by base branch name'),
|
|
1424
|
-
language: z.string().optional().describe('Filter by coding language'),
|
|
1425
|
-
created: z
|
|
1426
|
-
.string()
|
|
1427
|
-
.optional()
|
|
1428
|
-
.describe("Filter by created date (e.g., '>2022-01-01')"),
|
|
1429
|
-
updated: z.string().optional().describe('Filter by last updated date'),
|
|
1430
|
-
mergedAt: z.string().optional().describe('Filter by merged date'),
|
|
1431
|
-
closed: z.string().optional().describe('Filter by closed date'),
|
|
1432
|
-
draft: z.boolean().optional().describe('Filter by draft state'),
|
|
1433
|
-
checks: z
|
|
1434
|
-
.enum(['pending', 'success', 'failure'])
|
|
1435
|
-
.optional()
|
|
1436
|
-
.describe('Filter based on status of the checks'),
|
|
1437
|
-
merged: z.boolean().optional().describe('Filter based on merged state'),
|
|
1438
|
-
review: z
|
|
1439
|
-
.enum(['none', 'required', 'approved', 'changes_requested'])
|
|
1440
|
-
.optional()
|
|
1441
|
-
.describe('Filter based on review status'),
|
|
1442
|
-
limit: z
|
|
1443
|
-
.number()
|
|
1444
|
-
.int()
|
|
1445
|
-
.min(1)
|
|
1446
|
-
.max(50)
|
|
1447
|
-
.optional()
|
|
1448
|
-
.default(25)
|
|
1449
|
-
.describe('Maximum results (default: 25, max: 50)'),
|
|
1450
|
-
sort: z
|
|
1451
|
-
.enum([
|
|
1452
|
-
'comments',
|
|
1453
|
-
'reactions',
|
|
1454
|
-
'reactions-+1',
|
|
1455
|
-
'reactions--1',
|
|
1456
|
-
'reactions-smile',
|
|
1457
|
-
'reactions-thinking_face',
|
|
1458
|
-
'reactions-heart',
|
|
1459
|
-
'reactions-tada',
|
|
1460
|
-
'interactions',
|
|
1461
|
-
'created',
|
|
1462
|
-
'updated',
|
|
1463
|
-
])
|
|
1464
|
-
.optional()
|
|
1465
|
-
.describe('Sort criteria'),
|
|
1466
|
-
order: z
|
|
1467
|
-
.enum(['asc', 'desc'])
|
|
1468
|
-
.optional()
|
|
1469
|
-
.default('desc')
|
|
1470
|
-
.describe('Order (default: desc)'),
|
|
1471
|
-
}, {
|
|
1472
|
-
title: TOOL_NAME$4,
|
|
2232
|
+
server.registerTool(TOOL_NAME$4, {
|
|
1473
2233
|
description: DESCRIPTION$4,
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2234
|
+
inputSchema: {
|
|
2235
|
+
query: z
|
|
2236
|
+
.string()
|
|
2237
|
+
.min(1, 'Search query is required and cannot be empty')
|
|
2238
|
+
.describe('Search query with GitHub syntax. Boolean: "fix AND bug", exact phrases: "initial commit", qualifiers: "is:merged review:approved".'),
|
|
2239
|
+
owner: z.string().optional().describe('Repository owner/organization'),
|
|
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
|
|
2247
|
+
.string()
|
|
2248
|
+
.optional()
|
|
2249
|
+
.describe('Filter by user who reviewed'),
|
|
2250
|
+
reviewRequested: z
|
|
2251
|
+
.string()
|
|
2252
|
+
.optional()
|
|
2253
|
+
.describe('Filter by user or team requested to review'),
|
|
2254
|
+
state: z
|
|
2255
|
+
.enum(['open', 'closed'])
|
|
2256
|
+
.optional()
|
|
2257
|
+
.describe('Filter by state'),
|
|
2258
|
+
head: z.string().optional().describe('Filter by head branch name'),
|
|
2259
|
+
base: z.string().optional().describe('Filter by base branch name'),
|
|
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'])
|
|
2268
|
+
.optional()
|
|
2269
|
+
.describe('Filter based on status of the checks'),
|
|
2270
|
+
merged: z.boolean().optional().describe('Filter based on merged state'),
|
|
2271
|
+
review: z
|
|
2272
|
+
.enum(['none', 'required', 'approved', 'changes_requested'])
|
|
2273
|
+
.optional()
|
|
2274
|
+
.describe('Filter based on review status'),
|
|
2275
|
+
limit: z
|
|
2276
|
+
.number()
|
|
2277
|
+
.int()
|
|
2278
|
+
.min(1)
|
|
2279
|
+
.max(50)
|
|
2280
|
+
.optional()
|
|
2281
|
+
.default(25)
|
|
2282
|
+
.describe('Maximum results (default: 25, max: 50)'),
|
|
2283
|
+
sort: z
|
|
2284
|
+
.enum([
|
|
2285
|
+
'comments',
|
|
2286
|
+
'reactions',
|
|
2287
|
+
'reactions-+1',
|
|
2288
|
+
'reactions--1',
|
|
2289
|
+
'reactions-smile',
|
|
2290
|
+
'reactions-thinking_face',
|
|
2291
|
+
'reactions-heart',
|
|
2292
|
+
'reactions-tada',
|
|
2293
|
+
'interactions',
|
|
2294
|
+
'created',
|
|
2295
|
+
'updated',
|
|
2296
|
+
])
|
|
2297
|
+
.optional()
|
|
2298
|
+
.describe('Sort criteria'),
|
|
2299
|
+
order: z
|
|
2300
|
+
.enum(['asc', 'desc'])
|
|
2301
|
+
.optional()
|
|
2302
|
+
.default('desc')
|
|
2303
|
+
.describe('Order (default: desc)'),
|
|
2304
|
+
},
|
|
2305
|
+
annotations: {
|
|
2306
|
+
title: 'GitHub Pull Requests Search',
|
|
2307
|
+
readOnlyHint: true,
|
|
2308
|
+
destructiveHint: false,
|
|
2309
|
+
idempotentHint: true,
|
|
2310
|
+
openWorldHint: true,
|
|
2311
|
+
},
|
|
1478
2312
|
}, async (args) => {
|
|
1479
2313
|
if (!args.query?.trim()) {
|
|
1480
|
-
return
|
|
2314
|
+
return createResult({
|
|
2315
|
+
error: 'Search query is required and cannot be empty - provide keywords to search for pull requests',
|
|
2316
|
+
});
|
|
1481
2317
|
}
|
|
1482
2318
|
if (args.query.length > 256) {
|
|
1483
|
-
return
|
|
2319
|
+
return createResult({
|
|
2320
|
+
error: 'Search query is too long. Please limit to 256 characters or less - simplify your search terms',
|
|
2321
|
+
});
|
|
1484
2322
|
}
|
|
1485
2323
|
try {
|
|
1486
2324
|
return await searchGitHubPullRequests(args);
|
|
1487
2325
|
}
|
|
1488
2326
|
catch (error) {
|
|
1489
|
-
return
|
|
2327
|
+
return createResult({
|
|
2328
|
+
error: 'GitHub pull requests search failed - verify repository access and query syntax',
|
|
2329
|
+
});
|
|
1490
2330
|
}
|
|
1491
2331
|
});
|
|
1492
2332
|
}
|
|
@@ -1509,8 +2349,8 @@ async function searchGitHubPullRequests(params) {
|
|
|
1509
2349
|
author: pr.user?.login || '',
|
|
1510
2350
|
repository: pr.repository_url?.split('/').slice(-2).join('/') || 'unknown',
|
|
1511
2351
|
labels: pr.labels?.map(l => l.name) || [],
|
|
1512
|
-
created_at: pr.created_at,
|
|
1513
|
-
updated_at: pr.updated_at,
|
|
2352
|
+
created_at: toDDMMYYYY(pr.created_at),
|
|
2353
|
+
updated_at: toDDMMYYYY(pr.updated_at),
|
|
1514
2354
|
url: pr.html_url,
|
|
1515
2355
|
comments: pr.comments,
|
|
1516
2356
|
reactions: pr.reactions?.total_count || 0,
|
|
@@ -1520,7 +2360,7 @@ async function searchGitHubPullRequests(params) {
|
|
|
1520
2360
|
if (pr.merged_at)
|
|
1521
2361
|
result.merged_at = pr.merged_at;
|
|
1522
2362
|
if (pr.closed_at)
|
|
1523
|
-
result.closed_at = pr.closed_at;
|
|
2363
|
+
result.closed_at = toDDMMYYYY(pr.closed_at);
|
|
1524
2364
|
if (pr.head?.ref)
|
|
1525
2365
|
result.head = pr.head.ref;
|
|
1526
2366
|
if (pr.base?.ref)
|
|
@@ -1528,15 +2368,13 @@ async function searchGitHubPullRequests(params) {
|
|
|
1528
2368
|
return result;
|
|
1529
2369
|
});
|
|
1530
2370
|
const searchResult = {
|
|
1531
|
-
searchType: 'prs',
|
|
1532
|
-
query: params.query || '',
|
|
1533
2371
|
results: cleanPRs,
|
|
2372
|
+
total_count: apiResponse.total_count || cleanPRs.length,
|
|
1534
2373
|
metadata: {
|
|
1535
|
-
total_count: apiResponse.total_count || 0,
|
|
1536
2374
|
incomplete_results: apiResponse.incomplete_results || false,
|
|
1537
2375
|
},
|
|
1538
2376
|
};
|
|
1539
|
-
return
|
|
2377
|
+
return createResult({ data: searchResult });
|
|
1540
2378
|
});
|
|
1541
2379
|
}
|
|
1542
2380
|
function buildGitHubPullRequestsAPICommand(params) {
|
|
@@ -1597,36 +2435,32 @@ function buildGitHubPullRequestsAPICommand(params) {
|
|
|
1597
2435
|
}
|
|
1598
2436
|
|
|
1599
2437
|
const TOOL_NAME$3 = 'npm_package_search';
|
|
1600
|
-
const DESCRIPTION$3 = `Search npm packages by keywords using fuzzy matching.
|
|
1601
|
-
|
|
1602
|
-
IMPORTANT LIMITATIONS:
|
|
1603
|
-
• NO BOOLEAN OPERATORS: NPM search does NOT support AND/OR/NOT - use space-separated keywords for broader search
|
|
1604
|
-
• FUZZY MATCHING ONLY: No exact phrase matching - searches are approximate keyword matching
|
|
1605
|
-
• KEYWORD-BASED: Best results with simple, space-separated terms like "react hooks" or "cli typescript"
|
|
1606
|
-
|
|
1607
|
-
Required when package name is unknown. If you have the exact package name, use npm_view_package directly. This reduces the need to use GitHub search when packages are found.`;
|
|
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.`;
|
|
1608
2439
|
const MAX_DESCRIPTION_LENGTH = 100;
|
|
1609
2440
|
const MAX_KEYWORDS = 10;
|
|
1610
2441
|
function registerNpmSearchTool(server) {
|
|
1611
|
-
server.
|
|
1612
|
-
queries: z
|
|
1613
|
-
.union([z.string(), z.array(z.string())])
|
|
1614
|
-
.describe('Package names or keywords to search for. NOTE: No boolean operators (AND/OR/NOT) supported - use simple space-separated keywords like "react hooks" or "typescript cli" for fuzzy matching.'),
|
|
1615
|
-
searchlimit: z
|
|
1616
|
-
.number()
|
|
1617
|
-
.int()
|
|
1618
|
-
.min(1)
|
|
1619
|
-
.max(50)
|
|
1620
|
-
.optional()
|
|
1621
|
-
.default(20)
|
|
1622
|
-
.describe('Max results per query (default: 20, max: 50)'),
|
|
1623
|
-
}, {
|
|
1624
|
-
title: TOOL_NAME$3,
|
|
2442
|
+
server.registerTool(TOOL_NAME$3, {
|
|
1625
2443
|
description: DESCRIPTION$3,
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
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
|
+
},
|
|
1630
2464
|
}, async (args) => {
|
|
1631
2465
|
try {
|
|
1632
2466
|
const queries = Array.isArray(args.queries)
|
|
@@ -1645,17 +2479,22 @@ function registerNpmSearchTool(server) {
|
|
|
1645
2479
|
const deduplicatedPackages = deduplicatePackages(allPackages);
|
|
1646
2480
|
if (deduplicatedPackages.length > 0) {
|
|
1647
2481
|
return createResult({
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
:
|
|
1651
|
-
|
|
1652
|
-
results: deduplicatedPackages,
|
|
2482
|
+
data: {
|
|
2483
|
+
total_count: deduplicatedPackages.length,
|
|
2484
|
+
results: deduplicatedPackages,
|
|
2485
|
+
},
|
|
1653
2486
|
});
|
|
1654
2487
|
}
|
|
1655
|
-
return createResult(
|
|
2488
|
+
return createResult({
|
|
2489
|
+
error: 'No packages found',
|
|
2490
|
+
cli_command: `npm search ${Array.isArray(args.queries) ? args.queries.join(' ') : args.queries}`,
|
|
2491
|
+
});
|
|
1656
2492
|
}
|
|
1657
2493
|
catch (error) {
|
|
1658
|
-
return createResult(
|
|
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
|
+
});
|
|
1659
2498
|
}
|
|
1660
2499
|
});
|
|
1661
2500
|
}
|
|
@@ -1708,70 +2547,54 @@ function parseNpmSearchOutput(output) {
|
|
|
1708
2547
|
}
|
|
1709
2548
|
|
|
1710
2549
|
const TOOL_NAME$2 = 'github_get_contents';
|
|
1711
|
-
const DESCRIPTION$2 = `Browse repository structure and verify file existence. Use before
|
|
2550
|
+
const DESCRIPTION$2 = `Browse repository structure and verify file existence. Smart branch detection with fallbacks. Use before fetching files to understand organization.`;
|
|
1712
2551
|
function registerViewRepositoryStructureTool(server) {
|
|
1713
|
-
server.
|
|
1714
|
-
owner: z
|
|
1715
|
-
.string()
|
|
1716
|
-
.min(1)
|
|
1717
|
-
.max(100)
|
|
1718
|
-
.regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
|
|
1719
|
-
.describe(`Repository owner/organization (e.g., 'microsoft', 'facebook'). Use github_search_repositories to discover valid owners.`),
|
|
1720
|
-
repo: z
|
|
1721
|
-
.string()
|
|
1722
|
-
.min(1)
|
|
1723
|
-
.max(100)
|
|
1724
|
-
.regex(/^[a-zA-Z0-9._-]+$/, 'Invalid repository name format')
|
|
1725
|
-
.describe('Repository name (e.g., "vscode", "react"). Case-sensitive. Use github_search_repositories to find exact names.'),
|
|
1726
|
-
branch: z
|
|
1727
|
-
.string()
|
|
1728
|
-
.min(1)
|
|
1729
|
-
.max(255)
|
|
1730
|
-
.regex(/^[^\s]+$/, 'Branch name cannot contain spaces')
|
|
1731
|
-
.describe("Target branch name (e.g., 'main', 'canary', 'develop'). " +
|
|
1732
|
-
'Auto-detects repository default if not found. ' +
|
|
1733
|
-
'Use github_search_repositories or api calls to discover valid branches first.'),
|
|
1734
|
-
path: z
|
|
1735
|
-
.string()
|
|
1736
|
-
.optional()
|
|
1737
|
-
.default('')
|
|
1738
|
-
.refine(path => !path.includes('..'), 'Path traversal not allowed')
|
|
1739
|
-
.refine(path => path.length <= 500, 'Path too long')
|
|
1740
|
-
.describe('Directory path within repository (e.g., "src/components", "packages/core"). ' +
|
|
1741
|
-
'Leave empty for root. Use previous results to navigate deeper.'),
|
|
1742
|
-
}, {
|
|
1743
|
-
title: TOOL_NAME$2,
|
|
2552
|
+
server.registerTool(TOOL_NAME$2, {
|
|
1744
2553
|
description: DESCRIPTION$2,
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2554
|
+
inputSchema: {
|
|
2555
|
+
owner: z
|
|
2556
|
+
.string()
|
|
2557
|
+
.min(1)
|
|
2558
|
+
.max(100)
|
|
2559
|
+
.regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
|
|
2560
|
+
.describe(`Repository owner/organization.`),
|
|
2561
|
+
repo: z
|
|
2562
|
+
.string()
|
|
2563
|
+
.min(1)
|
|
2564
|
+
.max(100)
|
|
2565
|
+
.regex(/^[a-zA-Z0-9._-]+$/, 'Invalid repository name format')
|
|
2566
|
+
.describe('Repository name. Case-sensitive.'),
|
|
2567
|
+
branch: z
|
|
2568
|
+
.string()
|
|
2569
|
+
.min(1)
|
|
2570
|
+
.max(255)
|
|
2571
|
+
.regex(/^[^\s]+$/, 'Branch name cannot contain spaces')
|
|
2572
|
+
.describe('Target branch name. Auto-detects default if not found.'),
|
|
2573
|
+
path: z
|
|
2574
|
+
.string()
|
|
2575
|
+
.optional()
|
|
2576
|
+
.default('')
|
|
2577
|
+
.refine(path => !path.includes('..'), 'Path traversal not allowed')
|
|
2578
|
+
.refine(path => path.length <= 500, 'Path too long')
|
|
2579
|
+
.describe('Directory path within repository. Leave empty for root.'),
|
|
2580
|
+
},
|
|
2581
|
+
annotations: {
|
|
2582
|
+
title: 'GitHub Repository Contents',
|
|
2583
|
+
readOnlyHint: true,
|
|
2584
|
+
destructiveHint: false,
|
|
2585
|
+
idempotentHint: true,
|
|
2586
|
+
openWorldHint: true,
|
|
2587
|
+
},
|
|
1749
2588
|
}, async (args) => {
|
|
1750
2589
|
try {
|
|
1751
2590
|
const result = await viewRepositoryStructure(args);
|
|
1752
|
-
if (result.isError) {
|
|
1753
|
-
return createResult(result.content[0].text, true);
|
|
1754
|
-
}
|
|
1755
|
-
if (result.content && result.content[0] && !result.isError) {
|
|
1756
|
-
const { data, parsed } = parseJsonResponse(result.content[0].text);
|
|
1757
|
-
if (parsed) {
|
|
1758
|
-
const typedResult = {
|
|
1759
|
-
path: data.path,
|
|
1760
|
-
baseUrl: data.baseUrl,
|
|
1761
|
-
files: data.files || [],
|
|
1762
|
-
folders: data.folders || [],
|
|
1763
|
-
...(data.branchFallback && {
|
|
1764
|
-
branchFallback: data.branchFallback,
|
|
1765
|
-
}),
|
|
1766
|
-
};
|
|
1767
|
-
return createResult(typedResult);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
2591
|
return result;
|
|
1771
2592
|
}
|
|
1772
2593
|
catch (error) {
|
|
1773
2594
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1774
|
-
return createResult(
|
|
2595
|
+
return createResult({
|
|
2596
|
+
error: `Repository exploration failed: ${errorMessage} - verify access and permissions`,
|
|
2597
|
+
});
|
|
1775
2598
|
}
|
|
1776
2599
|
});
|
|
1777
2600
|
}
|
|
@@ -1784,6 +2607,7 @@ function registerViewRepositoryStructureTool(server) {
|
|
|
1784
2607
|
* - Input validation: prevents path traversal and validates GitHub naming
|
|
1785
2608
|
* - Clear error context: provides descriptive error messages
|
|
1786
2609
|
* - Efficient caching: avoids redundant API calls
|
|
2610
|
+
* - Rich metadata: includes all GitHub API fields (sha, urls, links, etc.)
|
|
1787
2611
|
*/
|
|
1788
2612
|
async function viewRepositoryStructure(params) {
|
|
1789
2613
|
const cacheKey = generateCacheKey('gh-repo-structure', params);
|
|
@@ -1825,17 +2649,25 @@ async function viewRepositoryStructure(params) {
|
|
|
1825
2649
|
const errorMsg = lastError?.message || 'Unknown error';
|
|
1826
2650
|
if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
|
|
1827
2651
|
if (path) {
|
|
1828
|
-
|
|
2652
|
+
return createResult({
|
|
2653
|
+
error: `Path "${path}" not found - verify path or use code search`,
|
|
2654
|
+
});
|
|
1829
2655
|
}
|
|
1830
2656
|
else {
|
|
1831
|
-
|
|
2657
|
+
return createResult({
|
|
2658
|
+
error: `Repository not found: ${owner}/${repo} - verify names`,
|
|
2659
|
+
});
|
|
1832
2660
|
}
|
|
1833
2661
|
}
|
|
1834
2662
|
else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
|
|
1835
|
-
|
|
2663
|
+
return createResult({
|
|
2664
|
+
error: `Access denied to ${owner}/${repo} - check permissions`,
|
|
2665
|
+
});
|
|
1836
2666
|
}
|
|
1837
2667
|
else {
|
|
1838
|
-
|
|
2668
|
+
return createResult({
|
|
2669
|
+
error: `Access failed: ${owner}/${repo} - check connection`,
|
|
2670
|
+
});
|
|
1839
2671
|
}
|
|
1840
2672
|
}
|
|
1841
2673
|
// Limit total items to 100 for efficiency
|
|
@@ -1847,53 +2679,54 @@ async function viewRepositoryStructure(params) {
|
|
|
1847
2679
|
}
|
|
1848
2680
|
return a.name.localeCompare(b.name);
|
|
1849
2681
|
});
|
|
1850
|
-
// Create
|
|
1851
|
-
const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${cleanPath ? cleanPath + '/' : ''}`;
|
|
1852
|
-
// Transform to lean structure with simplified paths and URLs
|
|
2682
|
+
// Create simplified, token-efficient structure
|
|
1853
2683
|
const files = limitedItems
|
|
1854
2684
|
.filter(item => item.type === 'file')
|
|
1855
|
-
.map(item => {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
if (cleanPath && item.path.startsWith(cleanPath + '/')) {
|
|
1861
|
-
simplifiedPath = item.path.substring(cleanPath.length + 1);
|
|
1862
|
-
}
|
|
1863
|
-
else if (!cleanPath) {
|
|
1864
|
-
// At root level, use the full path but without leading slash
|
|
1865
|
-
simplifiedPath = item.path;
|
|
1866
|
-
}
|
|
1867
|
-
// Extract just the filename and query params from the URL
|
|
1868
|
-
const urlParts = item.url.split('/');
|
|
1869
|
-
const filename = urlParts[urlParts.length - 1]; // Gets "filename?ref=branch"
|
|
1870
|
-
return {
|
|
1871
|
-
name: simplifiedPath,
|
|
1872
|
-
size: item.size,
|
|
1873
|
-
url: filename, // Just the filename and query params
|
|
1874
|
-
};
|
|
1875
|
-
});
|
|
2685
|
+
.map(item => ({
|
|
2686
|
+
name: item.name,
|
|
2687
|
+
size: item.size,
|
|
2688
|
+
url: item.path, // Use path for fetching
|
|
2689
|
+
}));
|
|
1876
2690
|
const folders = limitedItems
|
|
1877
2691
|
.filter(item => item.type === 'dir')
|
|
1878
|
-
.map(item =>
|
|
2692
|
+
.map(item => ({
|
|
2693
|
+
name: item.name,
|
|
2694
|
+
url: item.path, // Use path for browsing
|
|
2695
|
+
}));
|
|
2696
|
+
// Simplified result structure - token efficient
|
|
1879
2697
|
const result = {
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
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
|
+
}),
|
|
1889
2721
|
},
|
|
1890
2722
|
}),
|
|
1891
2723
|
};
|
|
1892
|
-
return
|
|
2724
|
+
return createResult({ data: result });
|
|
1893
2725
|
}
|
|
1894
2726
|
catch (error) {
|
|
1895
|
-
|
|
1896
|
-
|
|
2727
|
+
return createResult({
|
|
2728
|
+
error: `Repository access failed - verify repository and authentication: ${error}`,
|
|
2729
|
+
});
|
|
1897
2730
|
}
|
|
1898
2731
|
});
|
|
1899
2732
|
}
|
|
@@ -1930,133 +2763,152 @@ async function getSmartBranchFallback(owner, repo, requestedBranch) {
|
|
|
1930
2763
|
}
|
|
1931
2764
|
|
|
1932
2765
|
const TOOL_NAME$1 = 'github_search_issues';
|
|
1933
|
-
const DESCRIPTION$1 = `Find GitHub issues
|
|
1934
|
-
|
|
1935
|
-
SEARCH PATTERNS SUPPORTED:
|
|
1936
|
-
• BOOLEAN OPERATORS: "bug AND crash" (both required), "feature OR enhancement" (either term), "error NOT test" (excludes test)
|
|
1937
|
-
• EXACT PHRASES: "memory leak" (precise phrase matching)
|
|
1938
|
-
• GITHUB QUALIFIERS: Built-in support for "is:open", "label:bug", "author:username", etc.
|
|
1939
|
-
• COMBINABLE: Mix search terms with filters for surgical precision
|
|
1940
|
-
|
|
1941
|
-
Filter by state, labels, assignee, or date ranges for comprehensive issue discovery.`;
|
|
2766
|
+
const DESCRIPTION$1 = `Find GitHub issues with rich metadata. Discover pain points, feature requests, and bug patterns with boolean logic and GitHub qualifiers.`;
|
|
1942
2767
|
function registerSearchGitHubIssuesTool(server) {
|
|
1943
|
-
server.
|
|
1944
|
-
query: z
|
|
1945
|
-
.string()
|
|
1946
|
-
.min(1, 'Search query is required and cannot be empty')
|
|
1947
|
-
.describe('Search query with GITHUB SEARCH SYNTAX support. BOOLEAN OPERATORS: "bug AND crash" (both required), "feature OR enhancement" (either term), "error NOT test" (excludes). EXACT PHRASES: "memory leak" (precise matching). GITHUB QUALIFIERS: "is:open label:bug author:username" (native GitHub syntax). COMBINED: Mix boolean logic with qualifiers for precise issue discovery.'),
|
|
1948
|
-
owner: z
|
|
1949
|
-
.string()
|
|
1950
|
-
.min(1)
|
|
1951
|
-
.optional()
|
|
1952
|
-
.describe('Repository owner/organization. Leave empty for global search.'),
|
|
1953
|
-
repo: z
|
|
1954
|
-
.string()
|
|
1955
|
-
.optional()
|
|
1956
|
-
.describe('Repository name. Do exploratory search without repo filter first'),
|
|
1957
|
-
app: z.string().optional().describe('Filter by GitHub App author'),
|
|
1958
|
-
archived: z
|
|
1959
|
-
.boolean()
|
|
1960
|
-
.optional()
|
|
1961
|
-
.describe('Filter by repository archived state'),
|
|
1962
|
-
assignee: z.string().optional().describe('Filter by assignee'),
|
|
1963
|
-
author: z.string().optional().describe('Filter by issue author'),
|
|
1964
|
-
closed: z.string().optional().describe('Filter by closed date'),
|
|
1965
|
-
commenter: z.string().optional().describe('Filter by user who commented'),
|
|
1966
|
-
comments: z.number().optional().describe('Filter by number of comments'),
|
|
1967
|
-
created: z
|
|
1968
|
-
.string()
|
|
1969
|
-
.optional()
|
|
1970
|
-
.describe("Filter by created date (e.g., '>2022-01-01')"),
|
|
1971
|
-
includePrs: z
|
|
1972
|
-
.boolean()
|
|
1973
|
-
.optional()
|
|
1974
|
-
.describe('Include pull requests in results'),
|
|
1975
|
-
interactions: z
|
|
1976
|
-
.number()
|
|
1977
|
-
.optional()
|
|
1978
|
-
.describe('Filter by reactions and comments count'),
|
|
1979
|
-
involves: z.string().optional().describe('Filter by user involvement'),
|
|
1980
|
-
labels: z.string().optional().describe('Filter by labels'),
|
|
1981
|
-
language: z.string().optional().describe('Filter by coding language'),
|
|
1982
|
-
locked: z
|
|
1983
|
-
.boolean()
|
|
1984
|
-
.optional()
|
|
1985
|
-
.describe('Filter by locked conversation status'),
|
|
1986
|
-
match: z
|
|
1987
|
-
.enum(['title', 'body', 'comments'])
|
|
1988
|
-
.optional()
|
|
1989
|
-
.describe('Restrict search to specific field'),
|
|
1990
|
-
mentions: z.string().optional().describe('Filter by user mentions'),
|
|
1991
|
-
milestone: z.string().optional().describe('Filter by milestone title'),
|
|
1992
|
-
noAssignee: z.boolean().optional().describe('Filter by missing assignee'),
|
|
1993
|
-
noLabel: z.boolean().optional().describe('Filter by missing label'),
|
|
1994
|
-
noMilestone: z
|
|
1995
|
-
.boolean()
|
|
1996
|
-
.optional()
|
|
1997
|
-
.describe('Filter by missing milestone'),
|
|
1998
|
-
noProject: z.boolean().optional().describe('Filter by missing project'),
|
|
1999
|
-
project: z.string().optional().describe('Filter by project board'),
|
|
2000
|
-
reactions: z.number().optional().describe('Filter by reactions count'),
|
|
2001
|
-
state: z
|
|
2002
|
-
.enum(['open', 'closed'])
|
|
2003
|
-
.optional()
|
|
2004
|
-
.describe('Filter by issue state'),
|
|
2005
|
-
teamMentions: z.string().optional().describe('Filter by team mentions'),
|
|
2006
|
-
updated: z.string().optional().describe('Filter by last updated date'),
|
|
2007
|
-
visibility: z
|
|
2008
|
-
.enum(['public', 'private', 'internal'])
|
|
2009
|
-
.optional()
|
|
2010
|
-
.describe('Filter by repository visibility'),
|
|
2011
|
-
sort: z
|
|
2012
|
-
.enum([
|
|
2013
|
-
'comments',
|
|
2014
|
-
'created',
|
|
2015
|
-
'interactions',
|
|
2016
|
-
'reactions',
|
|
2017
|
-
'reactions-+1',
|
|
2018
|
-
'reactions--1',
|
|
2019
|
-
'reactions-heart',
|
|
2020
|
-
'reactions-smile',
|
|
2021
|
-
'reactions-tada',
|
|
2022
|
-
'reactions-thinking_face',
|
|
2023
|
-
'updated',
|
|
2024
|
-
'best-match',
|
|
2025
|
-
])
|
|
2026
|
-
.optional()
|
|
2027
|
-
.describe('Sort criteria'),
|
|
2028
|
-
order: z
|
|
2029
|
-
.enum(['asc', 'desc'])
|
|
2030
|
-
.optional()
|
|
2031
|
-
.default('desc')
|
|
2032
|
-
.describe('Order (default: desc)'),
|
|
2033
|
-
limit: z
|
|
2034
|
-
.number()
|
|
2035
|
-
.int()
|
|
2036
|
-
.min(1)
|
|
2037
|
-
.max(50)
|
|
2038
|
-
.optional()
|
|
2039
|
-
.default(25)
|
|
2040
|
-
.describe('Maximum results (default: 25, max: 50)'),
|
|
2041
|
-
}, {
|
|
2042
|
-
title: TOOL_NAME$1,
|
|
2768
|
+
server.registerTool(TOOL_NAME$1, {
|
|
2043
2769
|
description: DESCRIPTION$1,
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2770
|
+
inputSchema: {
|
|
2771
|
+
query: z
|
|
2772
|
+
.string()
|
|
2773
|
+
.min(1, 'Search query is required and cannot be empty')
|
|
2774
|
+
.describe('Search query with GitHub syntax. Boolean: "bug AND crash", exact phrases: "memory leak", qualifiers: "is:open label:bug".'),
|
|
2775
|
+
owner: z
|
|
2776
|
+
.string()
|
|
2777
|
+
.min(1)
|
|
2778
|
+
.optional()
|
|
2779
|
+
.describe('Repository owner/organization. Leave empty for global search.'),
|
|
2780
|
+
repo: z
|
|
2781
|
+
.string()
|
|
2782
|
+
.optional()
|
|
2783
|
+
.describe('Repository name. Do exploratory search without repo filter first'),
|
|
2784
|
+
app: z.string().optional().describe('Filter by GitHub App author'),
|
|
2785
|
+
archived: z
|
|
2786
|
+
.boolean()
|
|
2787
|
+
.optional()
|
|
2788
|
+
.describe('Filter by repository archived state'),
|
|
2789
|
+
assignee: z.string().optional().describe('Filter by assignee'),
|
|
2790
|
+
author: z.string().optional().describe('Filter by issue author'),
|
|
2791
|
+
closed: z.string().optional().describe('Filter by closed date'),
|
|
2792
|
+
commenter: z
|
|
2793
|
+
.string()
|
|
2794
|
+
.optional()
|
|
2795
|
+
.describe('Filter by user who commented'),
|
|
2796
|
+
comments: z
|
|
2797
|
+
.number()
|
|
2798
|
+
.optional()
|
|
2799
|
+
.describe('Filter by number of comments'),
|
|
2800
|
+
created: z.string().optional().describe('Filter by created date'),
|
|
2801
|
+
includePrs: z
|
|
2802
|
+
.boolean()
|
|
2803
|
+
.optional()
|
|
2804
|
+
.describe('Include pull requests in results'),
|
|
2805
|
+
interactions: z
|
|
2806
|
+
.number()
|
|
2807
|
+
.optional()
|
|
2808
|
+
.describe('Filter by reactions and comments count'),
|
|
2809
|
+
involves: z.string().optional().describe('Filter by user involvement'),
|
|
2810
|
+
labels: z.string().optional().describe('Filter by labels'),
|
|
2811
|
+
language: z.string().optional().describe('Filter by coding language'),
|
|
2812
|
+
locked: z
|
|
2813
|
+
.boolean()
|
|
2814
|
+
.optional()
|
|
2815
|
+
.describe('Filter by locked conversation status'),
|
|
2816
|
+
match: z
|
|
2817
|
+
.enum(['title', 'body', 'comments'])
|
|
2818
|
+
.optional()
|
|
2819
|
+
.describe('Restrict search to specific field'),
|
|
2820
|
+
mentions: z.string().optional().describe('Filter by user mentions'),
|
|
2821
|
+
milestone: z.string().optional().describe('Filter by milestone title'),
|
|
2822
|
+
noAssignee: z
|
|
2823
|
+
.boolean()
|
|
2824
|
+
.optional()
|
|
2825
|
+
.describe('Filter by missing assignee'),
|
|
2826
|
+
noLabel: z.boolean().optional().describe('Filter by missing label'),
|
|
2827
|
+
noMilestone: z
|
|
2828
|
+
.boolean()
|
|
2829
|
+
.optional()
|
|
2830
|
+
.describe('Filter by missing milestone'),
|
|
2831
|
+
noProject: z.boolean().optional().describe('Filter by missing project'),
|
|
2832
|
+
project: z.string().optional().describe('Filter by project board'),
|
|
2833
|
+
reactions: z.number().optional().describe('Filter by reactions count'),
|
|
2834
|
+
state: z
|
|
2835
|
+
.enum(['open', 'closed'])
|
|
2836
|
+
.optional()
|
|
2837
|
+
.describe('Filter by issue state'),
|
|
2838
|
+
teamMentions: z.string().optional().describe('Filter by team mentions'),
|
|
2839
|
+
updated: z.string().optional().describe('Filter by last updated date'),
|
|
2840
|
+
visibility: z
|
|
2841
|
+
.enum(['public', 'private', 'internal'])
|
|
2842
|
+
.optional()
|
|
2843
|
+
.describe('Filter by repository visibility'),
|
|
2844
|
+
sort: z
|
|
2845
|
+
.enum([
|
|
2846
|
+
'comments',
|
|
2847
|
+
'created',
|
|
2848
|
+
'interactions',
|
|
2849
|
+
'reactions',
|
|
2850
|
+
'reactions-+1',
|
|
2851
|
+
'reactions--1',
|
|
2852
|
+
'reactions-heart',
|
|
2853
|
+
'reactions-smile',
|
|
2854
|
+
'reactions-tada',
|
|
2855
|
+
'reactions-thinking_face',
|
|
2856
|
+
'updated',
|
|
2857
|
+
'best-match',
|
|
2858
|
+
])
|
|
2859
|
+
.optional()
|
|
2860
|
+
.describe('Sort criteria'),
|
|
2861
|
+
order: z
|
|
2862
|
+
.enum(['asc', 'desc'])
|
|
2863
|
+
.optional()
|
|
2864
|
+
.default('desc')
|
|
2865
|
+
.describe('Order (default: desc)'),
|
|
2866
|
+
limit: z
|
|
2867
|
+
.number()
|
|
2868
|
+
.int()
|
|
2869
|
+
.min(1)
|
|
2870
|
+
.max(50)
|
|
2871
|
+
.optional()
|
|
2872
|
+
.default(25)
|
|
2873
|
+
.describe('Maximum results (default: 25, max: 50)'),
|
|
2874
|
+
},
|
|
2875
|
+
annotations: {
|
|
2876
|
+
title: 'GitHub Issues Search',
|
|
2877
|
+
readOnlyHint: true,
|
|
2878
|
+
destructiveHint: false,
|
|
2879
|
+
idempotentHint: true,
|
|
2880
|
+
openWorldHint: true,
|
|
2881
|
+
},
|
|
2048
2882
|
}, async (args) => {
|
|
2049
2883
|
if (!args.query?.trim()) {
|
|
2050
|
-
return
|
|
2884
|
+
return createResult({
|
|
2885
|
+
error: 'Search query is required and cannot be empty - provide keywords to search for issues',
|
|
2886
|
+
});
|
|
2051
2887
|
}
|
|
2052
2888
|
if (args.query.length > 256) {
|
|
2053
|
-
return
|
|
2889
|
+
return createResult({
|
|
2890
|
+
error: 'Search query is too long. Please limit to 256 characters or less - simplify your search terms',
|
|
2891
|
+
});
|
|
2054
2892
|
}
|
|
2055
2893
|
try {
|
|
2056
2894
|
return await searchGitHubIssues(args);
|
|
2057
2895
|
}
|
|
2058
2896
|
catch (error) {
|
|
2059
|
-
|
|
2897
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
2898
|
+
if (errorMessage.includes('authentication')) {
|
|
2899
|
+
return createResult({
|
|
2900
|
+
error: 'GitHub authentication required - run api_status_check tool',
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
if (errorMessage.includes('rate limit')) {
|
|
2904
|
+
return createResult({
|
|
2905
|
+
error: 'GitHub rate limit exceeded - wait or use specific filters',
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
// Generic fallback
|
|
2909
|
+
return createResult({
|
|
2910
|
+
error: 'GitHub issue search failed - check authentication or simplify query',
|
|
2911
|
+
});
|
|
2060
2912
|
}
|
|
2061
2913
|
});
|
|
2062
2914
|
}
|
|
@@ -2078,34 +2930,55 @@ async function searchGitHubIssues(params) {
|
|
|
2078
2930
|
author: issue.user?.login || '',
|
|
2079
2931
|
repository: issue.repository_url?.split('/').slice(-2).join('/') || 'unknown',
|
|
2080
2932
|
labels: issue.labels?.map(l => l.name) || [],
|
|
2081
|
-
created_at: issue.created_at,
|
|
2082
|
-
updated_at: issue.updated_at,
|
|
2933
|
+
created_at: toDDMMYYYY(issue.created_at),
|
|
2934
|
+
updated_at: toDDMMYYYY(issue.updated_at),
|
|
2083
2935
|
url: issue.html_url,
|
|
2084
2936
|
comments: issue.comments,
|
|
2085
2937
|
reactions: issue.reactions?.total_count || 0,
|
|
2086
2938
|
}));
|
|
2087
2939
|
const searchResult = {
|
|
2088
|
-
searchType: 'issues',
|
|
2089
|
-
query: params.query || '',
|
|
2090
2940
|
results: cleanIssues,
|
|
2941
|
+
total_count: apiResponse.total_count || cleanIssues.length,
|
|
2091
2942
|
metadata: {
|
|
2092
|
-
total_count: apiResponse.total_count || 0,
|
|
2093
2943
|
incomplete_results: apiResponse.incomplete_results || false,
|
|
2094
2944
|
},
|
|
2095
2945
|
};
|
|
2096
|
-
return
|
|
2946
|
+
return createResult({ data: searchResult });
|
|
2097
2947
|
});
|
|
2098
2948
|
}
|
|
2099
2949
|
function buildGitHubIssuesAPICommand(params) {
|
|
2100
|
-
const queryParts = [
|
|
2101
|
-
//
|
|
2950
|
+
const queryParts = [];
|
|
2951
|
+
// Start with the base query, but filter out qualifiers that will be added separately
|
|
2952
|
+
const baseQuery = params.query?.trim() || '';
|
|
2953
|
+
// Extract and remove qualifiers from the main query to avoid conflicts
|
|
2954
|
+
const qualifierPatterns = [
|
|
2955
|
+
/\bis:(open|closed)\b/gi,
|
|
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();
|
|
2969
|
+
});
|
|
2970
|
+
// Add the cleaned query if it has content
|
|
2971
|
+
if (cleanQuery) {
|
|
2972
|
+
queryParts.push(cleanQuery);
|
|
2973
|
+
}
|
|
2974
|
+
// Repository/organization qualifiers - prioritize function params over query
|
|
2102
2975
|
if (params.owner && params.repo) {
|
|
2103
2976
|
queryParts.push(`repo:${params.owner}/${params.repo}`);
|
|
2104
2977
|
}
|
|
2105
2978
|
else if (params.owner) {
|
|
2106
2979
|
queryParts.push(`org:${params.owner}`);
|
|
2107
2980
|
}
|
|
2108
|
-
// Build search qualifiers from
|
|
2981
|
+
// Build search qualifiers from function parameters (these take precedence)
|
|
2109
2982
|
const qualifiers = {
|
|
2110
2983
|
author: params.author,
|
|
2111
2984
|
assignee: params.assignee,
|
|
@@ -2122,9 +2995,10 @@ function buildGitHubIssuesAPICommand(params) {
|
|
|
2122
2995
|
if (value)
|
|
2123
2996
|
queryParts.push(`${key}:${value}`);
|
|
2124
2997
|
});
|
|
2125
|
-
// Special qualifiers
|
|
2126
|
-
if (params.labels)
|
|
2998
|
+
// Special qualifiers - handle labels carefully
|
|
2999
|
+
if (params.labels) {
|
|
2127
3000
|
queryParts.push(`label:"${params.labels}"`);
|
|
3001
|
+
}
|
|
2128
3002
|
if (params.milestone)
|
|
2129
3003
|
queryParts.push(`milestone:"${params.milestone}"`);
|
|
2130
3004
|
if (params.noAssignee)
|
|
@@ -2139,6 +3013,26 @@ function buildGitHubIssuesAPICommand(params) {
|
|
|
2139
3013
|
queryParts.push('is:locked');
|
|
2140
3014
|
if (params.visibility)
|
|
2141
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()}`);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
if (baseQuery.includes('label:') && !params.labels) {
|
|
3024
|
+
const labelMatch = baseQuery.match(/\blabel:("[^"]*"|[^\s]+)/i);
|
|
3025
|
+
if (labelMatch) {
|
|
3026
|
+
const labelValue = labelMatch[1].replace(/"/g, '');
|
|
3027
|
+
queryParts.push(`label:"${labelValue}"`);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (baseQuery.includes('created:') && !params.created) {
|
|
3031
|
+
const createdMatch = baseQuery.match(/\bcreated:([^\s]+)/i);
|
|
3032
|
+
if (createdMatch) {
|
|
3033
|
+
queryParts.push(`created:${createdMatch[1]}`);
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
2142
3036
|
const query = queryParts.filter(Boolean).join(' ');
|
|
2143
3037
|
const limit = Math.min(params.limit || 25, 100);
|
|
2144
3038
|
let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
|
|
@@ -2150,95 +3044,160 @@ function buildGitHubIssuesAPICommand(params) {
|
|
|
2150
3044
|
}
|
|
2151
3045
|
|
|
2152
3046
|
const TOOL_NAME = 'npm_view_package';
|
|
2153
|
-
const DESCRIPTION = `
|
|
2154
|
-
name from search results or user input. This tool is effieicnt since it gets important package metadata
|
|
2155
|
-
on a package fast to optimize tools calls. get package git repository path easily without need to search it (using repo/code search tools),
|
|
2156
|
-
exported files of a package are there, along with dependencies and version history.`;
|
|
3047
|
+
const DESCRIPTION = `Get comprehensive NPM package metadata efficiently. Returns repository URL, exports, dependencies, and version history. Essential for finding package source code and understanding project structure.`;
|
|
2157
3048
|
function registerNpmViewPackageTool(server) {
|
|
2158
|
-
server.
|
|
2159
|
-
packageName: z
|
|
2160
|
-
.string()
|
|
2161
|
-
.min(1, 'Package name is required')
|
|
2162
|
-
.describe('NPM package name to analyze. Returns complete package context including exports (critical for GitHub file discovery), repository URL, dependencies, and version history.'),
|
|
2163
|
-
}, {
|
|
2164
|
-
title: TOOL_NAME,
|
|
3049
|
+
server.registerTool(TOOL_NAME, {
|
|
2165
3050
|
description: DESCRIPTION,
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
3051
|
+
inputSchema: {
|
|
3052
|
+
packageName: z
|
|
3053
|
+
.string()
|
|
3054
|
+
.min(1)
|
|
3055
|
+
.describe('NPM package name to analyze. Returns complete package context including exports (critical for GitHub file discovery), repository URL, dependencies, and version history.'),
|
|
3056
|
+
},
|
|
3057
|
+
annotations: {
|
|
3058
|
+
title: 'NPM Package Metadata',
|
|
3059
|
+
readOnlyHint: true,
|
|
3060
|
+
destructiveHint: false,
|
|
3061
|
+
idempotentHint: true,
|
|
3062
|
+
openWorldHint: true,
|
|
3063
|
+
},
|
|
2170
3064
|
}, async (args) => {
|
|
2171
3065
|
try {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
// Basic package name validation
|
|
2176
|
-
if (!/^[a-z0-9@._/-]+$/.test(args.packageName)) {
|
|
2177
|
-
return createResult('Invalid package name format - use standard NPM naming (e.g., "package-name" or "@scope/package")', true);
|
|
3066
|
+
const result = await viewNpmPackage(args.packageName);
|
|
3067
|
+
if (result.isError) {
|
|
3068
|
+
return result;
|
|
2178
3069
|
}
|
|
2179
|
-
const
|
|
2180
|
-
|
|
3070
|
+
const execResult = JSON.parse(result.content[0].text);
|
|
3071
|
+
const packageData = JSON.parse(execResult.result);
|
|
3072
|
+
// Transform to optimized format
|
|
3073
|
+
const optimizedResult = transformToOptimizedFormat(packageData);
|
|
3074
|
+
return createResult({ data: optimizedResult });
|
|
2181
3075
|
}
|
|
2182
3076
|
catch (error) {
|
|
2183
|
-
|
|
3077
|
+
const errorMessage = error.message || '';
|
|
3078
|
+
if (errorMessage.includes('not found')) {
|
|
3079
|
+
return createResult({
|
|
3080
|
+
error: 'Package not found - verify package name spelling',
|
|
3081
|
+
cli_command: `npm view ${args.packageName} --json`,
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
if (errorMessage.includes('network')) {
|
|
3085
|
+
return createResult({
|
|
3086
|
+
error: 'Network error - check internet connection',
|
|
3087
|
+
cli_command: `npm view ${args.packageName} --json`,
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
return createResult({
|
|
3091
|
+
error: 'NPM package lookup failed',
|
|
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
|
+
],
|
|
3098
|
+
});
|
|
2184
3099
|
}
|
|
2185
3100
|
});
|
|
2186
3101
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
3102
|
+
/**
|
|
3103
|
+
* Transform NPM CLI response to optimized format
|
|
3104
|
+
*/
|
|
3105
|
+
function transformToOptimizedFormat(packageData) {
|
|
3106
|
+
// Extract repository URL and simplify
|
|
3107
|
+
const repoUrl = packageData.repository?.url || packageData.repositoryGitUrl || '';
|
|
3108
|
+
const repository = repoUrl ? simplifyRepoUrl(repoUrl) : '';
|
|
3109
|
+
// Simplify exports to essential entry points only
|
|
3110
|
+
const exports = packageData.exports
|
|
3111
|
+
? simplifyExports(packageData.exports)
|
|
3112
|
+
: undefined;
|
|
3113
|
+
// Get version timestamps from time object and limit to last 5
|
|
3114
|
+
const timeData = packageData.time || {};
|
|
3115
|
+
const versionList = packageData.versions || [];
|
|
3116
|
+
const recentVersions = versionList.slice(-5).map((version) => ({
|
|
3117
|
+
version,
|
|
3118
|
+
date: timeData[version] ? toDDMMYYYY(timeData[version]) : 'Unknown',
|
|
3119
|
+
}));
|
|
3120
|
+
const result = {
|
|
3121
|
+
name: packageData.name,
|
|
3122
|
+
version: packageData.version,
|
|
3123
|
+
description: packageData.description || '',
|
|
3124
|
+
license: packageData.license || 'Unknown',
|
|
3125
|
+
repository,
|
|
3126
|
+
size: humanizeBytes(packageData.dist?.unpackedSize || 0),
|
|
3127
|
+
created: timeData.created ? toDDMMYYYY(timeData.created) : 'Unknown',
|
|
3128
|
+
updated: timeData.modified ? toDDMMYYYY(timeData.modified) : 'Unknown',
|
|
3129
|
+
versions: recentVersions,
|
|
2198
3130
|
stats: {
|
|
2199
|
-
|
|
2200
|
-
|
|
3131
|
+
total_versions: versionList.length,
|
|
3132
|
+
weekly_downloads: packageData.weeklyDownloads,
|
|
2201
3133
|
},
|
|
2202
3134
|
};
|
|
3135
|
+
// Add exports only if they exist and are useful
|
|
3136
|
+
if (exports && Object.keys(exports).length > 0) {
|
|
3137
|
+
result.exports = exports;
|
|
3138
|
+
}
|
|
3139
|
+
return result;
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Simplify exports object to essential entry points
|
|
3143
|
+
*/
|
|
3144
|
+
function simplifyExports(exports) {
|
|
3145
|
+
if (typeof exports === 'string') {
|
|
3146
|
+
return { main: exports };
|
|
3147
|
+
}
|
|
3148
|
+
if (typeof exports === 'object') {
|
|
3149
|
+
const simplified = {};
|
|
3150
|
+
// Extract main entry point
|
|
3151
|
+
if (exports['.']) {
|
|
3152
|
+
const mainExport = exports['.'];
|
|
3153
|
+
if (typeof mainExport === 'string') {
|
|
3154
|
+
simplified.main = mainExport;
|
|
3155
|
+
}
|
|
3156
|
+
else if (mainExport.default) {
|
|
3157
|
+
simplified.main = mainExport.default;
|
|
3158
|
+
}
|
|
3159
|
+
else if (mainExport.import) {
|
|
3160
|
+
simplified.main = mainExport.import;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
// Extract types if available
|
|
3164
|
+
if (exports['./types'] || exports['.']?.types) {
|
|
3165
|
+
simplified.types = exports['./types'] || exports['.'].types;
|
|
3166
|
+
}
|
|
3167
|
+
// Add a few other important exports (max 3 total)
|
|
3168
|
+
let count = 0;
|
|
3169
|
+
for (const [key, value] of Object.entries(exports)) {
|
|
3170
|
+
if (count >= 3 || key === '.' || key === './types')
|
|
3171
|
+
continue;
|
|
3172
|
+
if (key.includes('package.json') || key.includes('node_modules'))
|
|
3173
|
+
continue;
|
|
3174
|
+
simplified[key] =
|
|
3175
|
+
typeof value === 'object' ? value.default || value : value;
|
|
3176
|
+
count++;
|
|
3177
|
+
}
|
|
3178
|
+
return simplified;
|
|
3179
|
+
}
|
|
3180
|
+
return { main: 'index.js' };
|
|
2203
3181
|
}
|
|
2204
|
-
async function
|
|
2205
|
-
const cacheKey = generateCacheKey('npm-view
|
|
3182
|
+
async function viewNpmPackage(packageName) {
|
|
3183
|
+
const cacheKey = generateCacheKey('npm-view', { packageName });
|
|
2206
3184
|
return withCache(cacheKey, async () => {
|
|
2207
3185
|
try {
|
|
2208
3186
|
const result = await executeNpmCommand('view', [packageName, '--json'], {
|
|
2209
|
-
cache:
|
|
3187
|
+
cache: false,
|
|
2210
3188
|
});
|
|
2211
|
-
|
|
2212
|
-
return result;
|
|
2213
|
-
}
|
|
2214
|
-
// Parse the result from the executed command
|
|
2215
|
-
const commandOutput = JSON.parse(result.content[0].text);
|
|
2216
|
-
const npmData = JSON.parse(commandOutput.result);
|
|
2217
|
-
// Process versions
|
|
2218
|
-
const versionData = processVersions(npmData.time);
|
|
2219
|
-
// Extract registry URL from tarball
|
|
2220
|
-
const registryUrl = npmData.dist?.tarball?.match(/^(https?:\/\/[^/]+)/)?.[1] || '';
|
|
2221
|
-
// Build result
|
|
2222
|
-
const viewResult = {
|
|
2223
|
-
name: npmData.name,
|
|
2224
|
-
latest: npmData['dist-tags']?.latest || '',
|
|
2225
|
-
license: npmData.license || '',
|
|
2226
|
-
timeCreated: npmData.time?.created || '',
|
|
2227
|
-
timeModified: npmData.time?.modified || '',
|
|
2228
|
-
repositoryGitUrl: npmData.repository?.url || '',
|
|
2229
|
-
registryUrl,
|
|
2230
|
-
description: npmData.description || '',
|
|
2231
|
-
size: npmData.dist?.unpackedSize || 0,
|
|
2232
|
-
dependencies: npmData.dependencies || {},
|
|
2233
|
-
devDependencies: npmData.devDependencies || {},
|
|
2234
|
-
exports: npmData.exports || {},
|
|
2235
|
-
versions: versionData.recent,
|
|
2236
|
-
versionStats: versionData.stats,
|
|
2237
|
-
};
|
|
2238
|
-
return createSuccessResult$1(viewResult);
|
|
3189
|
+
return result;
|
|
2239
3190
|
}
|
|
2240
3191
|
catch (error) {
|
|
2241
|
-
|
|
3192
|
+
const errorMessage = error.message || '';
|
|
3193
|
+
if (errorMessage.includes('404')) {
|
|
3194
|
+
return createResult({
|
|
3195
|
+
error: 'Package not found on NPM registry',
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
return createResult({
|
|
3199
|
+
error: 'NPM command execution failed',
|
|
3200
|
+
});
|
|
2242
3201
|
}
|
|
2243
3202
|
});
|
|
2244
3203
|
}
|
|
@@ -2258,7 +3217,7 @@ function registerAllTools(server) {
|
|
|
2258
3217
|
fn: registerFetchGitHubFileContentTool,
|
|
2259
3218
|
},
|
|
2260
3219
|
{ name: 'SearchGitHubRepos', fn: registerSearchGitHubReposTool },
|
|
2261
|
-
{ name: 'SearchGitHubCommits', fn:
|
|
3220
|
+
{ name: 'SearchGitHubCommits', fn: registerGitHubSearchCommitsTool },
|
|
2262
3221
|
{
|
|
2263
3222
|
name: 'SearchGitHubPullRequests',
|
|
2264
3223
|
fn: registerSearchGitHubPullRequestsTool,
|