Skip to content

Commit

Permalink
Merge pull request #128 from dcSpark/feature/add-hacker-news-tool
Browse files Browse the repository at this point in the history
feat: add hacker-news tool
  • Loading branch information
guillevalin authored Feb 4, 2025
2 parents 08dcf2d + 0e078da commit 2ec2b44
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 0 deletions.
Binary file added tools/hacker-news/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tools/hacker-news/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions tools/hacker-news/index.test.ts
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
});
});
57 changes: 57 additions & 0 deletions tools/hacker-news/metadata.json
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"]
}
}
3 changes: 3 additions & 0 deletions tools/hacker-news/store.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"categoryId": "e9e36f6d-a0e3-47e9-b782-acb946b0199e"
}
60 changes: 60 additions & 0 deletions tools/hacker-news/tool.ts
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: [] };
}
};

0 comments on commit 2ec2b44

Please sign in to comment.