agentic-rss-parser 1.0.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.
@@ -0,0 +1,73 @@
1
+ import { AnalysisSchema, validateAnalysis } from '../agent.js';
2
+ import { z } from 'zod';
3
+
4
+ async function loadProviderModel(provider, modelId, apiKey, baseURL) {
5
+ if (!provider || provider === 'heuristic') return null;
6
+
7
+ if (provider === 'openai') {
8
+ const { openai } = await import('@ai-sdk/openai');
9
+ return openai(modelId ?? 'gpt-4o-mini', apiKey ? { apiKey, baseURL } : baseURL ? { baseURL } : undefined);
10
+ }
11
+
12
+ if (provider === 'anthropic') {
13
+ const { anthropic } = await import('@ai-sdk/anthropic');
14
+ return anthropic(modelId ?? 'claude-3-5-sonnet-latest', apiKey ? { apiKey, baseURL } : baseURL ? { baseURL } : undefined);
15
+ }
16
+
17
+ if (provider === 'local') {
18
+ const { openai } = await import('@ai-sdk/openai');
19
+ return openai(modelId ?? 'local-model', {
20
+ apiKey: apiKey ?? 'local',
21
+ baseURL: baseURL ?? 'http://localhost:11434/v1'
22
+ });
23
+ }
24
+
25
+ throw new Error(`Unsupported provider: ${provider}`);
26
+ }
27
+
28
+ export async function createAnalyzer(config = {}) {
29
+ const model = await loadProviderModel(config.provider, config.model, config.apiKey, config.baseURL);
30
+
31
+ if (!model) {
32
+ return async ({ item, context }) => {
33
+ const text = `${item.title}\n${item.contentSnippet ?? ''}\n${context ?? ''}`.toLowerCase();
34
+ const signals = ['release', 'security', 'vulnerability', 'node', 'javascript', 'typescript', 'framework', 'api', 'breaking', 'performance', 'agent', 'rss'];
35
+ const score = signals.reduce((total, signal) => total + (text.includes(signal) ? 1 : 0), 0);
36
+ return validateAnalysis({
37
+ decision: score >= 3 ? 'relevant' : 'ignore',
38
+ confidence: Math.min(95, 35 + score * 10),
39
+ summary: score >= 3 ? `Likely worth reading: ${item.title}` : `Low-signal item: ${item.title}`,
40
+ impact: score >= 3 ? 'Could affect engineering decisions or tooling.' : 'Probably noise for a technical feed.',
41
+ actionItems: score >= 3 ? ['Review the source article.', 'Share with the relevant team if actionable.'] : [],
42
+ tags: signals.filter((signal) => text.includes(signal)).slice(0, 5)
43
+ });
44
+ };
45
+ }
46
+
47
+ return async ({ item, context }) => {
48
+ const { generateObject } = await import('ai');
49
+ const schema = z.object({
50
+ decision: z.enum(['relevant', 'ignore']),
51
+ confidence: z.number().int().min(0).max(100),
52
+ summary: z.string().min(1),
53
+ impact: z.string().min(1),
54
+ actionItems: z.array(z.string()).default([]),
55
+ tags: z.array(z.string()).default([])
56
+ });
57
+
58
+ const prompt = [
59
+ `Title: ${item.title}`,
60
+ `URL: ${item.link}`,
61
+ `Feed snippet: ${item.contentSnippet ?? ''}`,
62
+ `Expanded context: ${context ?? ''}`
63
+ ].join('\n');
64
+
65
+ const response = await generateObject({
66
+ model,
67
+ schema,
68
+ prompt
69
+ });
70
+
71
+ return AnalysisSchema.parse(response.object);
72
+ };
73
+ }
package/src/agent.js ADDED
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+ import { fetchFullArticle } from './fetch-article.js';
3
+
4
+ function validateAnalysis(result) {
5
+ const decision = result?.decision === 'relevant' ? 'relevant' : 'ignore';
6
+ const confidence = Number.isFinite(result?.confidence)
7
+ ? Math.max(0, Math.min(100, Math.trunc(result.confidence)))
8
+ : 0;
9
+ const summary = typeof result?.summary === 'string' && result.summary.trim() ? result.summary.trim() : 'No summary provided.';
10
+ const impact = typeof result?.impact === 'string' && result.impact.trim() ? result.impact.trim() : 'No impact provided.';
11
+ const actionItems = Array.isArray(result?.actionItems)
12
+ ? result.actionItems.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim())
13
+ : [];
14
+ const tags = Array.isArray(result?.tags)
15
+ ? [...new Set(result.tags.filter((tag) => typeof tag === 'string' && tag.trim()).map((tag) => tag.trim()))]
16
+ : [];
17
+
18
+ return { decision, confidence, summary, impact, actionItems, tags };
19
+ }
20
+
21
+ export const AnalysisSchema = z.object({
22
+ decision: z.enum(['relevant', 'ignore']),
23
+ confidence: z.number().int().min(0).max(100),
24
+ summary: z.string().min(1),
25
+ impact: z.string().min(1),
26
+ actionItems: z.array(z.string()),
27
+ tags: z.array(z.string())
28
+ });
29
+
30
+ function heuristicAnalyze(item, context) {
31
+ const text = `${item.title}\n${item.contentSnippet ?? ''}\n${context ?? ''}`.toLowerCase();
32
+ const signals = [
33
+ 'release',
34
+ 'security',
35
+ 'vulnerability',
36
+ 'node',
37
+ 'javascript',
38
+ 'typescript',
39
+ 'framework',
40
+ 'api',
41
+ 'breaking',
42
+ 'performance',
43
+ 'agent',
44
+ 'rss'
45
+ ];
46
+ const score = signals.reduce((total, signal) => total + (text.includes(signal) ? 1 : 0), 0);
47
+ const confidence = Math.min(95, 35 + score * 10);
48
+ const decision = score >= 3 ? 'relevant' : 'ignore';
49
+ return validateAnalysis({
50
+ decision,
51
+ confidence,
52
+ summary: decision === 'relevant' ? `Likely worth reading: ${item.title}` : `Low-signal item: ${item.title}`,
53
+ impact: decision === 'relevant' ? 'Could affect engineering decisions or tooling.' : 'Probably noise for a technical feed.',
54
+ actionItems: decision === 'relevant' ? ['Review the source article.', 'Share with the relevant team if actionable.'] : [],
55
+ tags: [...new Set(signals.filter((signal) => text.includes(signal)).slice(0, 5))]
56
+ });
57
+ }
58
+
59
+ export async function analyzeFeedItem(item, options = {}) {
60
+ const shouldFetch = Boolean(options.fetchFullArticle) && !item.contentSnippet;
61
+ const context = shouldFetch ? await fetchFullArticle(item.link) : item.contentSnippet ?? item.content ?? '';
62
+
63
+ if (typeof options.analyzer === 'function') {
64
+ const result = await options.analyzer({ item, context });
65
+ return validateAnalysis(result);
66
+ }
67
+
68
+ return heuristicAnalyze(item, context);
69
+ }
70
+
71
+ export { validateAnalysis };
package/src/cli.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { runAgenticParser } from './parser.js';
5
+
6
+ function parseArgs(argv) {
7
+ const args = { feeds: [], db: './data/rss-agent.db', fetchFullArticle: false };
8
+ for (let i = 2; i < argv.length; i += 1) {
9
+ const current = argv[i];
10
+ if (current === '--feed') args.feeds.push(argv[++i]);
11
+ else if (current === '--db') args.db = argv[++i];
12
+ else if (current === '--fetch-full-article') args.fetchFullArticle = true;
13
+ }
14
+ return args;
15
+ }
16
+
17
+ const args = parseArgs(process.argv);
18
+
19
+ if (!args.feeds.length) {
20
+ console.error('Usage: agentic-rss-parser --feed <url> [--feed <url>] [--db <path>] [--fetch-full-article]');
21
+ process.exit(1);
22
+ }
23
+
24
+ const dbPath = resolve(args.db);
25
+ if (!existsSync(resolve('.'))) {
26
+ console.error('Working directory not found.');
27
+ process.exit(1);
28
+ }
29
+
30
+ const results = await runAgenticParser({
31
+ feedUrls: args.feeds,
32
+ dbPath,
33
+ fetchFullArticle: args.fetchFullArticle
34
+ });
35
+
36
+ for (const { item, analysis } of results) {
37
+ if (analysis.decision === 'relevant') {
38
+ console.log(JSON.stringify({ title: item.title, link: item.link, ...analysis }, null, 2));
39
+ }
40
+ }
package/src/compat.js ADDED
@@ -0,0 +1,82 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { runAgenticParser } from './parser.js';
3
+ import { parseFeedXml } from './core/parser.js';
4
+ import { fetchTextWithRedirects } from './core/http.js';
5
+
6
+ const DEFAULT_OPTIONS = {
7
+ normalize: true,
8
+ customFields: { feed: [], item: [] },
9
+ headers: undefined,
10
+ timeout: 10000,
11
+ maxRedirects: 5,
12
+ requestOptions: {},
13
+ defaultRSS: 2.0,
14
+ xml2js: {}
15
+ };
16
+
17
+ function mergeOptions(options = {}) {
18
+ return {
19
+ ...DEFAULT_OPTIONS,
20
+ ...options,
21
+ customFields: {
22
+ feed: [...(DEFAULT_OPTIONS.customFields.feed || []), ...(options.customFields?.feed || [])],
23
+ item: [...(DEFAULT_OPTIONS.customFields.item || []), ...(options.customFields?.item || [])]
24
+ },
25
+ requestOptions: {
26
+ ...DEFAULT_OPTIONS.requestOptions,
27
+ ...(options.requestOptions || {})
28
+ },
29
+ xml2js: {
30
+ ...DEFAULT_OPTIONS.xml2js,
31
+ ...(options.xml2js || {})
32
+ }
33
+ };
34
+ }
35
+
36
+ export class ParserCompat {
37
+ constructor(options = {}) {
38
+ this.options = mergeOptions(options);
39
+ }
40
+
41
+ parseURL(url, callback) {
42
+ const promise = fetchTextWithRedirects(url, this.options).then((xml) => this.parseString(xml));
43
+
44
+ return maybeCallback(promise, callback);
45
+ }
46
+
47
+ parseString(xml, callback) {
48
+ const promise = parseFeedXml(xml, this.options);
49
+ return maybeCallback(promise, callback);
50
+ }
51
+
52
+ parseFile(filePath, callback) {
53
+ const promise = readFile(filePath, 'utf8').then((xml) => this.parseString(xml));
54
+ return maybeCallback(promise, callback);
55
+ }
56
+
57
+ async parseFeed(urls, config = {}) {
58
+ return runAgenticParser({
59
+ feedUrls: Array.isArray(urls) ? urls : [urls],
60
+ dbPath: config.dbPath ?? './data/rss-agent.db',
61
+ fetchFullArticle: Boolean(config.fetchFullArticle),
62
+ concurrency: config.concurrency,
63
+ parserOptions: this.options,
64
+ analyzer: config.analyzer,
65
+ model: config.model
66
+ });
67
+ }
68
+ }
69
+
70
+ function maybeCallback(promise, callback) {
71
+ if (typeof callback === 'function') {
72
+ promise.then((value) => callback(null, value), (error) => callback(error));
73
+ return undefined;
74
+ }
75
+ return promise;
76
+ }
77
+
78
+ export function createParser(options = {}) {
79
+ return new ParserCompat(options);
80
+ }
81
+
82
+ export default ParserCompat;
@@ -0,0 +1,63 @@
1
+ const DEFAULT_USER_AGENT = 'agentic-rss-parser/1.0.1';
2
+
3
+ export async function fetchTextWithRedirects(url, options = {}) {
4
+ assertHttpUrl(url);
5
+
6
+ const maxRedirects = Number.isFinite(options.maxRedirects) ? Math.max(0, options.maxRedirects) : 5;
7
+ let currentUrl = url;
8
+ let redirects = 0;
9
+
10
+ while (true) {
11
+ const controller = new AbortController();
12
+ const timeoutMs = Number.isFinite(options.timeout) ? options.timeout : 10000;
13
+ const timeoutId = setTimeout(() => controller.abort(new Error('Request timed out')), timeoutMs);
14
+ const requestOptions = {
15
+ ...options.requestOptions,
16
+ signal: controller.signal,
17
+ redirect: 'manual',
18
+ headers: {
19
+ 'user-agent': DEFAULT_USER_AGENT,
20
+ ...(options.headers || {}),
21
+ ...(options.requestOptions?.headers || {})
22
+ }
23
+ };
24
+
25
+ try {
26
+ const response = await fetch(currentUrl, requestOptions);
27
+
28
+ if (isRedirect(response.status)) {
29
+ if (redirects >= maxRedirects) {
30
+ throw new Error(`Too many redirects while fetching ${url}`);
31
+ }
32
+
33
+ const location = response.headers.get('location');
34
+ if (!location) {
35
+ throw new Error(`Redirect response missing Location header for ${currentUrl}`);
36
+ }
37
+
38
+ currentUrl = new URL(location, currentUrl).toString();
39
+ redirects += 1;
40
+ continue;
41
+ }
42
+
43
+ if (!response.ok) {
44
+ throw new Error(`Request failed with status ${response.status}`);
45
+ }
46
+
47
+ return await response.text();
48
+ } finally {
49
+ clearTimeout(timeoutId);
50
+ }
51
+ }
52
+ }
53
+
54
+ function isRedirect(status) {
55
+ return status >= 300 && status < 400;
56
+ }
57
+
58
+ function assertHttpUrl(url) {
59
+ const parsed = new URL(url);
60
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
61
+ throw new Error(`Unsupported feed URL protocol: ${parsed.protocol}`);
62
+ }
63
+ }
@@ -0,0 +1,143 @@
1
+ import { XMLParser } from 'fast-xml-parser';
2
+
3
+ function asArray(value) {
4
+ if (value == null) return [];
5
+ return Array.isArray(value) ? value : [value];
6
+ }
7
+
8
+ function stripHtml(html = '') {
9
+ return String(html)
10
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
11
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
12
+ .replace(/<[^>]+>/g, ' ')
13
+ .replace(/&nbsp;/gi, ' ')
14
+ .replace(/\s+/g, ' ')
15
+ .trim();
16
+ }
17
+
18
+ function textValue(value) {
19
+ if (value == null) return '';
20
+ if (typeof value === 'string') return value;
21
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
22
+ if (Array.isArray(value)) return textValue(value[0]);
23
+ if (typeof value === 'object') {
24
+ if ('#text' in value) return textValue(value['#text']);
25
+ if ('#cdata' in value) return textValue(value['#cdata']);
26
+ if ('_text' in value) return textValue(value._text);
27
+ const first = Object.values(value)[0];
28
+ return textValue(first);
29
+ }
30
+ return '';
31
+ }
32
+
33
+ function extractCustomValue(source, key, keepArray = false) {
34
+ const value = source?.[key];
35
+ if (value == null) return keepArray ? [] : undefined;
36
+ if (keepArray) return asArray(value).map(textValue);
37
+ return textValue(asArray(value)[0]);
38
+ }
39
+
40
+ function applyCustomFields(target, source, fields = []) {
41
+ for (const field of fields) {
42
+ if (typeof field === 'string') {
43
+ const value = extractCustomValue(source, field);
44
+ if (value !== undefined) target[field] = value;
45
+ continue;
46
+ }
47
+
48
+ const [fromField, toField, flags = {}] = field;
49
+ const value = extractCustomValue(source, fromField, Boolean(flags.keepArray));
50
+ if (value !== undefined) {
51
+ target[toField] = value;
52
+ if (flags.includeSnippet) {
53
+ target[`${toField}Snippet`] = stripHtml(Array.isArray(value) ? value.join(' ') : value);
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ function getLinkValue(node) {
60
+ if (node == null) return '';
61
+ if (Array.isArray(node)) return getLinkValue(node[0]);
62
+ if (typeof node === 'string') return node;
63
+ if (typeof node !== 'object') return textValue(node);
64
+ if ('@_href' in node) return textValue(node['@_href']);
65
+ if ('href' in node) return textValue(node.href);
66
+ return textValue(node);
67
+ }
68
+
69
+ function normalizeItem(itemNode, options) {
70
+ const normalized = {
71
+ title: textValue(itemNode.title),
72
+ link: getLinkValue(itemNode.link),
73
+ pubDate: textValue(itemNode.pubDate || itemNode.updated || itemNode.published),
74
+ isoDate: textValue(itemNode.pubDate || itemNode.updated || itemNode.published),
75
+ guid: textValue(itemNode.guid || itemNode.id),
76
+ content: textValue(itemNode['content:encoded'] || itemNode.content || itemNode.summary || itemNode.description),
77
+ contentSnippet: stripHtml(textValue(itemNode.description || itemNode.summary || itemNode.content || itemNode['content:encoded'])),
78
+ categories: asArray(itemNode.category ?? itemNode.categories).map(textValue).filter(Boolean)
79
+ };
80
+
81
+ if (!options.normalize) {
82
+ Object.assign(normalized, itemNode);
83
+ }
84
+
85
+ applyCustomFields(normalized, itemNode, options.customFields?.item || []);
86
+
87
+ if (!normalized.creator) {
88
+ const creator = itemNode.creator || itemNode.author || itemNode['dc:creator'];
89
+ if (creator) normalized.creator = textValue(creator);
90
+ }
91
+
92
+ const contentSnippet = normalized.contentSnippet || stripHtml(normalized.content || '');
93
+ normalized.contentSnippet = contentSnippet;
94
+
95
+ return normalized;
96
+ }
97
+
98
+ function normalizeFeed(feedNode, items, options) {
99
+ const feed = {
100
+ title: textValue(feedNode.title),
101
+ description: textValue(feedNode.description || feedNode.subtitle),
102
+ link: getLinkValue(feedNode.link),
103
+ feedUrl: textValue(feedNode.feedUrl),
104
+ items
105
+ };
106
+
107
+ applyCustomFields(feed, feedNode, options.customFields?.feed || []);
108
+ return feed;
109
+ }
110
+
111
+ function pickFeedNode(parsed) {
112
+ if (parsed?.rss?.channel) return parsed.rss.channel;
113
+ if (parsed?.feed) return parsed.feed;
114
+ if (parsed?.channel) return parsed.channel;
115
+ return parsed;
116
+ }
117
+
118
+ function getFeedAndItems(parsed) {
119
+ const feedContainer = pickFeedNode(parsed);
120
+ const feedNode = Array.isArray(feedContainer) ? feedContainer[0] : feedContainer;
121
+ const rawItems = asArray(feedNode?.item || feedNode?.entry);
122
+ return { feedNode: feedNode || {}, rawItems };
123
+ }
124
+
125
+ export async function parseFeedXml(xml, options = {}) {
126
+ const parser = new XMLParser({
127
+ ignoreAttributes: false,
128
+ attributeNamePrefix: '@_',
129
+ textNodeName: '#text',
130
+ cdataPropName: '#cdata',
131
+ trimValues: true,
132
+ allowBooleanAttributes: true,
133
+ parseTagValue: false,
134
+ parseAttributeValue: false,
135
+ removeNSPrefix: false,
136
+ ...options.xml2js
137
+ });
138
+
139
+ const parsed = parser.parse(xml);
140
+ const { feedNode, rawItems } = getFeedAndItems(parsed);
141
+ const items = rawItems.map((itemNode) => normalizeItem(itemNode, options));
142
+ return normalizeFeed(feedNode, items, options);
143
+ }
@@ -0,0 +1,29 @@
1
+ const MAX_SNIPPET_CHARS = 1200;
2
+
3
+ function stripHtml(html) {
4
+ return html
5
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
6
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
7
+ .replace(/<[^>]+>/g, ' ')
8
+ .replace(/&nbsp;/gi, ' ')
9
+ .replace(/\s+/g, ' ')
10
+ .trim();
11
+ }
12
+
13
+ export async function fetchFullArticle(url) {
14
+ const response = await fetch(url, {
15
+ headers: {
16
+ 'user-agent': 'agentic-rss-parser/1.0 (+https://example.local)'
17
+ }
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(`Failed to fetch article: ${response.status} ${response.statusText}`);
22
+ }
23
+
24
+ const contentType = response.headers.get('content-type') || '';
25
+ const body = await response.text();
26
+ const plainText = contentType.includes('text/html') ? stripHtml(body) : body.replace(/\s+/g, ' ').trim();
27
+
28
+ return plainText.slice(0, MAX_SNIPPET_CHARS);
29
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,135 @@
1
+ export interface CustomFieldTuple {
2
+ 0: string;
3
+ 1: string;
4
+ 2?: {
5
+ keepArray?: boolean;
6
+ includeSnippet?: boolean;
7
+ };
8
+ }
9
+
10
+ export interface CustomFieldConfig {
11
+ feed?: Array<string | [string, string] | CustomFieldTuple>;
12
+ item?: Array<string | [string, string] | CustomFieldTuple>;
13
+ }
14
+
15
+ export interface ParserOptions {
16
+ customFields?: CustomFieldConfig;
17
+ defaultRSS?: number | string;
18
+ headers?: Record<string, string>;
19
+ timeout?: number;
20
+ maxRedirects?: number;
21
+ requestOptions?: Record<string, unknown>;
22
+ xml2js?: Record<string, unknown>;
23
+ normalize?: boolean;
24
+ }
25
+
26
+ export interface ParserFeedItem {
27
+ title?: string;
28
+ link?: string;
29
+ pubDate?: string;
30
+ isoDate?: string;
31
+ content?: string;
32
+ contentSnippet?: string;
33
+ guid?: string;
34
+ categories?: string[];
35
+ creator?: string;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ export interface ParserFeed<Feed = unknown, Item = ParserFeedItem> {
40
+ feedUrl?: string;
41
+ title?: string;
42
+ description?: string;
43
+ link?: string;
44
+ items: Item[];
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ export type ParserCallback<T> = (err: Error | null, result?: T) => void;
49
+
50
+ export class Parser<Feed = unknown, Item = ParserFeedItem> {
51
+ constructor(options?: ParserOptions);
52
+ parseURL(url: string): Promise<ParserFeed<Feed, Item>>;
53
+ parseURL(url: string, callback: ParserCallback<ParserFeed<Feed, Item>>): void;
54
+ parseString(xml: string): Promise<ParserFeed<Feed, Item>>;
55
+ parseString(xml: string, callback: ParserCallback<ParserFeed<Feed, Item>>): void;
56
+ parseFile(path: string): Promise<ParserFeed<Feed, Item>>;
57
+ parseFile(path: string, callback: ParserCallback<ParserFeed<Feed, Item>>): void;
58
+ parseFeed(
59
+ urls: string | string[],
60
+ config?: {
61
+ dbPath?: string;
62
+ fetchFullArticle?: boolean;
63
+ concurrency?: number;
64
+ analyzer?: (input: { item: Item; context: string }) => unknown;
65
+ model?: {
66
+ provider?: 'heuristic' | 'openai' | 'anthropic' | 'local';
67
+ model?: string;
68
+ apiKey?: string;
69
+ baseURL?: string;
70
+ };
71
+ }
72
+ ): Promise<Array<{ item: Item; analysis: unknown }>>;
73
+ }
74
+
75
+ export function createParser<Feed = unknown, Item = ParserFeedItem>(
76
+ options?: ParserOptions
77
+ ): Parser<Feed, Item>;
78
+
79
+ export function runAgenticParser(config: {
80
+ feedUrls: string[];
81
+ dbPath: string;
82
+ fetchFullArticle?: boolean;
83
+ concurrency?: number;
84
+ parserOptions?: ParserOptions;
85
+ analyzer?: (input: { item: ParserFeedItem; context: string }) => unknown;
86
+ model?: {
87
+ provider?: 'heuristic' | 'openai' | 'anthropic' | 'local';
88
+ model?: string;
89
+ apiKey?: string;
90
+ baseURL?: string;
91
+ };
92
+ }): Promise<Array<{ item: ParserFeedItem; analysis: unknown }>>;
93
+
94
+ export function analyzeFeedItem(
95
+ item: ParserFeedItem,
96
+ options?: {
97
+ fetchFullArticle?: boolean;
98
+ analyzer?: (input: { item: ParserFeedItem; context: string }) => unknown;
99
+ }
100
+ ): Promise<{
101
+ decision: 'relevant' | 'ignore';
102
+ confidence: number;
103
+ summary: string;
104
+ impact: string;
105
+ actionItems: string[];
106
+ tags: string[];
107
+ }>;
108
+
109
+ export function fetchFullArticle(url: string): Promise<string>;
110
+ export function createStorage(dbPath: string): {
111
+ hasProcessed(id: string): boolean;
112
+ markProcessed(item: { id: string; feedUrl: string; title: string; link: string; publishedAt?: string | null }): void;
113
+ saveAnalysis(
114
+ itemId: string,
115
+ analysis: {
116
+ id: string;
117
+ decision: string;
118
+ confidence: number;
119
+ summary: string;
120
+ impact: string;
121
+ actionItems: string[];
122
+ tags: string[];
123
+ }
124
+ ): void;
125
+ close(): void;
126
+ };
127
+ export function createAnalyzer(config?: {
128
+ provider?: 'heuristic' | 'openai' | 'anthropic' | 'local';
129
+ model?: string;
130
+ apiKey?: string;
131
+ baseURL?: string;
132
+ }): Promise<(input: { item: ParserFeedItem; context: string }) => Promise<unknown>>;
133
+
134
+ declare const ParserDefault: typeof Parser;
135
+ export default ParserDefault;
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { runAgenticParser } from './parser.js';
2
+ export { analyzeFeedItem } from './agent.js';
3
+ export { fetchFullArticle } from './fetch-article.js';
4
+ export { createStorage } from './storage.js';
5
+ export { createAnalyzer } from './adapters/provider.js';
6
+ export { ParserCompat as Parser, createParser } from './compat.js';
7
+ export { ParserCompat as default } from './compat.js';