@framers/agentos-ext-hacker-news 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/manifest.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@framers/agentos-ext-hacker-news",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "displayName": "Hacker News",
5
5
  "description": "Fetch and search Hacker News stories with category inference, filtering, and sorting. No API key required.",
6
6
  "category": "research",
7
7
  "author": "framers",
8
- "license": "MIT",
8
+ "license": "Apache-2.0",
9
9
  "tools": ["hacker_news"],
10
10
  "requiredSecrets": [],
11
11
  "entryPoint": "src/index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@framers/agentos-ext-hacker-news",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Hacker News extension for AgentOS — story search with category inference",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,25 +12,29 @@
12
12
  }
13
13
  },
14
14
  "peerDependencies": {
15
- "@framers/agentos": "^0.5.2"
15
+ "@framers/agentos": ">=0.10.40"
16
16
  },
17
17
  "devDependencies": {
18
- "@framers/agentos": "^0.6.6",
18
+ "@framers/agentos": "^0.13.40",
19
19
  "typescript": "^5.7.0",
20
- "vitest": "^3.0.0"
20
+ "vitest": "^3.2.7"
21
21
  },
22
- "license": "MIT",
22
+ "license": "Apache-2.0",
23
23
  "repository": {
24
24
  "type": "git",
25
- "url": "git+https://github.com/framersai/agentos-extensions.git",
25
+ "url": "git+https://github.com/framerslab/agentos-extensions.git",
26
26
  "directory": "registry/curated/research/hacker-news"
27
27
  },
28
- "homepage": "https://github.com/framersai/agentos-extensions/tree/master/registry/curated/research/hacker-news#readme",
28
+ "homepage": "https://github.com/framerslab/agentos-extensions/tree/master/registry/curated/research/hacker-news#readme",
29
29
  "bugs": {
30
- "url": "https://github.com/framersai/agentos-extensions/issues"
30
+ "url": "https://github.com/framerslab/agentos-extensions/issues"
31
31
  },
32
+ "files": [
33
+ "dist",
34
+ "manifest.json"
35
+ ],
32
36
  "scripts": {
33
37
  "build": "tsc",
34
- "test": "vitest run"
38
+ "test": "vitest run --passWithNoTests"
35
39
  }
36
40
  }
