src / tools / feedTools.ts
src / tools / feedTools.ts
/**
* Feed/Web Tools
* Provides RSS/Atom feed fetching, parsing, and monitoring capabilities.
*/
import Parser from 'rss-parser';
import { z } from 'zod';
import fetch from 'node-fetch';
// Zod schemas for validation
const rssFetchSchema = z.object({
url: z.string().url(),
});
const atomParseSchema = z.object({
xml_content: z.string().min(1),
});
const feedMonitorCheckSchema = z.object({
feed_url: z.string().url(),
last_etag: z.string().optional(),
});
/**
* Fetch and parse an RSS/Atom feed.
*/
export async function rss_fetch(url: string): Promise<{ title: string; link: string; items: Array<{ title: string; link: string; description: string; pubDate: string }> }> {
const validated = rssFetchSchema.parse({ url });
try {
const parser = new Parser();
const feed = await parser.parseURL(validated.url);
return {
title: feed.title || '',
link: feed.link || '',
items: (feed.items || []).map(item => ({
title: item.title || '',
link: item.link || '',
description: item.contentSnippet || item.content || '',
pubDate: item.pubDate ? new Date(item.pubDate).toISOString() : ''
}))
};
} catch (error) {
throw new Error(`Failed to fetch RSS feed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Parse Atom XML content directly.
*/
export async function atom_parse(xml_content: string): Promise<{ feed_info: any; entries: Array<{ title: string; link: string; summary: string; published: string }> }> {
const validated = atomParseSchema.parse({ xml_content });
try {
// Parse XML manually (simple implementation)
const parser = new Parser();
const feed = await parser.parseString(validated.xml_content);
return {
feed_info: {
title: feed.title || '',
link: feed.link || '',
updated: feed.updated ? new Date(feed.updated).toISOString() : ''
},
entries: (feed.items || []).map(item => ({
title: item.title || '',
link: item.link || '',
summary: item.contentSnippet || item.content || '',
published: item.pubDate ? new Date(item.pubDate).toISOString() : ''
}))
};
} catch (error) {
throw new Error(`Failed to parse Atom feed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Check if a feed has been updated since last check.
*/
export async function feed_monitor_check(feed_url: string, last_etag?: string): Promise<{ updated: boolean; new_items?: Array<{ title: string; link: string }> }> {
const validated = feedMonitorCheckSchema.parse({ feed_url, last_etag });
try {
const response = await fetch(validated.feed_url, {
headers: {
'User-Agent': 'BeledariansLMStudioTools/1.0',
...(validated.last_etag ? { 'If-None-Match': validated.last_etag } : {})
},
timeout: 30000 // 30 second timeout
});
if (response.status === 304) {
return { updated: false };
}
const etag = response.headers.get('etag') || '';
if (!etag && !validated.last_etag) {
// First check - no ETag to compare against
const feed = await rss_fetch(validated.feed_url);
return {
updated: true,
new_items: feed.items.slice(0, 5).map(item => ({ title: item.title, link: item.link }))
};
}
// Parse and check for updates
const parser = new Parser();
const text = await response.text();
const feed = await parser.parseString(text);
return {
updated: true,
new_items: (feed.items || []).slice(0, 5).map(item => ({ title: item.title || '', link: item.link || '' }))
};
} catch (error) {
throw new Error(`Feed monitoring check failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Feed/Web Tools
* Provides RSS/Atom feed fetching, parsing, and monitoring capabilities.
*/
import Parser from 'rss-parser';
import { z } from 'zod';
import fetch from 'node-fetch';
// Zod schemas for validation
const rssFetchSchema = z.object({
url: z.string().url(),
});
const atomParseSchema = z.object({
xml_content: z.string().min(1),
});
const feedMonitorCheckSchema = z.object({
feed_url: z.string().url(),
last_etag: z.string().optional(),
});
/**
* Fetch and parse an RSS/Atom feed.
*/
export async function rss_fetch(url: string): Promise<{ title: string; link: string; items: Array<{ title: string; link: string; description: string; pubDate: string }> }> {
const validated = rssFetchSchema.parse({ url });
try {
const parser = new Parser();
const feed = await parser.parseURL(validated.url);
return {
title: feed.title || '',
link: feed.link || '',
items: (feed.items || []).map(item => ({
title: item.title || '',
link: item.link || '',
description: item.contentSnippet || item.content || '',
pubDate: item.pubDate ? new Date(item.pubDate).toISOString() : ''
}))
};
} catch (error) {
throw new Error(`Failed to fetch RSS feed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Parse Atom XML content directly.
*/
export async function atom_parse(xml_content: string): Promise<{ feed_info: any; entries: Array<{ title: string; link: string; summary: string; published: string }> }> {
const validated = atomParseSchema.parse({ xml_content });
try {
// Parse XML manually (simple implementation)
const parser = new Parser();
const feed = await parser.parseString(validated.xml_content);
return {
feed_info: {
title: feed.title || '',
link: feed.link || '',
updated: feed.updated ? new Date(feed.updated).toISOString() : ''
},
entries: (feed.items || []).map(item => ({
title: item.title || '',
link: item.link || '',
summary: item.contentSnippet || item.content || '',
published: item.pubDate ? new Date(item.pubDate).toISOString() : ''
}))
};
} catch (error) {
throw new Error(`Failed to parse Atom feed: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Check if a feed has been updated since last check.
*/
export async function feed_monitor_check(feed_url: string, last_etag?: string): Promise<{ updated: boolean; new_items?: Array<{ title: string; link: string }> }> {
const validated = feedMonitorCheckSchema.parse({ feed_url, last_etag });
try {
const response = await fetch(validated.feed_url, {
headers: {
'User-Agent': 'BeledariansLMStudioTools/1.0',
...(validated.last_etag ? { 'If-None-Match': validated.last_etag } : {})
},
timeout: 30000 // 30 second timeout
});
if (response.status === 304) {
return { updated: false };
}
const etag = response.headers.get('etag') || '';
if (!etag && !validated.last_etag) {
// First check - no ETag to compare against
const feed = await rss_fetch(validated.feed_url);
return {
updated: true,
new_items: feed.items.slice(0, 5).map(item => ({ title: item.title, link: item.link }))
};
}
// Parse and check for updates
const parser = new Parser();
const text = await response.text();
const feed = await parser.parseString(text);
return {
updated: true,
new_items: (feed.items || []).slice(0, 5).map(item => ({ title: item.title || '', link: item.link || '' }))
};
} catch (error) {
throw new Error(`Feed monitoring check failed: ${error instanceof Error ? error.message : String(error)}`);
}
}