Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

- Use `--render-js` for JS-heavy pages.
- Add `--include-raw-content` to include the source page content.
Expand Down
47 changes: 41 additions & 6 deletions src/__tests__/fetch-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '\n<html><body>Hello</body></html>\n',
});

expect(lines).toEqual(['', 'Hello', '', 'Raw HTML:', '<html><body>Hello</body></html>', '']);
expect(lines).toEqual([
'',
'Hello',
'',
'Favicon: https://example.com/favicon.ico',
'',
'Raw HTML:',
'<html><body>Hello</body></html>',
'',
]);
});

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' },
Expand All @@ -44,6 +77,8 @@ describe('formatFetch', () => {
'',
'Hello',
'',
'Favicon: https://example.com/favicon.ico',
'',
'Images:',
' • Logo',
' https://example.com/logo.png',
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/fetch.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -30,6 +33,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 () => {
Expand Down
3 changes: 2 additions & 1 deletion src/__tests__/tasks-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ 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',
}),
);

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', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/output/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { FetchImage, LinkupFetchResponse } from 'linkup-sdk';

type FetchOutputResponse = Pick<LinkupFetchResponse, 'markdown'> & {
type FetchOutputResponse = Pick<LinkupFetchResponse, 'favicon' | 'markdown'> & {
rawContent?: string;
rawHtml?: string;
images?: FetchImage[];
};

// 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(), '');
Expand Down