Skip to main content
Glama

LLM Researcher

by Code-Hex
cli.ts11.8 kB
import readline from 'readline'; import type { SearchResult, SearchResultsPage, ExtractedContent, ContentMatch, Platform } from './types.js'; import type { LLMResearcher } from './index.js'; import { GitHubCodeSearcher } from './github-code-search.js'; export class CLIInterface { private researcher: LLMResearcher; public currentResults: SearchResult[] = []; public currentPage: SearchResultsPage | null = null; private rl: readline.Interface | null = null; constructor(researcher: LLMResearcher) { this.researcher = researcher; } private setupReadline(): void { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout }); } private closeReadline(): void { if (this.rl) { this.rl.close(); this.rl = null; } } displayResults(page: SearchResultsPage): void { console.log(`\n🔍 Search Results (Page ${page.currentPage}/${page.totalPages}) - ${this.researcher.getSearcherType()}:`); console.log(`📊 Total Results: ~${page.totalResults} | Showing: ${page.results.length} results`); page.results.forEach((result, index) => { console.log(`\n${index + 1}. ${result.title}`); console.log(` URL: ${result.url}`); if (result.snippet) { console.log(` ${result.snippet}`); } }); // Build pagination commands const commands = [`[1-${page.results.length}] select result`]; if (this.researcher.searcher.hasPreviousPage(page)) { commands.push('p) previous page'); } if (this.researcher.searcher.hasNextPage(page)) { commands.push('n) next page'); } commands.push('b) back to search', 'q) quit', 'open <n>) open in browser'); console.log(`\nCommands: ${commands.join(' | ')}`); } displayContent(content: ExtractedContent): void { console.log('\n📄 Content:'); console.log(`\n**${content.title}**`); console.log(`Source: ${content.url}`); console.log(`Extracted: ${new Date(content.extractedAt).toLocaleString()}`); console.log('\n---'); console.log(content.content); console.log('\nCommands: b) back to results | /<term>) search in text | q) quit | open) open in browser'); } private async prompt(message: string): Promise<string> { return new Promise(resolve => { this.rl!.question(message, resolve); }); } async handleSearchMode(query: string, page = 1): Promise<void> { try { console.log(`\n🔍 Searching for: "${query}" (Page ${page})`); // Convert page number to nextToken for GitHub searcher const nextToken = page > 1 ? page.toString() : undefined; const searchPage = await this.researcher.search(query, nextToken, undefined); if (searchPage.results.length === 0) { console.log('No results found.'); return; } this.currentResults = searchPage.results; this.currentPage = searchPage; this.displayResults(searchPage); await this.handleResultSelection(); } catch (error) { console.error('Search failed:', (error as Error).message); } } async handleResultSelection(): Promise<void> { this.setupReadline(); while (true) { const input = await this.prompt('\n> '); const command = input.trim().toLowerCase(); if (command === 'q' || command === 'quit') { break; } else if (command === 'b' || command === 'back') { // Go back to main search prompt break; } else if (command === 'n' || command === 'next') { if (this.currentPage && this.researcher.searcher.hasNextPage(this.currentPage)) { try { const nextPage = await this.researcher.searcher.getNextPage(this.currentPage); this.currentResults = nextPage.results; this.currentPage = nextPage; this.displayResults(nextPage); } catch (error) { console.log('Failed to load next page:', (error as Error).message); } } else { console.log('No next page available.'); } } else if (command === 'p' || command === 'prev' || command === 'previous') { if (this.currentPage && this.researcher.searcher.hasPreviousPage(this.currentPage)) { try { const prevPage = await this.researcher.searcher.getPreviousPage(this.currentPage); this.currentResults = prevPage.results; this.currentPage = prevPage; this.displayResults(prevPage); } catch (error) { console.log('Failed to load previous page:', (error as Error).message); } } else { console.log('No previous page available.'); } } else if (command.startsWith('open ')) { const index = parseInt(command.split(' ')[1] || '') - 1; if (index >= 0 && index < this.currentResults.length) { await this.openInBrowser(this.currentResults[index]!.url); } else { console.log('Invalid result number.'); } } else { const index = parseInt(command) - 1; if (index >= 0 && index < this.currentResults.length) { await this.handleContentView(this.currentResults[index]!); } else { console.log('Invalid selection. Please enter a number 1-' + this.currentResults.length); } } } this.closeReadline(); } private async handleContentView(result: SearchResult): Promise<void> { try { console.log(`\n📥 Extracting content from: ${result.title}`); const content = await this.researcher.extractFromUrl(result.url, false); this.displayContent(content); await this.handleContentCommands(content); } catch (error) { console.error('Content extraction failed:', (error as Error).message); console.log('Press Enter to return to results...'); await this.prompt(''); } } async handleContentCommands(content: ExtractedContent): Promise<string | void> { while (true) { const input = await this.prompt('\n> '); const command = input.trim(); if (command.toLowerCase() === 'q' || command.toLowerCase() === 'quit') { return 'quit'; } else if (command.toLowerCase() === 'b' || command.toLowerCase() === 'back') { if (this.currentPage) { this.displayResults(this.currentPage); } return; } else if (command.toLowerCase() === 'open') { await this.openInBrowser(content.url); } else if (command.startsWith('/')) { const searchTerm = command.slice(1); this.searchInContent(content, searchTerm); } else { console.log('Unknown command. Available: b) back | /<term>) search | open) open in browser | q) quit'); } } } private searchInContent(content: ExtractedContent, searchTerm: string): void { if (!searchTerm) { console.log('Please provide a search term after /'); return; } const lines = content.content.split('\n'); const matches: ContentMatch[] = []; lines.forEach((line, index) => { if (line.toLowerCase().includes(searchTerm.toLowerCase())) { matches.push({ lineNumber: index + 1, content: line.trim() }); } }); if (matches.length === 0) { console.log(`No matches found for "${searchTerm}"`); } else { console.log(`\n🔍 Found ${matches.length} matches for "${searchTerm}":`) matches.forEach(match => { console.log(`Line ${match.lineNumber}: ${match.content}`); }); } } private async openInBrowser(url: string): Promise<void> { const { exec } = await import('child_process'); const platform = process.platform as Platform; let command: string; if (platform === 'darwin') { command = `open "${url}"`; } else if (platform === 'win32') { command = `start "${url}"`; } else { command = `xdg-open "${url}"`; } exec(command, (error) => { if (error) { console.log(`Failed to open browser: ${error.message}`); console.log(`Please open manually: ${url}`); } else { console.log('Opened in browser'); } }); } async interactive(): Promise<void> { console.log('🔬 LLM Researcher - Interactive Mode'); console.log(`Current search engine: ${this.researcher.getSearcherType()}`); console.log('Enter search queries or commands. Type "help" for help, "quit" to exit.\n'); this.setupReadline(); while (true) { const input = await this.prompt('Search> '); const command = input.trim(); if (command.toLowerCase() === 'quit' || command.toLowerCase() === 'q') { break; } else if (command.toLowerCase() === 'help') { this.showHelp(); } else if (command.toLowerCase() === 'engine') { console.log(`Current search engine: ${this.researcher.getSearcherType()}`); } else if (command.toLowerCase() === 'ddg') { this.researcher.useDuckDuckGoSearch(); console.log(`Switched to DuckDuckGo search`); } else if (command.toLowerCase() === 'github') { this.researcher.useGitHubCodeSearch(); console.log(`Switched to GitHub Code Search`); } else if (command.toLowerCase() === 'examples') { this.showGitHubExamples(); } else if (command) { await this.handleSearchMode(command); } } this.closeReadline(); } private showHelp(): void { console.log(` 🔬 LLM Researcher Help Search Commands: <query> Search using current search engine (${this.researcher.getSearcherType()}) help Show this help quit, q Exit the program Search Engine Commands: github Switch to GitHub Code Search ddg Switch to DuckDuckGo search engine Show current search engine examples Show GitHub Code Search query examples Result Navigation: 1-N Select a result by number (N = number of results shown) b, back Return to search results open <n> Open result #n in external browser q, quit Exit the program Content Navigation: b, back Return to search results /<term> Search for term within the content open Open current page in external browser q, quit Exit the program Direct URL Mode: llmresearcher -u <url> Extract content directly from URL llmresearcher -g <query> Search GitHub code llmresearcher -g --language js <query> Search JavaScript code llmresearcher -g --repo owner/repo <query> Search in specific repository llmresearcher -v <query> Enable verbose logging `); } private showGitHubExamples(): void { console.log(` 🔍 GitHub Code Search Query Examples: Basic Usage: addClass language:js repo:jquery/jquery useState in:file extension:tsx function language:python user:octocat console.log extension:js org:facebook Advanced Queries: import filename:package.json class size:>1000 language:java TODO in:file path:src/ async/await language:javascript created:>2020-01-01 Query Syntax: language:LANG Filter by programming language extension:EXT Filter by file extension repo:OWNER/NAME Search in specific repository user:USERNAME Search in user's repositories org:ORGNAME Search in organization's repositories filename:NAME Search in files with specific name path:PATH Search in specific path size:>1000 Filter by file size in:file Search file contents in:path Search file paths Examples you can try right now: > useState language:typescript > import React extension:tsx > console.log repo:facebook/react `); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Code-Hex/light-research-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server