package/src/index.ts DELETED
@@ -1,38 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * Hacker News Extension Pack — HN story fetching for agents.
4
- *
5
- * General-purpose tool: returns raw stories without opinionated categorization.
6
- * Consumers decide how to classify content. No API key required.
7
- */
8
-
9
- import { HackerNewsTool } from './tools/hackerNews.js';
10
-
11
- export interface HackerNewsExtensionOptions {
12
- priority?: number;
13
- }
14
-
15
- export function createExtensionPack(context: any) {
16
- const options = (context.options || {}) as HackerNewsExtensionOptions;
17
- const tool = new HackerNewsTool();
18
-
19
- return {
20
- name: '@framers/agentos-ext-hacker-news',
21
- version: '1.0.0',
22
- descriptors: [
23
- {
24
- id: tool.name,
25
- kind: 'tool' as const,
26
- priority: options.priority || 50,
27
- payload: tool,
28
- requiredSecrets: [],
29
- },
30
- ],
31
- onActivate: async () => context.logger?.info('Hacker News Extension activated'),
32
- onDeactivate: async () => context.logger?.info('Hacker News Extension deactivated'),
33
- };
34
- }
35
-
36
- export { HackerNewsTool };
37
- export type { HackerNewsInput, HackerNewsOutput, HackerNewsStory } from './tools/hackerNews.js';
38
- export default createExtensionPack;
@@ -1,177 +0,0 @@
1
- // @ts-nocheck
2
- /**
3
- * Hacker News Tool — fetch and search HN stories via Algolia API.
4
- *
5
- * General-purpose extension tool: fetches raw stories and returns them as-is.
6
- * No category inference — consumers (wunderland, wunderland-sol, or any agent)
7
- * decide how to classify content on their end.
8
- *
9
- * No API key required — uses the public Algolia HN Search API.
10
- */
11
-
12
- import { createHash } from 'crypto';
13
- import type { ITool, ToolExecutionContext, ToolExecutionResult, JSONSchemaObject } from '@framers/agentos';
14
-
15
- // ---------------------------------------------------------------------------
16
- // Types
17
- // ---------------------------------------------------------------------------
18
-
19
- export interface HackerNewsInput {
20
- /** Search query (empty = front page stories). */
21
- query?: string;
22
- /** Sort order. */
23
- sortBy?: 'relevance' | 'date' | 'points';
24
- /** Minimum point threshold. */
25
- minPoints?: number;
26
- /** Time range filter. */
27
- timeRange?: 'day' | 'week' | 'month' | 'year';
28
- /** HN tag filter (story, comment, show_hn, ask_hn, front_page). */
29
- tag?: 'story' | 'comment' | 'show_hn' | 'ask_hn' | 'front_page';
30
- /** Maximum results to return. */
31
- maxResults?: number;
32
- }
33
-
34
- export interface HackerNewsStory {
35
- /** Story title. */
36
- title: string;
37
- /** External URL (or HN discussion if no external link). */
38
- url: string;
39
- /** Upvote count. */
40
- points: number;
41
- /** Author username. */
42
- author: string;
43
- /** Number of comments. */
44
- commentCount: number;
45
- /** HN discussion URL. */
46
- hnUrl: string;
47
- /** ISO timestamp. */
48
- publishedAt: string;
49
- /** SHA-256 hash of title::url for deduplication. */
50
- contentHash: string;
51
- /** Algolia object ID. */
52
- objectID: string;
53
- }
54
-
55
- export interface HackerNewsOutput {
56
- /** The query that was searched, or 'front_page' if none. */
57
- query: string;
58
- /** Returned stories. */
59
- stories: HackerNewsStory[];
60
- /** Number of stories returned. */
61
- totalFound: number;
62
- }
63
-
64
- // ---------------------------------------------------------------------------
65
- // Tool implementation
66
- // ---------------------------------------------------------------------------
67
-
68
- export class HackerNewsTool implements ITool<HackerNewsInput, HackerNewsOutput> {
69
- readonly id = 'hacker-news-v1';
70
- readonly name = 'hacker_news';
71
- readonly displayName = 'Hacker News';
72
- readonly description =
73
- 'Fetch and search Hacker News stories. Supports filtering by search query, ' +
74
- 'time range, minimum points, HN tags (show_hn, ask_hn, etc.), and sort order. ' +
75
- 'Returns raw story data — no opinionated categorization. No API key required.';
76
- readonly category = 'research';
77
- readonly version = '1.0.0';
78
- readonly hasSideEffects = false;
79
-
80
- readonly inputSchema: JSONSchemaObject = {
81
- type: 'object',
82
- properties: {
83
- query: { type: 'string', description: 'Search query. Leave empty for front page stories.' },
84
- sortBy: { type: 'string', enum: ['relevance', 'date', 'points'], default: 'points' },
85
- minPoints: { type: 'integer', minimum: 0, default: 0, description: 'Minimum upvote threshold.' },
86
- timeRange: { type: 'string', enum: ['day', 'week', 'month', 'year'], description: 'Restrict results to a time window.' },
87
- tag: { type: 'string', enum: ['story', 'comment', 'show_hn', 'ask_hn', 'front_page'], default: 'story', description: 'HN content type filter.' },
88
- maxResults: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
89
- },
90
- required: [],
91
- };
92
-
93
- readonly requiredCapabilities = ['capability:web_search'];
94
-
95
- async execute(args: HackerNewsInput, _context: ToolExecutionContext): Promise<ToolExecutionResult<HackerNewsOutput>> {
96
- try {
97
- const maxResults = args.maxResults ?? 10;
98
- const query = args.query?.trim() ?? '';
99
- const tag = args.tag ?? (query ? 'story' : 'front_page');
100
-
101
- // Build Algolia API URL
102
- const params = new URLSearchParams({
103
- tags: tag,
104
- hitsPerPage: String(Math.min(maxResults * 2, 100)),
105
- });
106
-
107
- if (query) {
108
- params.set('query', query);
109
- }
110
-
111
- if (args.timeRange) {
112
- const now = Math.floor(Date.now() / 1000);
113
- const ranges: Record<string, number> = { day: 86400, week: 604800, month: 2592000, year: 31536000 };
114
- params.set('numericFilters', `created_at_i>${now - (ranges[args.timeRange] ?? 604800)}`);
115
- }
116
-
117
- if (args.minPoints && args.minPoints > 0) {
118
- const existing = params.get('numericFilters');
119
- const pointsFilter = `points>${args.minPoints}`;
120
- params.set('numericFilters', existing ? `${existing},${pointsFilter}` : pointsFilter);
121
- }
122
-
123
- const url = `https://hn.algolia.com/api/v1/search?${params}`;
124
-
125
- const controller = new AbortController();
126
- const timeout = setTimeout(() => controller.abort(), 15000);
127
-
128
- let data: any;
129
- try {
130
- const res = await fetch(url, { signal: controller.signal });
131
- if (!res.ok) return { success: false, error: `HN API returned ${res.status}` };
132
- data = await res.json();
133
- } finally {
134
- clearTimeout(timeout);
135
- }
136
-
137
- if (!data.hits?.length) {
138
- return { success: true, output: { query: query || 'front_page', stories: [], totalFound: 0 } };
139
- }
140
-
141
- // Map raw Algolia hits to clean story objects
142
- let stories: HackerNewsStory[] = data.hits
143
- .filter((h: any) => h.title)
144
- .map((hit: any) => {
145
- const storyUrl = hit.url || `https://news.ycombinator.com/item?id=${hit.objectID}`;
146
- return {
147
- title: hit.title,
148
- url: storyUrl,
149
- points: hit.points ?? 0,
150
- author: hit.author ?? 'unknown',
151
- commentCount: hit.num_comments ?? 0,
152
- hnUrl: `https://news.ycombinator.com/item?id=${hit.objectID}`,
153
- publishedAt: hit.created_at ?? new Date().toISOString(),
154
- contentHash: createHash('sha256').update(`${hit.title}::${storyUrl}`).digest('hex'),
155
- objectID: hit.objectID,
156
- };
157
- });
158
-
159
- // Sort
160
- if (args.sortBy === 'date') {
161
- stories.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
162
- } else if (args.sortBy === 'points' || !args.sortBy) {
163
- stories.sort((a, b) => b.points - a.points);
164
- }
165
- // 'relevance' — keep Algolia's ordering
166
-
167
- stories = stories.slice(0, maxResults);
168
-
169
- return {
170
- success: true,
171
- output: { query: query || 'front_page', stories, totalFound: stories.length },
172
- };
173
- } catch (err: any) {
174
- return { success: false, error: `Hacker News fetch failed: ${err.message}` };
175
- }
176
- }
177
- }
@@ -1,84 +0,0 @@
1
- // @ts-nocheck
2
- import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
3
- import { HackerNewsTool } from '../src/tools/hackerNews';
4
-
5
- const MOCK_HN_RESPONSE = {
6
- hits: [
7
- {
8
- objectID: '1',
9
- title: 'Show HN: A new AI framework',
10
- url: 'https://example.com/ai',
11
- points: 150,
12
- created_at: '2026-03-30T10:00:00Z',
13
- author: 'testuser',
14
- num_comments: 42,
15
- story_text: null,
16
- },
17
- {
18
- objectID: '2',
19
- title: 'Ask HN: Best programming language 2026?',
20
- url: '',
21
- points: 85,
22
- created_at: '2026-03-30T09:00:00Z',
23
- author: 'dev123',
24
- num_comments: 200,
25
- story_text: 'What do you all think?',
26
- },
27
- ],
28
- nbHits: 2,
29
- };
30
-
31
- const originalFetch = globalThis.fetch;
32
- const mockFetch = vi.fn();
33
-
34
- beforeEach(() => {
35
- globalThis.fetch = mockFetch as any;
36
- mockFetch.mockReset();
37
- });
38
-
39
- afterAll(() => {
40
- globalThis.fetch = originalFetch;
41
- });
42
-
43
- describe('HackerNewsTool', () => {
44
- const tool = new HackerNewsTool();
45
-
46
- it('has correct tool metadata', () => {
47
- expect(tool.id).toBe('hacker-news-v1');
48
- expect(tool.name).toBe('hacker_news');
49
- });
50
-
51
- it('fetches front page stories by default', async () => {
52
- mockFetch.mockResolvedValue({
53
- ok: true,
54
- json: () => Promise.resolve(MOCK_HN_RESPONSE),
55
- });
56
-
57
- const result = await tool.execute({}, {} as any);
58
- // The tool wraps output in ToolExecutionResult — verify structure
59
- expect(result).toHaveProperty('success');
60
- if (result.success && result.data) {
61
- expect(Array.isArray(result.data.stories)).toBe(true);
62
- }
63
- });
64
-
65
- it('filters by minimum points', async () => {
66
- mockFetch.mockResolvedValue({
67
- ok: true,
68
- json: () => Promise.resolve(MOCK_HN_RESPONSE),
69
- });
70
-
71
- const result = await tool.execute({ minPoints: 100 }, {} as any);
72
- expect(result.success).toBe(true);
73
- // Only the 150-point story should survive the filter
74
- const stories = result.data?.stories ?? [];
75
- expect(stories.every((s: any) => s.points >= 100)).toBe(true);
76
- });
77
-
78
- it('handles API errors gracefully', async () => {
79
- mockFetch.mockResolvedValue({ ok: false, status: 500 });
80
-
81
- const result = await tool.execute({}, {} as any);
82
- expect(result.success).toBe(false);
83
- });
84
- });
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "declaration": true,
7
- "outDir": "dist",
8
- "rootDir": "src",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true
12
- },
13
- "include": ["src"]
14
- }