searchFiles
Search for notes, documents, and files within your Obsidian vault hosted on GitHub. Find specific content using GitHub search syntax, targeting filenames, paths, or file contents for precise results.
Instructions
Search for notes, documents, and files within your Obsidian vault on GitHub (my-organization/obsidian-vault). Find specific knowledge base content using GitHub's powerful search syntax. Supports searching in filenames, paths, and content.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| page | No | Page number to retrieve (0-indexed) | |
| perPage | No | Number of results per page | |
| query | Yes | Search query - can be a simple term or use GitHub search qualifiers | |
| searchIn | No | Where to search: 'filename' (exact filename match), 'path' (anywhere in file path), 'content' (file contents), or 'all' (comprehensive search) | all |
Implementation Reference
- src/github/client.ts:474-594 (handler)Main handler function that executes the searchFiles tool. Constructs GitHub code search queries tailored to searchIn (filename, path, content, all), fetches results via Octokit, formats output with match indicators, and provides detailed diagnostics/formatted response if no results.async ({ query, searchIn = "all", page = 0, perPage = 100 }) => { // Empty query is allowed - useful for listing files const repoQualifier = `repo:${this.config.owner}/${this.config.repo}`; // Build search query based on searchIn parameter let qualifiedQuery: string; if (searchIn === "filename") { // Search for exact filename matches qualifiedQuery = `filename:${ query.includes(" ") ? `"${query}"` : query } ${repoQualifier}`; } else if (searchIn === "path") { // Search anywhere in the file path. The `in:path` qualifier searches for the // query term within the file path. qualifiedQuery = `${query} in:path ${repoQualifier}`; } else if (searchIn === "content") { // Search only in file contents. This is the default behavior without qualifiers. qualifiedQuery = `${query} ${repoQualifier}`; } else { // "all" - comprehensive search. The GitHub search API (legacy) does not // support OR operators. The best we can do in a single query is to search // in file content and file path. The `in:file,path` qualifier does this. // This will match the query term if it appears in the content or anywhere // in the full path of a file, which includes the filename. qualifiedQuery = `${query} in:file,path ${repoQualifier}`; } let searchResults: { items: Array<{ name: string; path: string }>; total_count: number; }; try { searchResults = await this.handleRequest(async () => { return this.octokit.search.code({ q: qualifiedQuery, page, per_page: perPage, }); }); } catch (error) { // Enhanced error messages with specific GitHub search issues const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("validation failed")) { throw new Error( `GitHub search query invalid: "${qualifiedQuery}". Try simpler terms or check syntax.` ); } if (errorMessage.includes("rate limit")) { throw new Error( "GitHub code search rate limit exceeded. Wait a moment and try again." ); } if ( errorMessage.includes("Forbidden") || errorMessage.includes("401") ) { throw new Error( "GitHub API access denied. Check that your token has 'repo' scope for private repositories." ); } throw error; // Re-throw other errors } // Enhanced formatting with file sizes and relevance indicators const formattedResults = searchResults.items .map((item) => { const fileName = item.name; const filePath = item.path; // const score = item.score || 0; // Could be used for relevance ranking in future // Determine why this file matched let matchReason = ""; if (searchIn === "filename") { matchReason = "π filename match"; } else if (searchIn === "path") { matchReason = "π path match"; } else if (searchIn === "content") { matchReason = "π content match"; } else { // searchIn is 'all', so we deduce the reason if (fileName.toLowerCase().includes(query.toLowerCase())) { matchReason = "π filename match"; } else if (filePath.toLowerCase().includes(query.toLowerCase())) { matchReason = "π path match"; } else { matchReason = "π content match"; } } return `- **${fileName}** (${filePath}) ${matchReason}`; }) .join("\n"); let resultText = `Found ${searchResults.total_count} files`; if (searchIn !== "all") { resultText += ` searching in ${searchIn}`; } resultText += `:\n\n${formattedResults}`; // If no results, run diagnostics and provide enhanced response if (searchResults.total_count === 0) { const diagnostics = await this.runSearchDiagnostics(query); return this.formatNoResultsResponse( searchResults, diagnostics, query, searchIn ); } return { content: [ { type: "text" as const, text: resultText, }, ], }; }
- src/github/client.ts:444-467 (schema)Zod input schema defining parameters for the searchFiles tool: query (required string), searchIn (enum: filename|path|content|all, default all), page (number, default 0), perPage (number, default 100).{ query: z .string() .describe( "Search query - can be a simple term or use GitHub search qualifiers" ), searchIn: z .enum(["filename", "path", "content", "all"]) .optional() .default("all") .describe( "Where to search: 'filename' (exact filename match), 'path' (anywhere in file path), 'content' (file contents), or 'all' (comprehensive search)" ), page: z .number() .optional() .default(0) .describe("Page number to retrieve (0-indexed)"), perPage: z .number() .optional() .default(100) .describe("Number of results per page"), },
- src/github/client.ts:441-595 (registration)Registration of the searchFiles tool via server.tool() within the registerGithubTools method of GithubClient.server.tool( "searchFiles", `Search for notes, documents, and files within your Obsidian vault on GitHub (${this.config.owner}/${this.config.repo}). Find specific knowledge base content using GitHub's powerful search syntax. Supports searching in filenames, paths, and content.`, { query: z .string() .describe( "Search query - can be a simple term or use GitHub search qualifiers" ), searchIn: z .enum(["filename", "path", "content", "all"]) .optional() .default("all") .describe( "Where to search: 'filename' (exact filename match), 'path' (anywhere in file path), 'content' (file contents), or 'all' (comprehensive search)" ), page: z .number() .optional() .default(0) .describe("Page number to retrieve (0-indexed)"), perPage: z .number() .optional() .default(100) .describe("Number of results per page"), }, { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true, }, async ({ query, searchIn = "all", page = 0, perPage = 100 }) => { // Empty query is allowed - useful for listing files const repoQualifier = `repo:${this.config.owner}/${this.config.repo}`; // Build search query based on searchIn parameter let qualifiedQuery: string; if (searchIn === "filename") { // Search for exact filename matches qualifiedQuery = `filename:${ query.includes(" ") ? `"${query}"` : query } ${repoQualifier}`; } else if (searchIn === "path") { // Search anywhere in the file path. The `in:path` qualifier searches for the // query term within the file path. qualifiedQuery = `${query} in:path ${repoQualifier}`; } else if (searchIn === "content") { // Search only in file contents. This is the default behavior without qualifiers. qualifiedQuery = `${query} ${repoQualifier}`; } else { // "all" - comprehensive search. The GitHub search API (legacy) does not // support OR operators. The best we can do in a single query is to search // in file content and file path. The `in:file,path` qualifier does this. // This will match the query term if it appears in the content or anywhere // in the full path of a file, which includes the filename. qualifiedQuery = `${query} in:file,path ${repoQualifier}`; } let searchResults: { items: Array<{ name: string; path: string }>; total_count: number; }; try { searchResults = await this.handleRequest(async () => { return this.octokit.search.code({ q: qualifiedQuery, page, per_page: perPage, }); }); } catch (error) { // Enhanced error messages with specific GitHub search issues const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("validation failed")) { throw new Error( `GitHub search query invalid: "${qualifiedQuery}". Try simpler terms or check syntax.` ); } if (errorMessage.includes("rate limit")) { throw new Error( "GitHub code search rate limit exceeded. Wait a moment and try again." ); } if ( errorMessage.includes("Forbidden") || errorMessage.includes("401") ) { throw new Error( "GitHub API access denied. Check that your token has 'repo' scope for private repositories." ); } throw error; // Re-throw other errors } // Enhanced formatting with file sizes and relevance indicators const formattedResults = searchResults.items .map((item) => { const fileName = item.name; const filePath = item.path; // const score = item.score || 0; // Could be used for relevance ranking in future // Determine why this file matched let matchReason = ""; if (searchIn === "filename") { matchReason = "π filename match"; } else if (searchIn === "path") { matchReason = "π path match"; } else if (searchIn === "content") { matchReason = "π content match"; } else { // searchIn is 'all', so we deduce the reason if (fileName.toLowerCase().includes(query.toLowerCase())) { matchReason = "π filename match"; } else if (filePath.toLowerCase().includes(query.toLowerCase())) { matchReason = "π path match"; } else { matchReason = "π content match"; } } return `- **${fileName}** (${filePath}) ${matchReason}`; }) .join("\n"); let resultText = `Found ${searchResults.total_count} files`; if (searchIn !== "all") { resultText += ` searching in ${searchIn}`; } resultText += `:\n\n${formattedResults}`; // If no results, run diagnostics and provide enhanced response if (searchResults.total_count === 0) { const diagnostics = await this.runSearchDiagnostics(query); return this.formatNoResultsResponse( searchResults, diagnostics, query, searchIn ); } return { content: [ { type: "text" as const, text: resultText, }, ], }; } );
- src/github/client.ts:42-97 (helper)Helper method called by handler when no results; performs repository diagnostics including size check, search test, indexing status.private async runSearchDiagnostics(_originalQuery: string): Promise<{ repoSize?: number; repoSizeGB?: number; isPrivate?: boolean; defaultBranch?: string; basicSearchWorks?: boolean; filesFound?: number; repoIndexed?: boolean; isLarge?: boolean; diagnosticError?: string; }> { try { // Test 1: Repository accessibility const repoInfo = await this.handleRequest(async () => { return this.octokit.repos.get({ owner: this.config.owner, repo: this.config.repo, }); }); // Test 2: Basic search functionality with simple query let basicSearchWorks = false; let basicSearchCount = 0; try { const basicTest = await this.handleRequest(async () => { return this.octokit.search.code({ q: `repo:${this.config.owner}/${this.config.repo} extension:md`, per_page: 1, }); }); basicSearchWorks = true; basicSearchCount = basicTest.total_count; } catch (_error) { basicSearchWorks = false; } const repoSizeKB = repoInfo.size; const repoSizeGB = repoSizeKB / (1024 * 1024); const isLarge = repoSizeGB > 50; return { repoSize: repoSizeKB, repoSizeGB: repoSizeGB, isPrivate: repoInfo.private, defaultBranch: repoInfo.default_branch, basicSearchWorks, filesFound: basicSearchCount, repoIndexed: basicSearchWorks && basicSearchCount > 0, isLarge, }; } catch (error) { return { diagnosticError: error instanceof Error ? error.message : String(error), }; } }
- src/github/client.ts:100-172 (helper)Helper method formats user-friendly response with search tips and diagnostic info when search yields zero results.private formatNoResultsResponse( _searchResults: { total_count: number }, diagnostics: { diagnosticError?: string; repoIndexed?: boolean; isLarge?: boolean; repoSizeGB?: number; isPrivate?: boolean; defaultBranch?: string; filesFound?: number; }, query: string, searchIn: string ): { content: Array<{ type: "text"; text: string }> } { let resultText = `Found 0 files matching "${query}"`; if (searchIn !== "all") { resultText += ` in ${searchIn}`; } resultText += "\n\n"; if (diagnostics.diagnosticError) { resultText += `β οΈ **Search System Issue**: ${diagnostics.diagnosticError}\n\n`; } else if (!diagnostics.repoIndexed) { resultText += "β οΈ **Repository May Not Be Indexed**: GitHub might not have indexed this repository for search.\n"; resultText += "This can happen with:\n"; resultText += "- New repositories (indexing takes time)\n"; if (diagnostics.isLarge) { resultText += `- Large repositories (${diagnostics.repoSizeGB?.toFixed( 2 )} GB exceeds 50 GB limit)\n`; } if (diagnostics.isPrivate) { resultText += "- Private repositories with indexing issues\n"; } resultText += "\n**Try**:\n"; resultText += "- Search directly on GitHub.com to confirm\n"; resultText += "- Use the diagnoseSearch tool for detailed diagnostics\n\n"; } else { resultText += "π **Search Debug Info**:\n"; resultText += `- Repository: ${ diagnostics.isPrivate ? "Private" : "Public" } (${diagnostics.repoSizeGB?.toFixed(3)} GB)\n`; resultText += `- Default branch: ${diagnostics.defaultBranch} (only branch searchable)\n`; resultText += `- Files in repo: ${diagnostics.filesFound} found\n`; resultText += `- Search query used: \`${query}\`\n\n`; resultText += "**Possible reasons for no results**:\n"; resultText += "- The search term doesn't exist in the repository\n"; resultText += "- Content might be in non-default branches (not searchable)\n"; resultText += "- Files might be larger than 384 KB (not indexed)\n\n"; } // Add search tips resultText += "π‘ **Search Tips:**\n"; resultText += `- Try \`searchIn: "filename"\` to search only filenames\n`; resultText += `- Try \`searchIn: "path"\` to search file paths\n`; resultText += `- Try \`searchIn: "content"\` to search file contents\n`; resultText += `- Use quotes for exact phrases: "exact phrase"\n`; resultText += "- Use wildcards: `*.md` for markdown files\n"; resultText += "- Try simpler or partial search terms"; return { content: [ { type: "text" as const, text: resultText, }, ], }; }