diff --git a/README.md b/README.md index 991172c..ffd1321 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Official command-line interface for [Linkup](https://linkup.so) — AI-powered w ## Features - **Search the web** with three depth modes: `fast`, `standard`, and `deep`. -- **Fetch** any URL as clean markdown. +- **Fetch** any URL as clean markdown with its favicon URL. - **Research** asynchronously, and batch mixed jobs with **tasks**. - **Scriptable**: `--json` output for any command, plus stdin and file input. @@ -63,7 +63,7 @@ linkup search "query" --include-domains linkup.so --from-date 2025-01-01 #### Fetch -Extract the content of a URL as markdown. +Extract the content of a URL as markdown together with its favicon URL. - Select `--mode standard` (default) or `--mode pro` for harder-to-retrieve pages. - Use `--render-js` for JS-heavy pages. diff --git a/src/__tests__/fetch-output.test.ts b/src/__tests__/fetch-output.test.ts index 5e88d87..df75f5c 100644 --- a/src/__tests__/fetch-output.test.ts +++ b/src/__tests__/fetch-output.test.ts @@ -2,37 +2,70 @@ import { formatFetch } from '../output/fetch.js'; describe('formatFetch', () => { it('wraps the extracted markdown in blank lines', () => { - const lines = formatFetch({ markdown: '# Title\n\nSome content' }); + const lines = formatFetch({ + favicon: 'https://example.com/favicon.ico', + markdown: '# Title\n\nSome content', + }); - expect(lines).toEqual(['', '# Title\n\nSome content', '']); + expect(lines).toEqual([ + '', + '# Title\n\nSome content', + '', + 'Favicon: https://example.com/favicon.ico', + '', + ]); }); it('trims surrounding whitespace from the markdown', () => { - const lines = formatFetch({ markdown: '\n\n Hello \n\n' }); + const lines = formatFetch({ + favicon: 'https://example.com/favicon.ico', + markdown: '\n\n Hello \n\n', + }); - expect(lines).toEqual(['', 'Hello', '']); + expect(lines).toEqual(['', 'Hello', '', 'Favicon: https://example.com/favicon.ico', '']); }); it('renders raw HTML when present', () => { const lines = formatFetch({ + favicon: 'https://example.com/favicon.ico', markdown: 'Hello', rawHtml: '\nHello\n', }); - expect(lines).toEqual(['', 'Hello', '', 'Raw HTML:', 'Hello', '']); + expect(lines).toEqual([ + '', + 'Hello', + '', + 'Favicon: https://example.com/favicon.ico', + '', + 'Raw HTML:', + 'Hello', + '', + ]); }); it('renders raw content when present', () => { const lines = formatFetch({ + favicon: 'https://example.com/favicon.ico', markdown: 'Hello', rawContent: '\nOriginal page content\n', }); - expect(lines).toEqual(['', 'Hello', '', 'Raw Content:', 'Original page content', '']); + expect(lines).toEqual([ + '', + 'Hello', + '', + 'Favicon: https://example.com/favicon.ico', + '', + 'Raw Content:', + 'Original page content', + '', + ]); }); it('renders extracted images when present', () => { const lines = formatFetch({ + favicon: 'https://example.com/favicon.ico', images: [ { alt: 'Logo', url: 'https://example.com/logo.png' }, { alt: '', url: 'https://example.com/no-alt.png' }, @@ -44,6 +77,8 @@ describe('formatFetch', () => { '', 'Hello', '', + 'Favicon: https://example.com/favicon.ico', + '', 'Images:', ' • Logo', ' https://example.com/logo.png', diff --git a/src/__tests__/fetch.integration.test.ts b/src/__tests__/fetch.integration.test.ts index 41a1389..fa7f957 100644 --- a/src/__tests__/fetch.integration.test.ts +++ b/src/__tests__/fetch.integration.test.ts @@ -10,7 +10,10 @@ describe('fetch command integration', () => { it('maps CLI fetch flags to sdk params and prints formatted output', async () => { const fakeClient = createFakeClient(); - fakeClient.fetch.mockResolvedValue({ markdown: '# Title' }); + fakeClient.fetch.mockResolvedValue({ + favicon: 'https://example.com/favicon.ico', + markdown: '# Title', + }); mockGlobals(fakeClient); const { logSpy } = captureConsole(); await run([ @@ -33,6 +36,7 @@ describe('fetch command integration', () => { url: 'https://example.com', }); expect(logSpy).toHaveBeenCalledWith('# Title'); + expect(logSpy).toHaveBeenCalledWith('Favicon: https://example.com/favicon.ico'); }); it('runs async fetch via tasks and prints submitted task in JSON mode', async () => { diff --git a/src/__tests__/tasks-output.test.ts b/src/__tests__/tasks-output.test.ts index ae98640..8d07a5d 100644 --- a/src/__tests__/tasks-output.test.ts +++ b/src/__tests__/tasks-output.test.ts @@ -70,7 +70,7 @@ describe('formatTask', () => { it('renders completed fetch output', () => { const lines = formatTask( fetchTask({ - output: { markdown: '# Title' }, + output: { favicon: 'https://example.com/favicon.ico', markdown: '# Title' }, status: 'completed', }), ); @@ -78,6 +78,7 @@ describe('formatTask', () => { expect(lines).toContain('Task task-fetch'); expect(lines).toContain('Type: fetch'); expect(lines).toContain('# Title'); + expect(lines).toContain('Favicon: https://example.com/favicon.ico'); }); it('prints the error message when failed', () => { diff --git a/src/output/fetch.ts b/src/output/fetch.ts index 1ccdf91..7b15636 100644 --- a/src/output/fetch.ts +++ b/src/output/fetch.ts @@ -1,6 +1,6 @@ import type { FetchImage, LinkupFetchResponse } from 'linkup-sdk'; -type FetchOutputResponse = Pick & { +type FetchOutputResponse = Pick & { rawContent?: string; rawHtml?: string; images?: FetchImage[]; @@ -8,7 +8,7 @@ type FetchOutputResponse = Pick & { // Render a fetch response as printable lines. export function formatFetch(response: FetchOutputResponse): string[] { - const lines = ['', response.markdown.trim(), '']; + const lines = ['', response.markdown.trim(), '', `Favicon: ${response.favicon}`, '']; if (response.rawContent) { lines.push('Raw Content:', response.rawContent.trim(), '');