diff --git a/package-lock.json b/package-lock.json index 3dcfe8e..cfd8b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "license": "ISC", "dependencies": { "@openrouter/sdk": "^0.11.2", + "chalk": "^5.6.2", + "dotenv": "^17.2.3", "exa-js": "^2.11.0", "express": "^5.2.1" } @@ -99,6 +101,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -175,9 +189,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", + "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -273,6 +287,18 @@ "zod-to-json-schema": "^3.20.0" } }, + "node_modules/exa-js/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", diff --git a/package.json b/package.json index 6f25a6d..d05db90 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "description": "", "dependencies": { "@openrouter/sdk": "^0.11.2", + "chalk": "^5.6.2", "dotenv": "^17.2.3", "exa-js": "^2.11.0", "express": "^5.2.1" diff --git a/searchCLI.js b/searchCLI.js index 979d19d..058936f 100644 --- a/searchCLI.js +++ b/searchCLI.js @@ -2,6 +2,7 @@ import dotenv from 'dotenv'; import { createClients } from './src/clients.js'; import { getConfig, validateConfig } from './src/config/env.js'; import { createSearchService } from './src/services/searchService.js'; +import { renderHTML } from './src/utils/htmlConsoleRenderer.js'; // Load environment variables from .env file dotenv.config(); @@ -49,13 +50,11 @@ async function runCLI() { const result = await searchService.search(question); - - console.log(result); console.log(''); console.log('═══════════════════════════════════════════════════════════'); console.log('FINAL ANSWER:'); console.log('═══════════════════════════════════════════════════════════'); - console.log(result.fullAnswerHTMLSnippet || result.answer || 'No answer generated'); + console.log(renderHTML(result.fullAnswerHTMLSnippet) || 'No answer generated'); console.log(''); if (result.mostRelevantSources && result.mostRelevantSources.length > 0) { diff --git a/src/services/openRouterService.js b/src/services/openRouterService.js index 193bcee..35c90d8 100644 --- a/src/services/openRouterService.js +++ b/src/services/openRouterService.js @@ -1,7 +1,9 @@ import { logOpenRouterCall } from './openRouterLogger.js'; function parseResponse(response) { - return { cost: response.usage.cost, data: JSON.parse(response.choices[0].message.content) }; + if(!response?.usage?.cost) console.log(response); + + return { cost: response?.usage?.cost, data: JSON.parse(response.choices[0].message.content) }; } export async function summarizeSources({ openrouter, text, question }) { @@ -71,12 +73,12 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) { `; const params = { - model: 'openai/gpt-5.4-mini', + model: 'openai/gpt-oss-120b:nitro', messages: [ { role: 'system', content: prompt }, { role: 'user', content: text }, ], - reasoning: { effort: 'none' }, + reasoning: { effort: 'low' }, response_format: { type: 'json_schema', json_schema: { diff --git a/src/services/searchService.js b/src/services/searchService.js index cd38d83..1662db3 100644 --- a/src/services/searchService.js +++ b/src/services/searchService.js @@ -48,7 +48,7 @@ async function fetchDetailedContents({ exa, question, sources, broadcast }) { ); const content = await exa.getContents([source.url], EXA_CONTENT_OPTIONS(question)); - return { url: source.url, content }; + return { url: source.url, content, cost: content.costDollars.total }; } catch (error) { broadcast(`⚠️ Could not fetch content for ${source.url}: ${error.message}`, 'warning'); return { url: source.url, content: null }; @@ -82,7 +82,8 @@ export function createSearchService({ exa, openrouter, broadcast }) { broadcast(`✅ Found ${result.results.length} results`, 'success', { count: result.results.length, }); - let cost = result.costDollars.total; + const cost=[]; + cost.push({type:'exa_search', amount: result.costDollars.total}); const extractedContent = extractContent(result); @@ -104,7 +105,7 @@ export function createSearchService({ exa, openrouter, broadcast }) { question, }); summary = summaryResult.data; - cost += summaryResult.cost; + cost.push({type:'openrouter_summarize', amount: summaryResult.cost}); } catch (error) { throw new SearchServiceError('Failed to generate summary', 500, error); } @@ -127,6 +128,11 @@ export function createSearchService({ exa, openrouter, broadcast }) { broadcast('🔧 Enhancing summaries with detailed content...', 'info'); enrichSourcesWithDetails(summary.sources, detailedContents); + for (const detailed of detailedContents) { + if (detailed.cost) { + cost.push({type:'exa_get_content', amount: detailed.cost}); + } + } broadcast('📊 Generating final summary...', 'info'); @@ -138,7 +144,7 @@ export function createSearchService({ exa, openrouter, broadcast }) { question, }); finalSummary = finalSummaryResult.data; - cost += finalSummaryResult.cost; + cost.push({type:'openrouter_final_summary', amount: finalSummaryResult.cost}); } catch (error) { throw new SearchServiceError('Failed to generate final summary', 500, error); } @@ -151,7 +157,6 @@ export function createSearchService({ exa, openrouter, broadcast }) { answerLength: finalSummary.fullAnswerHTMLSnippet?.length || 0, sourcesCount: finalSummary.mostRelevantSources?.length || 0, }); - console.log(`Total cost of API calls: $${cost.toFixed(6)}`); return finalSummary; }, diff --git a/src/utils/htmlConsoleRenderer.js b/src/utils/htmlConsoleRenderer.js new file mode 100644 index 0000000..7ab9cc5 --- /dev/null +++ b/src/utils/htmlConsoleRenderer.js @@ -0,0 +1,80 @@ +import chalk from 'chalk'; + +/** + * Simple HTML to console renderer using chalk for styling. + * Supports: , , ,
    ,
  • , ,

    ,

    ,
    + */ + +// Color mapping for span styles +const colorMap = { + red: chalk.red, + blue: chalk.blue, + green: chalk.green, + yellow: chalk.yellow, + cyan: chalk.cyan, + magenta: chalk.magenta, + white: chalk.white, + gray: chalk.gray, + grey: chalk.gray, + black: chalk.black, +}; + +/** + * Parse HTML string and convert to styled console output + * @param {string} html - HTML string to render + * @returns {string} - Formatted string with chalk styling + */ +export function renderHTML(html) { + if (!html) return ''; + + let text = html; + + // Handle
    as a line + text = text.replace(//gi, '\n' + '─'.repeat(60) + '\n'); + + // Handle content + text = text.replace(/([\s\S]*?)<\/span>/gi, (match, color, content) => { + const colorFn = colorMap[color.toLowerCase()]; + return colorFn ? colorFn(content) : content; + }); + + // Handle content - bold + text = text.replace(/([\s\S]*?)<\/b>/gi, (match, content) => chalk.bold(content)); + + // Handle content - italic (dimmed as terminal alternative) + text = text.replace(/([\s\S]*?)<\/i>/gi, (match, content) => chalk.italic(content)); + + // Handle content - underline + text = text.replace(/([\s\S]*?)<\/u>/gi, (match, content) => chalk.underline(content)); + + // Handle
  • content
  • - list item with bullet + text = text.replace(/
  • ([\s\S]*?)<\/li>/gi, (match, content) => ` • ${content.trim()}\n`); + + // Handle
      content
    - unordered list (just preserve content, items will be formatted) + text = text.replace(/
      ([\s\S]*?)<\/ul>/gi, (match, content) => `${content}\n`); + + // Handle

      content

      - paragraph with spacing + text = text.replace(/

      ([\s\S]*?)<\/p>/gi, (match, content) => `${content.trim()}\n\n`); + + // Handle

      content
      - block with newline + text = text.replace(/
      ([\s\S]*?)<\/div>/gi, (match, content) => `${content.trim()}\n\n`); + + // Clean up any remaining HTML tags that weren't processed + text = text.replace(/<[^>]+>/g, ''); + + // Clean up excessive blank lines (more than 2 consecutive newlines) + text = text.replace(/\n{3,}/g, '\n\n'); + + // Trim extra whitespace at start and end + text = text.trim(); + + return text; +} + +/** + * Render HTML and print to console + * @param {string} html - HTML string to render + */ +export function printHTML(html) { + console.log(renderHTML(html)); +} \ No newline at end of file