-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #128 from dcSpark/feature/add-hacker-news-tool
feat: add hacker-news tool
- Loading branch information
Showing
6 changed files
with
169 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { expect } from '@jest/globals'; | ||
import { getToolTestClient } from '../../src/test/utils'; | ||
import * as path from 'path'; | ||
|
||
describe('Hacker News Tool', () => { | ||
const toolPath = path.join(__dirname, 'tool.ts'); | ||
const client = getToolTestClient(); | ||
|
||
it('fetches top stories from Hacker News with default limit', async () => { | ||
const response = await client.executeToolFromFile(toolPath, {}, {}); | ||
|
||
const stories = response.stories; | ||
expect(Array.isArray(stories)).toBe(true); | ||
expect(stories.length).toBe(10); // Default limit is 10 | ||
|
||
// Check first story has required properties | ||
if (stories.length > 0) { | ||
const story = stories[0]; | ||
expect(story).toHaveProperty('title'); | ||
expect(story).toHaveProperty('author'); | ||
expect(story).toHaveProperty('url'); | ||
|
||
// Check property types and values | ||
expect(typeof story.title).toBe('string'); | ||
expect(story.title.length).toBeGreaterThan(0); | ||
expect(typeof story.author).toBe('string'); | ||
expect(story.author.length).toBeGreaterThan(0); | ||
expect(typeof story.url).toBe('string'); | ||
expect(story.url).toMatch(/^https?:\/\//); | ||
} | ||
}, 10000); | ||
|
||
it('respects custom limit', async () => { | ||
const response = await client.executeToolFromFile(toolPath, {}, { limit: 3 }); | ||
console.log(response); | ||
expect(Array.isArray(response.stories)).toBe(true); | ||
expect(response.stories.length).toBe(3); | ||
}); | ||
|
||
it('handles invalid limits gracefully', async () => { | ||
// Test with negative limit | ||
const responseNegative = await client.executeToolFromFile(toolPath, {}, { limit: -1 }); | ||
expect(responseNegative.stories.length).toBe(1); // Should use minimum limit of 1 | ||
|
||
// Test with limit > 10 | ||
const responseOverLimit = await client.executeToolFromFile(toolPath, {}, { limit: 20 }); | ||
expect(responseOverLimit.stories.length).toBe(10); // Should cap at maximum of 10 | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
{ | ||
"id": "hacker-news", | ||
"name": "hacker-news", | ||
"version": "1.0.0", | ||
"description": "Fetches top tech stories from Hacker News", | ||
"author": "Shinkai", | ||
"license": "MIT", | ||
"keywords": [ | ||
"hacker-news", | ||
"news", | ||
"tech", | ||
"stories" | ||
], | ||
"configurations": { | ||
"type": "object", | ||
"properties": { | ||
"limit": { | ||
"type": "number", | ||
"description": "Number of stories to fetch (default: 10)", | ||
"default": 10 | ||
} | ||
}, | ||
"required": [] | ||
}, | ||
"parameters": { | ||
"type": "object", | ||
"properties": {}, | ||
"required": [] | ||
}, | ||
"result": { | ||
"type": "object", | ||
"properties": { | ||
"stories": { | ||
"type": "array", | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"title": { | ||
"type": "string", | ||
"description": "Title of the story" | ||
}, | ||
"author": { | ||
"type": "string", | ||
"description": "Author/poster of the story" | ||
}, | ||
"url": { | ||
"type": "string", | ||
"description": "URL of the story or HN discussion if no URL provided" | ||
} | ||
}, | ||
"required": ["title", "author", "url"] | ||
} | ||
} | ||
}, | ||
"required": ["stories"] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"categoryId": "e9e36f6d-a0e3-47e9-b782-acb946b0199e" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import axios from 'npm:axios@1.7.7'; | ||
|
||
type Configurations = { | ||
limit?: number; // Optional limit of stories to fetch, defaults to 10 | ||
}; | ||
|
||
type Parameters = {}; // No input parameters needed | ||
|
||
type Result = { | ||
stories: Array<{ | ||
title: string; | ||
author: string; | ||
url: string; | ||
}>; | ||
}; | ||
|
||
export type Run<C extends Record<string, any>, I extends Record<string, any>, R extends Record<string, any>> = (config: C, inputs: I) => Promise<R>; | ||
|
||
export const run: Run<Configurations, Parameters, Result> = async ( | ||
configurations: Configurations, | ||
_params: Parameters, | ||
): Promise<Result> => { | ||
// Ensure limit is a positive number between 1 and 10 | ||
const requestedLimit = configurations.limit ?? 10; | ||
const limit = Math.min(Math.max(1, requestedLimit), 10); | ||
|
||
const TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json'; | ||
const ITEM_URL = 'https://hacker-news.firebaseio.com/v0/item'; | ||
|
||
try { | ||
// Fetch top stories IDs | ||
const response = await axios.get(TOP_STORIES_URL); | ||
const storyIds = response.data.slice(0, limit); | ||
|
||
// Fetch details for each story | ||
const stories = await Promise.all( | ||
storyIds.map(async (id: number) => { | ||
const storyResponse = await axios.get(`${ITEM_URL}/${id}.json`); | ||
const story = storyResponse.data; | ||
|
||
if (story && story.type === 'story') { | ||
return { | ||
title: story.title || '', | ||
author: story.by || '', | ||
url: story.url || `https://news.ycombinator.com/item?id=${id}`, | ||
}; | ||
} | ||
return null; | ||
}), | ||
); | ||
|
||
// Filter out null values and return results | ||
return { | ||
stories: stories.filter((story): story is NonNullable<typeof story> => story !== null), | ||
}; | ||
} catch (error) { | ||
console.error('Error fetching Hacker News stories:', error); | ||
return { stories: [] }; | ||
} | ||
}; |