osint-feed 0.1.0
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/README.md +296 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.js +495 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# osint-feed
|
|
2
|
+
|
|
3
|
+
Config-driven news harvester for Node.js. Pulls articles from RSS feeds and HTML pages, deduplicates them, and produces a compact digest ready to feed into an LLM context window.
|
|
4
|
+
|
|
5
|
+
No AI inside. No opinions about your stack. Just articles in, structured data out.
|
|
6
|
+
|
|
7
|
+
Use RSS when you have it. Use HTML selectors when you do not. Filtering for your specific topic belongs in the app that consumes this library.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
You're building something that needs fresh news context — a SITREP generator, a threat monitor, a research assistant. You have 30+ sources across languages and formats. You need the data compact enough to fit in a Llama/GPT context window without blowing the budget.
|
|
12
|
+
|
|
13
|
+
Existing tools are either Python-only (newspaper4k), heavy self-hosted platforms (Huginn), or commercial APIs (Newscatcher, NewsAPI). Nothing in the JS/TS ecosystem does config-driven multi-source harvesting with built-in LLM-ready compression.
|
|
14
|
+
|
|
15
|
+
`osint-feed` fills that gap.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install osint-feed
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js 18+.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { createHarvester } from "osint-feed";
|
|
29
|
+
|
|
30
|
+
const harvester = createHarvester({
|
|
31
|
+
sources: [
|
|
32
|
+
{
|
|
33
|
+
id: "bbc-world",
|
|
34
|
+
name: "BBC World",
|
|
35
|
+
type: "rss",
|
|
36
|
+
url: "https://feeds.bbci.co.uk/news/world/rss.xml",
|
|
37
|
+
tags: ["global", "uk"],
|
|
38
|
+
interval: 15,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "nato",
|
|
42
|
+
name: "NATO Newsroom",
|
|
43
|
+
type: "html",
|
|
44
|
+
url: "https://www.nato.int/cps/en/natohq/news.htm",
|
|
45
|
+
tags: ["nato"],
|
|
46
|
+
interval: 30,
|
|
47
|
+
selectors: {
|
|
48
|
+
article: ".event-list-item",
|
|
49
|
+
title: "a span:first-child",
|
|
50
|
+
link: "a",
|
|
51
|
+
date: ".event-date",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Fetch everything
|
|
58
|
+
const articles = await harvester.fetchAll();
|
|
59
|
+
|
|
60
|
+
// Or get an LLM-ready digest
|
|
61
|
+
const { articles: digest, stats } = await harvester.digest();
|
|
62
|
+
console.log(`${stats.totalFetched} articles -> ${stats.afterDedup} unique -> ${stats.estimatedTokens} tokens`);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Source Types
|
|
66
|
+
|
|
67
|
+
### RSS / Atom
|
|
68
|
+
|
|
69
|
+
Works out of the box. No selectors needed — feeds are parsed automatically.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
{
|
|
73
|
+
id: "france24",
|
|
74
|
+
name: "France24",
|
|
75
|
+
type: "rss",
|
|
76
|
+
url: "https://www.france24.com/en/rss",
|
|
77
|
+
tags: ["global", "europe"],
|
|
78
|
+
interval: 15,
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### HTML Scraping
|
|
83
|
+
|
|
84
|
+
You define CSS selectors per source. The library uses [cheerio](https://github.com/cheeriojs/cheerio) — no headless browser, no Puppeteer overhead.
|
|
85
|
+
|
|
86
|
+
This is still config-driven scraping: the library does not auto-discover article lists or infer what is relevant to your use case.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
{
|
|
90
|
+
id: "defence24",
|
|
91
|
+
name: "Defence24",
|
|
92
|
+
type: "html",
|
|
93
|
+
url: "https://defence24.pl/",
|
|
94
|
+
tags: ["poland", "defence"],
|
|
95
|
+
interval: 15,
|
|
96
|
+
selectors: {
|
|
97
|
+
article: "article", // repeating container
|
|
98
|
+
title: "h2 a", // title text (within article)
|
|
99
|
+
link: "h2 a", // link href (within article)
|
|
100
|
+
date: "time", // optional: publication date
|
|
101
|
+
summary: ".lead", // optional: description text
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## API
|
|
107
|
+
|
|
108
|
+
### `createHarvester(options)`
|
|
109
|
+
|
|
110
|
+
Creates a harvester instance. Options:
|
|
111
|
+
|
|
112
|
+
| Option | Type | Default | Description |
|
|
113
|
+
|--------|------|---------|-------------|
|
|
114
|
+
| `sources` | `SourceConfig[]` | required | Array of source definitions |
|
|
115
|
+
| `dedup.known` | `() => string[]` | — | Returns hashes already in your DB (for cross-session dedup) |
|
|
116
|
+
| `digest` | `DigestOptions` | see below | Default digest settings |
|
|
117
|
+
| `requestTimeout` | `number` | `15000` | HTTP timeout in ms |
|
|
118
|
+
| `requestGap` | `number` | `1000` | Minimum ms between requests (rate limiting) |
|
|
119
|
+
| `maxItemsPerSource` | `number` | `50` | Cap articles returned from one source |
|
|
120
|
+
| `fetch` | `Function` | global fetch | Custom fetch for proxies/testing |
|
|
121
|
+
| `onError` | `Function` | — | Callback for per-source fetch or parse errors |
|
|
122
|
+
| `onWarning` | `Function` | — | Callback for non-fatal source diagnostics |
|
|
123
|
+
|
|
124
|
+
### `harvester.fetchAll()`
|
|
125
|
+
|
|
126
|
+
Fetches all enabled sources. Returns `Article[]`.
|
|
127
|
+
|
|
128
|
+
If one source fails, the method still returns articles from the remaining sources and reports the problem through `onError` when provided.
|
|
129
|
+
|
|
130
|
+
### `harvester.fetch(sourceId)`
|
|
131
|
+
|
|
132
|
+
Fetches a single source by ID.
|
|
133
|
+
|
|
134
|
+
### `harvester.fetchByTags(tags)`
|
|
135
|
+
|
|
136
|
+
Fetches sources matching any of the given tags.
|
|
137
|
+
|
|
138
|
+
### `harvester.digest(options?)`
|
|
139
|
+
|
|
140
|
+
The main event. Fetches all sources, then runs the compression pipeline:
|
|
141
|
+
|
|
142
|
+
1. **Dedup** — Groups similar headlines (Jaccard similarity) and keeps the richest version
|
|
143
|
+
2. **Sort** — Newest first
|
|
144
|
+
3. **Tag budget** — Caps articles per tag so no single region dominates
|
|
145
|
+
4. **Truncate** — Cuts content to N characters per article
|
|
146
|
+
5. **Token budget** — Trims from the bottom until under the token limit
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const { articles, stats } = await harvester.digest({
|
|
150
|
+
maxTokens: 12_000, // total token budget
|
|
151
|
+
maxArticlesPerTag: 10, // max articles per tag group
|
|
152
|
+
maxContentLength: 500, // chars per article content
|
|
153
|
+
similarityThreshold: 0.6, // title dedup threshold (0-1)
|
|
154
|
+
sort: "recency",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// stats.totalFetched → 700 (raw from all sources)
|
|
158
|
+
// stats.afterDedup → 200 (unique stories)
|
|
159
|
+
// stats.afterBudget → 80 (within tag limits)
|
|
160
|
+
// stats.estimatedTokens → 18000 (final token count)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `harvester.start(callbacks)` / `harvester.stop()`
|
|
164
|
+
|
|
165
|
+
Runs sources on their configured intervals. You handle storage.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
harvester.start({
|
|
169
|
+
onArticles: async (articles, source) => {
|
|
170
|
+
await db.insert("articles", articles);
|
|
171
|
+
console.log(`${articles.length} new from ${source.name}`);
|
|
172
|
+
},
|
|
173
|
+
onError: (err, source) => {
|
|
174
|
+
console.error(`${source.name} failed:`, err);
|
|
175
|
+
},
|
|
176
|
+
onWarning: (warning, source) => {
|
|
177
|
+
console.warn(`${source.name}: ${warning.code} - ${warning.message}`);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Later:
|
|
182
|
+
harvester.stop();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Article Schema
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface Article {
|
|
189
|
+
sourceId: string; // matches source config id
|
|
190
|
+
url: string; // canonical article URL
|
|
191
|
+
title: string;
|
|
192
|
+
content: string | null; // full text (when available)
|
|
193
|
+
summary: string | null; // short description
|
|
194
|
+
publishedAt: Date | null;
|
|
195
|
+
hash: string; // SHA-256 of URL (dedup key)
|
|
196
|
+
fetchedAt: Date;
|
|
197
|
+
tags: string[]; // inherited from source
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Dedup Across Sessions
|
|
202
|
+
|
|
203
|
+
The library handles within-batch dedup automatically. For cross-session dedup (don't re-process articles already in your DB), pass a `known` callback:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const harvester = createHarvester({
|
|
207
|
+
sources,
|
|
208
|
+
dedup: {
|
|
209
|
+
known: async () => {
|
|
210
|
+
const rows = await db.query("SELECT hash FROM articles");
|
|
211
|
+
return rows.map(r => r.hash);
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// fetchAll() now skips articles whose URL hash is already known
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Diagnostics
|
|
220
|
+
|
|
221
|
+
The library keeps the happy path simple: `fetchAll()` and `digest()` still return article data directly.
|
|
222
|
+
|
|
223
|
+
Use `onError` and `onWarning` if you want visibility into partial failures or weak-quality source output.
|
|
224
|
+
|
|
225
|
+
- `onError` covers hard failures like timeouts, HTTP errors, and parsing failures.
|
|
226
|
+
- `onWarning` covers non-fatal issues like empty source results, missing publication dates, or per-source truncation.
|
|
227
|
+
|
|
228
|
+
This matches the typical small-library OSS pattern: easy defaults, optional hooks for logging and monitoring.
|
|
229
|
+
|
|
230
|
+
## Scope and Limits
|
|
231
|
+
|
|
232
|
+
- RSS and HTML are first-class source types.
|
|
233
|
+
- HTML works best when you can define stable list selectors.
|
|
234
|
+
- The library does not execute page JavaScript or run a headless browser.
|
|
235
|
+
- The library does not decide what is relevant for your domain; apply your own filters downstream.
|
|
236
|
+
|
|
237
|
+
## Use with Next.js
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// app/api/feed/route.ts
|
|
241
|
+
import { createHarvester } from "osint-feed";
|
|
242
|
+
|
|
243
|
+
const harvester = createHarvester({ sources: [...] });
|
|
244
|
+
|
|
245
|
+
export async function GET() {
|
|
246
|
+
const { articles, stats } = await harvester.digest({ maxTokens: 8000 });
|
|
247
|
+
return Response.json({ articles, stats });
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Use with Express
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import express from "express";
|
|
255
|
+
import { createHarvester } from "osint-feed";
|
|
256
|
+
|
|
257
|
+
const app = express();
|
|
258
|
+
const harvester = createHarvester({ sources: [...] });
|
|
259
|
+
|
|
260
|
+
app.get("/digest", async (_req, res) => {
|
|
261
|
+
const result = await harvester.digest();
|
|
262
|
+
res.json(result);
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## How the Digest Math Works
|
|
267
|
+
|
|
268
|
+
Real numbers from a smoke test with 10 RSS + 3 HTML sources:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
Raw fetch: 324 articles
|
|
272
|
+
After title dedup: 319 unique stories
|
|
273
|
+
After tag budget: 47 (8 per tag, 6 tags)
|
|
274
|
+
Estimated tokens: 5,781
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
That's **1.8% of Llama 3's 128k context**. Plenty of room for system prompt, history, and reasoning.
|
|
278
|
+
|
|
279
|
+
With 35 sources polling every 15 min you'd get ~700 articles/hour. The digest pipeline compresses that to ~80 articles / ~18k tokens. Adjust `maxArticlesPerTag` and `maxTokens` to taste.
|
|
280
|
+
|
|
281
|
+
## Dependencies
|
|
282
|
+
|
|
283
|
+
Just two:
|
|
284
|
+
|
|
285
|
+
- [`cheerio`](https://github.com/cheeriojs/cheerio) — HTML parsing
|
|
286
|
+
- [`rss-parser`](https://github.com/rbren/rss-parser) — RSS/Atom parsing
|
|
287
|
+
|
|
288
|
+
No headless browsers. No native modules. No bloat.
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT
|
|
293
|
+
|
|
294
|
+
## Disclaimer
|
|
295
|
+
|
|
296
|
+
This library is a tool for fetching and parsing publicly available web content. Users are responsible for compliance with target websites' terms of service and applicable laws. The authors assume no liability for how the library is used.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
interface RssSourceConfig {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly type: "rss";
|
|
5
|
+
readonly url: string;
|
|
6
|
+
readonly tags: readonly string[];
|
|
7
|
+
/** Minutes between checks. Defaults to 15. */
|
|
8
|
+
readonly interval?: number;
|
|
9
|
+
/** Whether this source is active. Defaults to true. */
|
|
10
|
+
readonly enabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface HtmlSelectors {
|
|
13
|
+
/** Selector for the repeating article container element. */
|
|
14
|
+
readonly article: string;
|
|
15
|
+
/** Selector (within article) for the title text. */
|
|
16
|
+
readonly title: string;
|
|
17
|
+
/** Selector (within article) for the link href. */
|
|
18
|
+
readonly link: string;
|
|
19
|
+
/** Selector (within article) for the publication date. */
|
|
20
|
+
readonly date?: string;
|
|
21
|
+
/** Selector (within article) for summary/description text. */
|
|
22
|
+
readonly summary?: string;
|
|
23
|
+
}
|
|
24
|
+
interface HtmlSourceConfig {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly type: "html";
|
|
28
|
+
readonly url: string;
|
|
29
|
+
readonly tags: readonly string[];
|
|
30
|
+
readonly selectors: HtmlSelectors;
|
|
31
|
+
/** Minutes between checks. Defaults to 15. */
|
|
32
|
+
readonly interval?: number;
|
|
33
|
+
/** Whether this source is active. Defaults to true. */
|
|
34
|
+
readonly enabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
type SourceConfig = RssSourceConfig | HtmlSourceConfig;
|
|
37
|
+
type HarvesterWarningCode = "empty-html-result" | "empty-rss-result" | "missing-published-at" | "truncated-source";
|
|
38
|
+
interface HarvesterWarning {
|
|
39
|
+
readonly code: HarvesterWarningCode;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly details?: Readonly<Record<string, string | number | boolean>>;
|
|
42
|
+
}
|
|
43
|
+
interface Article {
|
|
44
|
+
readonly sourceId: string;
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly title: string;
|
|
47
|
+
readonly content: string | null;
|
|
48
|
+
readonly summary: string | null;
|
|
49
|
+
readonly publishedAt: Date | null;
|
|
50
|
+
/** SHA-256 hex hash of the url — stable dedup key. */
|
|
51
|
+
readonly hash: string;
|
|
52
|
+
readonly fetchedAt: Date;
|
|
53
|
+
readonly tags: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
interface DigestOptions {
|
|
56
|
+
/** Approximate upper bound of tokens in the output. Defaults to 12000. */
|
|
57
|
+
readonly maxTokens?: number;
|
|
58
|
+
/** Maximum articles per tag group. Defaults to 10. */
|
|
59
|
+
readonly maxArticlesPerTag?: number;
|
|
60
|
+
/** Maximum character length of content/summary per article. Defaults to 500. */
|
|
61
|
+
readonly maxContentLength?: number;
|
|
62
|
+
/** Sort strategy. Defaults to "recency". */
|
|
63
|
+
readonly sort?: "recency" | "relevance";
|
|
64
|
+
/**
|
|
65
|
+
* Jaccard similarity threshold (0–1) for title dedup.
|
|
66
|
+
* Titles more similar than this are considered duplicates.
|
|
67
|
+
* Defaults to 0.6.
|
|
68
|
+
*/
|
|
69
|
+
readonly similarityThreshold?: number;
|
|
70
|
+
}
|
|
71
|
+
interface DigestResult {
|
|
72
|
+
readonly articles: readonly Article[];
|
|
73
|
+
readonly stats: {
|
|
74
|
+
readonly totalFetched: number;
|
|
75
|
+
readonly afterDedup: number;
|
|
76
|
+
readonly afterBudget: number;
|
|
77
|
+
readonly estimatedTokens: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Callback returning hashes already known/stored — used for dedup. */
|
|
81
|
+
type KnownHashesFn = () => Promise<readonly string[]> | readonly string[];
|
|
82
|
+
interface HarvesterOptions {
|
|
83
|
+
readonly sources: readonly SourceConfig[];
|
|
84
|
+
/** Dedup configuration. */
|
|
85
|
+
readonly dedup?: {
|
|
86
|
+
/** Callback returning hashes already known/stored. */
|
|
87
|
+
readonly known?: KnownHashesFn;
|
|
88
|
+
};
|
|
89
|
+
/** Default digest options. Can be overridden per call. */
|
|
90
|
+
readonly digest?: DigestOptions;
|
|
91
|
+
/**
|
|
92
|
+
* Request timeout in milliseconds. Defaults to 15000.
|
|
93
|
+
*/
|
|
94
|
+
readonly requestTimeout?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Minimum gap between HTTP requests in milliseconds. Defaults to 1000.
|
|
97
|
+
*/
|
|
98
|
+
readonly requestGap?: number;
|
|
99
|
+
/** Maximum number of articles returned per source. Defaults to 50. */
|
|
100
|
+
readonly maxItemsPerSource?: number;
|
|
101
|
+
/**
|
|
102
|
+
* Custom fetch function. Defaults to global fetch.
|
|
103
|
+
* Useful for testing or proxying.
|
|
104
|
+
*/
|
|
105
|
+
readonly fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
106
|
+
/** Optional callback for per-source fetch or parse failures. */
|
|
107
|
+
readonly onError?: (error: unknown, source: SourceConfig) => void;
|
|
108
|
+
/** Optional callback for non-fatal source diagnostics. */
|
|
109
|
+
readonly onWarning?: (warning: HarvesterWarning, source: SourceConfig) => void;
|
|
110
|
+
}
|
|
111
|
+
interface SchedulerCallbacks {
|
|
112
|
+
readonly onArticles: (articles: readonly Article[], source: SourceConfig) => void | Promise<void>;
|
|
113
|
+
readonly onError?: (error: unknown, source: SourceConfig) => void;
|
|
114
|
+
readonly onWarning?: (warning: HarvesterWarning, source: SourceConfig) => void;
|
|
115
|
+
}
|
|
116
|
+
interface Harvester {
|
|
117
|
+
fetchAll(): Promise<Article[]>;
|
|
118
|
+
fetch(sourceId: string): Promise<Article[]>;
|
|
119
|
+
fetchByTags(tags: readonly string[]): Promise<Article[]>;
|
|
120
|
+
digest(options?: DigestOptions): Promise<DigestResult>;
|
|
121
|
+
start(callbacks: SchedulerCallbacks): void;
|
|
122
|
+
stop(): void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
declare const createHarvester: (options: HarvesterOptions) => Harvester;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Deduplicate articles by title similarity.
|
|
129
|
+
* When two articles are similar above the threshold, the one with more content wins.
|
|
130
|
+
*/
|
|
131
|
+
declare const dedup: (articles: readonly Article[], threshold: number) => Article[];
|
|
132
|
+
/**
|
|
133
|
+
* Full digest pipeline: dedup → sort → tag budget → truncate → token budget.
|
|
134
|
+
*/
|
|
135
|
+
declare const buildDigest: (articles: readonly Article[], options?: DigestOptions) => {
|
|
136
|
+
articles: Article[];
|
|
137
|
+
stats: {
|
|
138
|
+
totalFetched: number;
|
|
139
|
+
afterDedup: number;
|
|
140
|
+
afterBudget: number;
|
|
141
|
+
estimatedTokens: number;
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
interface FetchRssOptions {
|
|
146
|
+
readonly fetchFn?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
147
|
+
readonly timeout?: number;
|
|
148
|
+
readonly maxItems?: number;
|
|
149
|
+
readonly onWarning?: (warning: HarvesterWarning) => void;
|
|
150
|
+
}
|
|
151
|
+
declare const fetchRss: (source: RssSourceConfig, options?: FetchRssOptions) => Promise<Article[]>;
|
|
152
|
+
|
|
153
|
+
interface FetchHtmlOptions {
|
|
154
|
+
readonly fetchFn?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
155
|
+
readonly timeout?: number;
|
|
156
|
+
readonly maxItems?: number;
|
|
157
|
+
readonly onWarning?: (warning: HarvesterWarning) => void;
|
|
158
|
+
}
|
|
159
|
+
declare const fetchHtml: (source: HtmlSourceConfig, options?: FetchHtmlOptions) => Promise<Article[]>;
|
|
160
|
+
|
|
161
|
+
export { type Article, type DigestOptions, type DigestResult, type Harvester, type HarvesterOptions, type HarvesterWarning, type HarvesterWarningCode, type HtmlSelectors, type HtmlSourceConfig, type KnownHashesFn, type RssSourceConfig, type SchedulerCallbacks, type SourceConfig, buildDigest, createHarvester, dedup, fetchHtml, fetchRss };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
// src/rss.ts
|
|
2
|
+
import RssParser from "rss-parser";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
var hashUrl = (url) => createHash("sha256").update(url).digest("hex");
|
|
7
|
+
var tokenize = (text) => {
|
|
8
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\u00C0-\u024F\u0400-\u04FF]+/gi, " ").trim().split(/\s+/).filter((w) => w.length > 1);
|
|
9
|
+
return new Set(words);
|
|
10
|
+
};
|
|
11
|
+
var jaccardSimilarity = (a, b) => {
|
|
12
|
+
const setA = tokenize(a);
|
|
13
|
+
const setB = tokenize(b);
|
|
14
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
15
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
16
|
+
let intersection = 0;
|
|
17
|
+
for (const word of setA) {
|
|
18
|
+
if (setB.has(word)) intersection++;
|
|
19
|
+
}
|
|
20
|
+
const union = setA.size + setB.size - intersection;
|
|
21
|
+
return union === 0 ? 0 : intersection / union;
|
|
22
|
+
};
|
|
23
|
+
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
24
|
+
var normalizeText = (text) => text.replace(/\s+/g, " ").trim();
|
|
25
|
+
var truncate = (text, maxLength) => {
|
|
26
|
+
if (text.length <= maxLength) return text;
|
|
27
|
+
const cut = text.lastIndexOf(" ", maxLength);
|
|
28
|
+
return (cut > 0 ? text.slice(0, cut) : text.slice(0, maxLength)) + "...";
|
|
29
|
+
};
|
|
30
|
+
var ThrottleQueue = class {
|
|
31
|
+
constructor(gapMs) {
|
|
32
|
+
this.gapMs = gapMs;
|
|
33
|
+
}
|
|
34
|
+
lastRun = 0;
|
|
35
|
+
async run(fn) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const wait = Math.max(0, this.gapMs - (now - this.lastRun));
|
|
38
|
+
if (wait > 0) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
40
|
+
}
|
|
41
|
+
this.lastRun = Date.now();
|
|
42
|
+
return fn();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var resolveUrl = (href, base) => {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(href, base).href;
|
|
48
|
+
} catch {
|
|
49
|
+
return href;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/rss.ts
|
|
54
|
+
var DEFAULT_UA = "osint-feed/0.1 (+https://github.com/osint-feed)";
|
|
55
|
+
var createRssParser = () => new RssParser();
|
|
56
|
+
var fetchRss = async (source, options = {}) => {
|
|
57
|
+
const now = /* @__PURE__ */ new Date();
|
|
58
|
+
const {
|
|
59
|
+
fetchFn = globalThis.fetch,
|
|
60
|
+
timeout = 15e3,
|
|
61
|
+
maxItems = Number.POSITIVE_INFINITY,
|
|
62
|
+
onWarning
|
|
63
|
+
} = options;
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetchFn(source.url, {
|
|
68
|
+
headers: {
|
|
69
|
+
"User-Agent": DEFAULT_UA,
|
|
70
|
+
Accept: "application/rss+xml, application/xml, text/xml, */*"
|
|
71
|
+
},
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
throw new Error(`RSS fetch failed for ${source.id}: HTTP ${res.status}`);
|
|
76
|
+
}
|
|
77
|
+
const xml = await res.text();
|
|
78
|
+
const feed = await createRssParser().parseString(xml);
|
|
79
|
+
const totalItems = feed.items.length;
|
|
80
|
+
const articles = feedToArticles(feed, source, now, maxItems);
|
|
81
|
+
if (articles.length === 0) {
|
|
82
|
+
onWarning?.({
|
|
83
|
+
code: "empty-rss-result",
|
|
84
|
+
message: `RSS source '${source.id}' returned zero articles`
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (articles.some((article) => article.publishedAt === null)) {
|
|
88
|
+
const missingDates = articles.filter((article) => article.publishedAt === null).length;
|
|
89
|
+
onWarning?.({
|
|
90
|
+
code: "missing-published-at",
|
|
91
|
+
message: `RSS source '${source.id}' returned articles without publication dates`,
|
|
92
|
+
details: { missingDates, totalArticles: articles.length }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (Number.isFinite(maxItems) && totalItems > maxItems) {
|
|
96
|
+
onWarning?.({
|
|
97
|
+
code: "truncated-source",
|
|
98
|
+
message: `RSS source '${source.id}' was truncated to ${maxItems} articles`,
|
|
99
|
+
details: { maxItems, totalItems }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return articles;
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var feedToArticles = (feed, source, fetchedAt, maxItems) => {
|
|
108
|
+
const articles = [];
|
|
109
|
+
for (const item of feed.items) {
|
|
110
|
+
if (articles.length >= maxItems) break;
|
|
111
|
+
const url = item.link?.trim();
|
|
112
|
+
if (!url) continue;
|
|
113
|
+
const title = normalizeText(item.title ?? "");
|
|
114
|
+
if (!title) continue;
|
|
115
|
+
const content = item["content:encoded"] ?? item.content ?? null;
|
|
116
|
+
const summary = item.contentSnippet ?? item.summary ?? null;
|
|
117
|
+
let publishedAt = null;
|
|
118
|
+
if (item.isoDate) {
|
|
119
|
+
const d = new Date(item.isoDate);
|
|
120
|
+
if (!isNaN(d.getTime())) publishedAt = d;
|
|
121
|
+
} else if (item.pubDate) {
|
|
122
|
+
const d = new Date(item.pubDate);
|
|
123
|
+
if (!isNaN(d.getTime())) publishedAt = d;
|
|
124
|
+
}
|
|
125
|
+
articles.push({
|
|
126
|
+
sourceId: source.id,
|
|
127
|
+
url,
|
|
128
|
+
title,
|
|
129
|
+
content: typeof content === "string" ? normalizeText(content) : null,
|
|
130
|
+
summary: typeof summary === "string" ? normalizeText(summary) : null,
|
|
131
|
+
publishedAt,
|
|
132
|
+
hash: hashUrl(url),
|
|
133
|
+
fetchedAt,
|
|
134
|
+
tags: [...source.tags]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return articles;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/html.ts
|
|
141
|
+
import * as cheerio from "cheerio";
|
|
142
|
+
var DEFAULT_UA2 = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
143
|
+
var parsePublishedAt = ($el, selector) => {
|
|
144
|
+
if (!selector) return null;
|
|
145
|
+
const dateEl = $el.find(selector).first();
|
|
146
|
+
const candidates = [
|
|
147
|
+
dateEl.attr("datetime"),
|
|
148
|
+
dateEl.attr("content"),
|
|
149
|
+
dateEl.text()
|
|
150
|
+
];
|
|
151
|
+
for (const candidate of candidates) {
|
|
152
|
+
const raw = normalizeText(candidate ?? "");
|
|
153
|
+
if (!raw) continue;
|
|
154
|
+
const parsed = new Date(raw);
|
|
155
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
};
|
|
161
|
+
var parseHtml = (html, source, fetchedAt = /* @__PURE__ */ new Date(), maxItems = Number.POSITIVE_INFINITY) => {
|
|
162
|
+
const $ = cheerio.load(html);
|
|
163
|
+
const { selectors } = source;
|
|
164
|
+
const articles = [];
|
|
165
|
+
$(selectors.article).each((_i, el) => {
|
|
166
|
+
if (articles.length >= maxItems) return false;
|
|
167
|
+
const $el = $(el);
|
|
168
|
+
const title = normalizeText($el.find(selectors.title).first().text());
|
|
169
|
+
if (!title) return;
|
|
170
|
+
const rawHref = normalizeText($el.find(selectors.link).first().attr("href") ?? "");
|
|
171
|
+
if (!rawHref) return;
|
|
172
|
+
const url = resolveUrl(rawHref, source.url);
|
|
173
|
+
const publishedAt = parsePublishedAt($el, selectors.date);
|
|
174
|
+
const summary = selectors.summary ? normalizeText($el.find(selectors.summary).first().text()) || null : null;
|
|
175
|
+
articles.push({
|
|
176
|
+
sourceId: source.id,
|
|
177
|
+
url,
|
|
178
|
+
title,
|
|
179
|
+
content: null,
|
|
180
|
+
summary,
|
|
181
|
+
publishedAt,
|
|
182
|
+
hash: hashUrl(url),
|
|
183
|
+
fetchedAt,
|
|
184
|
+
tags: [...source.tags]
|
|
185
|
+
});
|
|
186
|
+
return void 0;
|
|
187
|
+
});
|
|
188
|
+
return articles;
|
|
189
|
+
};
|
|
190
|
+
var fetchHtml = async (source, options = {}) => {
|
|
191
|
+
const {
|
|
192
|
+
fetchFn = globalThis.fetch,
|
|
193
|
+
timeout = 15e3,
|
|
194
|
+
maxItems = Number.POSITIVE_INFINITY,
|
|
195
|
+
onWarning
|
|
196
|
+
} = options;
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
199
|
+
let html;
|
|
200
|
+
try {
|
|
201
|
+
const res = await fetchFn(source.url, {
|
|
202
|
+
headers: {
|
|
203
|
+
"User-Agent": DEFAULT_UA2,
|
|
204
|
+
Accept: "text/html, application/xhtml+xml, */*"
|
|
205
|
+
},
|
|
206
|
+
signal: controller.signal
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
throw new Error(`HTML fetch failed for ${source.id}: HTTP ${res.status}`);
|
|
210
|
+
}
|
|
211
|
+
html = await res.text();
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timer);
|
|
214
|
+
}
|
|
215
|
+
const totalMatches = cheerio.load(html)(source.selectors.article).length;
|
|
216
|
+
const articles = parseHtml(html, source, /* @__PURE__ */ new Date(), maxItems);
|
|
217
|
+
if (articles.length === 0) {
|
|
218
|
+
onWarning?.({
|
|
219
|
+
code: "empty-html-result",
|
|
220
|
+
message: `HTML source '${source.id}' returned zero articles`
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (articles.some((article) => article.publishedAt === null)) {
|
|
224
|
+
const missingDates = articles.filter((article) => article.publishedAt === null).length;
|
|
225
|
+
onWarning?.({
|
|
226
|
+
code: "missing-published-at",
|
|
227
|
+
message: `HTML source '${source.id}' returned articles without publication dates`,
|
|
228
|
+
details: { missingDates, totalArticles: articles.length }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (Number.isFinite(maxItems) && totalMatches > maxItems) {
|
|
232
|
+
onWarning?.({
|
|
233
|
+
code: "truncated-source",
|
|
234
|
+
message: `HTML source '${source.id}' was truncated to ${maxItems} articles`,
|
|
235
|
+
details: { maxItems, totalMatches }
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return articles;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/digest.ts
|
|
242
|
+
var DEFAULT_DIGEST = {
|
|
243
|
+
maxTokens: 12e3,
|
|
244
|
+
maxArticlesPerTag: 10,
|
|
245
|
+
maxContentLength: 500,
|
|
246
|
+
sort: "recency",
|
|
247
|
+
similarityThreshold: 0.6
|
|
248
|
+
};
|
|
249
|
+
var dedup = (articles, threshold) => {
|
|
250
|
+
const kept = [];
|
|
251
|
+
for (const article of articles) {
|
|
252
|
+
const isDuplicate = kept.some(
|
|
253
|
+
(existing) => jaccardSimilarity(existing.title, article.title) >= threshold
|
|
254
|
+
);
|
|
255
|
+
if (!isDuplicate) {
|
|
256
|
+
kept.push({ ...article });
|
|
257
|
+
} else {
|
|
258
|
+
const existingIdx = kept.findIndex(
|
|
259
|
+
(existing) => jaccardSimilarity(existing.title, article.title) >= threshold
|
|
260
|
+
);
|
|
261
|
+
if (existingIdx !== -1) {
|
|
262
|
+
const existing = kept[existingIdx];
|
|
263
|
+
const existingLen = (existing.content?.length ?? 0) + (existing.summary?.length ?? 0);
|
|
264
|
+
const newLen = (article.content?.length ?? 0) + (article.summary?.length ?? 0);
|
|
265
|
+
if (newLen > existingLen) {
|
|
266
|
+
kept[existingIdx] = { ...article };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return kept;
|
|
272
|
+
};
|
|
273
|
+
var applyTagBudget = (articles, maxPerTag) => {
|
|
274
|
+
const tagCounts = /* @__PURE__ */ new Map();
|
|
275
|
+
const result = [];
|
|
276
|
+
for (const article of articles) {
|
|
277
|
+
const tags = article.tags.length > 0 ? article.tags : ["_untagged"];
|
|
278
|
+
let allowed = false;
|
|
279
|
+
for (const tag of tags) {
|
|
280
|
+
const count = tagCounts.get(tag) ?? 0;
|
|
281
|
+
if (count < maxPerTag) {
|
|
282
|
+
allowed = true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (allowed) {
|
|
286
|
+
result.push(article);
|
|
287
|
+
for (const tag of tags) {
|
|
288
|
+
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return result;
|
|
293
|
+
};
|
|
294
|
+
var truncateArticles = (articles, maxContentLength) => articles.map((a) => ({
|
|
295
|
+
...a,
|
|
296
|
+
content: a.content ? truncate(a.content, maxContentLength) : null,
|
|
297
|
+
summary: a.summary ? truncate(a.summary, maxContentLength) : null
|
|
298
|
+
}));
|
|
299
|
+
var sortArticles = (articles, sort) => {
|
|
300
|
+
if (sort === "recency") {
|
|
301
|
+
return [...articles].sort((a, b) => {
|
|
302
|
+
const dateA = a.publishedAt?.getTime() ?? a.fetchedAt.getTime();
|
|
303
|
+
const dateB = b.publishedAt?.getTime() ?? b.fetchedAt.getTime();
|
|
304
|
+
return dateB - dateA;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return [...articles].sort((a, b) => {
|
|
308
|
+
const dateA = a.publishedAt?.getTime() ?? a.fetchedAt.getTime();
|
|
309
|
+
const dateB = b.publishedAt?.getTime() ?? b.fetchedAt.getTime();
|
|
310
|
+
return dateB - dateA;
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
var applyTokenBudget = (articles, maxTokens) => {
|
|
314
|
+
let totalTokens = 0;
|
|
315
|
+
const result = [];
|
|
316
|
+
for (const article of articles) {
|
|
317
|
+
const text = [article.title, article.summary, article.content].filter(Boolean).join(" ");
|
|
318
|
+
const tokens = estimateTokens(text);
|
|
319
|
+
if (totalTokens + tokens > maxTokens && result.length > 0) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
totalTokens += tokens;
|
|
323
|
+
result.push(article);
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
};
|
|
327
|
+
var buildDigest = (articles, options) => {
|
|
328
|
+
const opts = { ...DEFAULT_DIGEST, ...options };
|
|
329
|
+
const totalFetched = articles.length;
|
|
330
|
+
let result = dedup(articles, opts.similarityThreshold);
|
|
331
|
+
const afterDedup = result.length;
|
|
332
|
+
result = sortArticles(result, opts.sort);
|
|
333
|
+
result = applyTagBudget(result, opts.maxArticlesPerTag);
|
|
334
|
+
const afterBudget = result.length;
|
|
335
|
+
result = truncateArticles(result, opts.maxContentLength);
|
|
336
|
+
result = applyTokenBudget(result, opts.maxTokens);
|
|
337
|
+
const estimatedTokens = result.reduce((sum, a) => {
|
|
338
|
+
const text = [a.title, a.summary, a.content].filter(Boolean).join(" ");
|
|
339
|
+
return sum + estimateTokens(text);
|
|
340
|
+
}, 0);
|
|
341
|
+
return {
|
|
342
|
+
articles: result,
|
|
343
|
+
stats: { totalFetched, afterDedup, afterBudget, estimatedTokens }
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/scheduler.ts
|
|
348
|
+
var createScheduler = (sources, fetchSource2, callbacks) => {
|
|
349
|
+
const entries = [];
|
|
350
|
+
for (const source of sources) {
|
|
351
|
+
if (source.enabled === false) continue;
|
|
352
|
+
const intervalMs = (source.interval ?? 15) * 6e4;
|
|
353
|
+
const tick = async () => {
|
|
354
|
+
try {
|
|
355
|
+
const articles = await fetchSource2(source);
|
|
356
|
+
if (articles.length > 0) {
|
|
357
|
+
await callbacks.onArticles(articles, source);
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
callbacks.onError?.(error, source);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
void tick();
|
|
364
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
365
|
+
entries.push({ source, timer });
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
stop: () => {
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
clearInterval(entry.timer);
|
|
371
|
+
}
|
|
372
|
+
entries.length = 0;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/harvester.ts
|
|
378
|
+
var fetchSource = async (source, throttle, options = {}) => {
|
|
379
|
+
return throttle.run(async () => {
|
|
380
|
+
if (source.type === "rss") {
|
|
381
|
+
const rssOptions = {
|
|
382
|
+
...options.fetchFn ? { fetchFn: options.fetchFn } : {},
|
|
383
|
+
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
384
|
+
...options.maxItemsPerSource !== void 0 ? { maxItems: options.maxItemsPerSource } : {},
|
|
385
|
+
...options.onWarning ? { onWarning: (warning) => options.onWarning?.(warning, source) } : {}
|
|
386
|
+
};
|
|
387
|
+
return fetchRss(source, rssOptions);
|
|
388
|
+
}
|
|
389
|
+
const htmlOptions = {
|
|
390
|
+
...options.fetchFn ? { fetchFn: options.fetchFn } : {},
|
|
391
|
+
...options.timeout !== void 0 ? { timeout: options.timeout } : {},
|
|
392
|
+
...options.maxItemsPerSource !== void 0 ? { maxItems: options.maxItemsPerSource } : {},
|
|
393
|
+
...options.onWarning ? { onWarning: (warning) => options.onWarning?.(warning, source) } : {}
|
|
394
|
+
};
|
|
395
|
+
return fetchHtml(source, htmlOptions);
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
var filterKnown = async (articles, knownFn) => {
|
|
399
|
+
if (!knownFn) return articles;
|
|
400
|
+
const known = new Set(await knownFn());
|
|
401
|
+
return articles.filter((a) => !known.has(a.hash));
|
|
402
|
+
};
|
|
403
|
+
var createHarvester = (options) => {
|
|
404
|
+
const {
|
|
405
|
+
sources,
|
|
406
|
+
dedup: dedupOpts,
|
|
407
|
+
digest: defaultDigestOpts,
|
|
408
|
+
requestTimeout = 15e3,
|
|
409
|
+
requestGap = 1e3,
|
|
410
|
+
maxItemsPerSource = 50,
|
|
411
|
+
onError,
|
|
412
|
+
onWarning
|
|
413
|
+
} = options;
|
|
414
|
+
const fetchFn = options.fetch;
|
|
415
|
+
const throttle = new ThrottleQueue(requestGap);
|
|
416
|
+
let scheduler = null;
|
|
417
|
+
const getEnabledSources = () => sources.filter((s) => s.enabled !== false);
|
|
418
|
+
const fetchSingle = async (source) => {
|
|
419
|
+
const fetchOptions = {
|
|
420
|
+
timeout: requestTimeout,
|
|
421
|
+
maxItemsPerSource,
|
|
422
|
+
...fetchFn ? { fetchFn } : {},
|
|
423
|
+
...onWarning ? { onWarning } : {}
|
|
424
|
+
};
|
|
425
|
+
const articles = await fetchSource(source, throttle, fetchOptions);
|
|
426
|
+
return filterKnown(articles, dedupOpts?.known);
|
|
427
|
+
};
|
|
428
|
+
const fetchMany = async (selectedSources) => {
|
|
429
|
+
const results = [];
|
|
430
|
+
for (const source of selectedSources) {
|
|
431
|
+
try {
|
|
432
|
+
const articles = await fetchSingle(source);
|
|
433
|
+
results.push(...articles);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
onError?.(error, source);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return results;
|
|
439
|
+
};
|
|
440
|
+
return {
|
|
441
|
+
async fetchAll() {
|
|
442
|
+
return fetchMany(getEnabledSources());
|
|
443
|
+
},
|
|
444
|
+
async fetch(sourceId) {
|
|
445
|
+
const source = sources.find((s) => s.id === sourceId);
|
|
446
|
+
if (!source) {
|
|
447
|
+
throw new Error(`Source not found: ${sourceId}`);
|
|
448
|
+
}
|
|
449
|
+
return fetchSingle(source);
|
|
450
|
+
},
|
|
451
|
+
async fetchByTags(tags) {
|
|
452
|
+
const tagSet = new Set(tags);
|
|
453
|
+
const matching = getEnabledSources().filter(
|
|
454
|
+
(s) => s.tags.some((t) => tagSet.has(t))
|
|
455
|
+
);
|
|
456
|
+
return fetchMany(matching);
|
|
457
|
+
},
|
|
458
|
+
async digest(overrides) {
|
|
459
|
+
const articles = await fetchMany(getEnabledSources());
|
|
460
|
+
const opts = { ...defaultDigestOpts, ...overrides };
|
|
461
|
+
const result = buildDigest(articles, opts);
|
|
462
|
+
return {
|
|
463
|
+
articles: result.articles,
|
|
464
|
+
stats: result.stats
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
start(callbacks) {
|
|
468
|
+
if (scheduler) {
|
|
469
|
+
scheduler.stop();
|
|
470
|
+
}
|
|
471
|
+
const schedulerCallbacks = {
|
|
472
|
+
onArticles: callbacks.onArticles,
|
|
473
|
+
...callbacks.onError ?? onError ? { onError: callbacks.onError ?? onError } : {},
|
|
474
|
+
...callbacks.onWarning ?? onWarning ? { onWarning: callbacks.onWarning ?? onWarning } : {}
|
|
475
|
+
};
|
|
476
|
+
scheduler = createScheduler(
|
|
477
|
+
getEnabledSources(),
|
|
478
|
+
(source) => fetchSingle(source),
|
|
479
|
+
schedulerCallbacks
|
|
480
|
+
);
|
|
481
|
+
},
|
|
482
|
+
stop() {
|
|
483
|
+
scheduler?.stop();
|
|
484
|
+
scheduler = null;
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
export {
|
|
489
|
+
buildDigest,
|
|
490
|
+
createHarvester,
|
|
491
|
+
dedup,
|
|
492
|
+
fetchHtml,
|
|
493
|
+
fetchRss
|
|
494
|
+
};
|
|
495
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rss.ts","../src/utils.ts","../src/html.ts","../src/digest.ts","../src/scheduler.ts","../src/harvester.ts"],"sourcesContent":["import RssParser from \"rss-parser\";\nimport type { Article, HarvesterWarning, RssSourceConfig } from \"./types.js\";\nimport { hashUrl, normalizeText } from \"./utils.js\";\n\nconst DEFAULT_UA = \"osint-feed/0.1 (+https://github.com/osint-feed)\";\n\nconst createRssParser = (): RssParser => new RssParser();\n\ninterface FetchRssOptions {\n readonly fetchFn?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n readonly timeout?: number;\n readonly maxItems?: number;\n readonly onWarning?: (warning: HarvesterWarning) => void;\n}\n\nexport const fetchRss = async (\n source: RssSourceConfig,\n options: FetchRssOptions = {},\n): Promise<Article[]> => {\n const now = new Date();\n const {\n fetchFn = globalThis.fetch,\n timeout = 15_000,\n maxItems = Number.POSITIVE_INFINITY,\n onWarning,\n } = options;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n try {\n const res = await fetchFn(source.url, {\n headers: {\n \"User-Agent\": DEFAULT_UA,\n Accept: \"application/rss+xml, application/xml, text/xml, */*\",\n },\n signal: controller.signal,\n });\n\n if (!res.ok) {\n throw new Error(`RSS fetch failed for ${source.id}: HTTP ${res.status}`);\n }\n\n const xml = await res.text();\n const feed = await createRssParser().parseString(xml);\n const totalItems = feed.items.length;\n const articles = feedToArticles(feed, source, now, maxItems);\n\n if (articles.length === 0) {\n onWarning?.({\n code: \"empty-rss-result\",\n message: `RSS source '${source.id}' returned zero articles`,\n });\n }\n\n if (articles.some((article) => article.publishedAt === null)) {\n const missingDates = articles.filter((article) => article.publishedAt === null).length;\n onWarning?.({\n code: \"missing-published-at\",\n message: `RSS source '${source.id}' returned articles without publication dates`,\n details: { missingDates, totalArticles: articles.length },\n });\n }\n\n if (Number.isFinite(maxItems) && totalItems > maxItems) {\n onWarning?.({\n code: \"truncated-source\",\n message: `RSS source '${source.id}' was truncated to ${maxItems} articles`,\n details: { maxItems, totalItems },\n });\n }\n\n return articles;\n } finally {\n clearTimeout(timer);\n }\n};\n\nconst feedToArticles = (\n feed: RssParser.Output<Record<string, unknown>>,\n source: RssSourceConfig,\n fetchedAt: Date,\n maxItems: number,\n): Article[] => {\n const articles: Article[] = [];\n\n for (const item of feed.items) {\n if (articles.length >= maxItems) break;\n\n const url = item.link?.trim();\n if (!url) continue;\n\n const title = normalizeText(item.title ?? \"\");\n if (!title) continue;\n\n const content = item[\"content:encoded\"] as string | undefined\n ?? item.content\n ?? null;\n\n const summary = item.contentSnippet\n ?? item.summary\n ?? null;\n\n let publishedAt: Date | null = null;\n if (item.isoDate) {\n const d = new Date(item.isoDate);\n if (!isNaN(d.getTime())) publishedAt = d;\n } else if (item.pubDate) {\n const d = new Date(item.pubDate);\n if (!isNaN(d.getTime())) publishedAt = d;\n }\n\n articles.push({\n sourceId: source.id,\n url,\n title,\n content: typeof content === \"string\" ? normalizeText(content) : null,\n summary: typeof summary === \"string\" ? normalizeText(summary) : null,\n publishedAt,\n hash: hashUrl(url),\n fetchedAt,\n tags: [...source.tags],\n });\n }\n\n return articles;\n};\n","import { createHash } from \"node:crypto\";\n\n/** SHA-256 hex hash of a string. */\nexport const hashUrl = (url: string): string =>\n createHash(\"sha256\").update(url).digest(\"hex\");\n\nconst tokenize = (text: string): Set<string> => {\n const words = text\n .toLowerCase()\n .replace(/[^a-z0-9\\u00C0-\\u024F\\u0400-\\u04FF]+/gi, \" \")\n .trim()\n .split(/\\s+/)\n .filter((w) => w.length > 1);\n return new Set(words);\n};\n\n/**\n * Jaccard similarity between two strings (0–1).\n * Used for title-based dedup.\n */\nexport const jaccardSimilarity = (a: string, b: string): number => {\n const setA = tokenize(a);\n const setB = tokenize(b);\n if (setA.size === 0 && setB.size === 0) return 1;\n if (setA.size === 0 || setB.size === 0) return 0;\n\n let intersection = 0;\n for (const word of setA) {\n if (setB.has(word)) intersection++;\n }\n const union = setA.size + setB.size - intersection;\n return union === 0 ? 0 : intersection / union;\n};\n\n/**\n * Rough token count estimate. ~4 chars per token for English, slightly more for\n * non-Latin scripts. Good enough for budget estimation without a tokenizer dep.\n */\nexport const estimateTokens = (text: string): number =>\n Math.ceil(text.length / 4);\n\nexport const normalizeText = (text: string): string =>\n text.replace(/\\s+/g, \" \").trim();\n\n/**\n * Truncate a string to maxLength characters, breaking at a word boundary.\n */\nexport const truncate = (text: string, maxLength: number): string => {\n if (text.length <= maxLength) return text;\n const cut = text.lastIndexOf(\" \", maxLength);\n return (cut > 0 ? text.slice(0, cut) : text.slice(0, maxLength)) + \"...\";\n};\n\nexport class ThrottleQueue {\n private lastRun = 0;\n\n constructor(private readonly gapMs: number) {}\n\n async run<T>(fn: () => Promise<T>): Promise<T> {\n const now = Date.now();\n const wait = Math.max(0, this.gapMs - (now - this.lastRun));\n if (wait > 0) {\n await new Promise((resolve) => setTimeout(resolve, wait));\n }\n this.lastRun = Date.now();\n return fn();\n }\n}\n\nexport const resolveUrl = (href: string, base: string): string => {\n try {\n return new URL(href, base).href;\n } catch {\n return href;\n }\n};\n","import * as cheerio from \"cheerio\";\nimport type { AnyNode } from \"domhandler\";\nimport type { Article, HarvesterWarning, HtmlSourceConfig } from \"./types.js\";\nimport { hashUrl, normalizeText, resolveUrl } from \"./utils.js\";\n\n/**\n * Many government / military sites block non-browser user agents, so we default\n * to a real-looking UA. Consumers can override via HarvesterOptions.fetch.\n */\nconst DEFAULT_UA =\n \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36\";\n\ninterface FetchHtmlOptions {\n readonly fetchFn?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n readonly timeout?: number;\n readonly maxItems?: number;\n readonly onWarning?: (warning: HarvesterWarning) => void;\n}\n\nconst parsePublishedAt = ($el: cheerio.Cheerio<AnyNode>, selector?: string): Date | null => {\n if (!selector) return null;\n\n const dateEl = $el.find(selector).first();\n const candidates = [\n dateEl.attr(\"datetime\"),\n dateEl.attr(\"content\"),\n dateEl.text(),\n ];\n\n for (const candidate of candidates) {\n const raw = normalizeText(candidate ?? \"\");\n if (!raw) continue;\n const parsed = new Date(raw);\n if (!Number.isNaN(parsed.getTime())) {\n return parsed;\n }\n }\n\n return null;\n};\n\nexport const parseHtml = (\n html: string,\n source: HtmlSourceConfig,\n fetchedAt = new Date(),\n maxItems = Number.POSITIVE_INFINITY,\n): Article[] => {\n const $ = cheerio.load(html);\n const { selectors } = source;\n const articles: Article[] = [];\n\n $(selectors.article).each((_i, el) => {\n if (articles.length >= maxItems) return false;\n\n const $el = $(el);\n const title = normalizeText($el.find(selectors.title).first().text());\n if (!title) return;\n\n const rawHref = normalizeText($el.find(selectors.link).first().attr(\"href\") ?? \"\");\n if (!rawHref) return;\n\n const url = resolveUrl(rawHref, source.url);\n const publishedAt = parsePublishedAt($el, selectors.date);\n const summary = selectors.summary\n ? normalizeText($el.find(selectors.summary).first().text()) || null\n : null;\n\n articles.push({\n sourceId: source.id,\n url,\n title,\n content: null,\n summary,\n publishedAt,\n hash: hashUrl(url),\n fetchedAt,\n tags: [...source.tags],\n });\n\n return undefined;\n });\n\n return articles;\n};\n\nexport const fetchHtml = async (\n source: HtmlSourceConfig,\n options: FetchHtmlOptions = {},\n): Promise<Article[]> => {\n const {\n fetchFn = globalThis.fetch,\n timeout = 15_000,\n maxItems = Number.POSITIVE_INFINITY,\n onWarning,\n } = options;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n let html: string;\n try {\n const res = await fetchFn(source.url, {\n headers: {\n \"User-Agent\": DEFAULT_UA,\n Accept: \"text/html, application/xhtml+xml, */*\",\n },\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new Error(`HTML fetch failed for ${source.id}: HTTP ${res.status}`);\n }\n html = await res.text();\n } finally {\n clearTimeout(timer);\n }\n\n const totalMatches = cheerio.load(html)(source.selectors.article).length;\n const articles = parseHtml(html, source, new Date(), maxItems);\n\n if (articles.length === 0) {\n onWarning?.({\n code: \"empty-html-result\",\n message: `HTML source '${source.id}' returned zero articles`,\n });\n }\n\n if (articles.some((article) => article.publishedAt === null)) {\n const missingDates = articles.filter((article) => article.publishedAt === null).length;\n onWarning?.({\n code: \"missing-published-at\",\n message: `HTML source '${source.id}' returned articles without publication dates`,\n details: { missingDates, totalArticles: articles.length },\n });\n }\n\n if (Number.isFinite(maxItems) && totalMatches > maxItems) {\n onWarning?.({\n code: \"truncated-source\",\n message: `HTML source '${source.id}' was truncated to ${maxItems} articles`,\n details: { maxItems, totalMatches },\n });\n }\n\n return articles;\n};\n","import type { Article, DigestOptions } from \"./types.js\";\nimport { estimateTokens, jaccardSimilarity, truncate } from \"./utils.js\";\n\nconst DEFAULT_DIGEST: Required<DigestOptions> = {\n maxTokens: 12_000,\n maxArticlesPerTag: 10,\n maxContentLength: 500,\n sort: \"recency\",\n similarityThreshold: 0.6,\n};\n\n/**\n * Deduplicate articles by title similarity.\n * When two articles are similar above the threshold, the one with more content wins.\n */\nexport const dedup = (\n articles: readonly Article[],\n threshold: number,\n): Article[] => {\n const kept: Article[] = [];\n\n for (const article of articles) {\n const isDuplicate = kept.some(\n (existing) => jaccardSimilarity(existing.title, article.title) >= threshold,\n );\n if (!isDuplicate) {\n kept.push({ ...article });\n } else {\n const existingIdx = kept.findIndex(\n (existing) => jaccardSimilarity(existing.title, article.title) >= threshold,\n );\n if (existingIdx !== -1) {\n const existing = kept[existingIdx]!;\n const existingLen = (existing.content?.length ?? 0) + (existing.summary?.length ?? 0);\n const newLen = (article.content?.length ?? 0) + (article.summary?.length ?? 0);\n if (newLen > existingLen) {\n kept[existingIdx] = { ...article };\n }\n }\n }\n }\n\n return kept;\n};\n\n/**\n * Apply tag-based budget limits — keep at most N articles per tag group.\n */\nconst applyTagBudget = (\n articles: Article[],\n maxPerTag: number,\n): Article[] => {\n const tagCounts = new Map<string, number>();\n const result: Article[] = [];\n\n for (const article of articles) {\n // An article passes if at least one of its tags hasn't exceeded budget\n const tags = article.tags.length > 0 ? article.tags : [\"_untagged\"];\n let allowed = false;\n\n for (const tag of tags) {\n const count = tagCounts.get(tag) ?? 0;\n if (count < maxPerTag) {\n allowed = true;\n }\n }\n\n if (allowed) {\n result.push(article);\n for (const tag of tags) {\n tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);\n }\n }\n }\n\n return result;\n};\n\n/**\n * Truncate article content/summary to fit within character limits.\n */\nconst truncateArticles = (\n articles: Article[],\n maxContentLength: number,\n): Article[] =>\n articles.map((a) => ({\n ...a,\n content: a.content ? truncate(a.content, maxContentLength) : null,\n summary: a.summary ? truncate(a.summary, maxContentLength) : null,\n }));\n\n/**\n * Sort articles by the chosen strategy.\n */\nconst sortArticles = (\n articles: Article[],\n sort: \"recency\" | \"relevance\",\n): Article[] => {\n if (sort === \"recency\") {\n return [...articles].sort((a, b) => {\n const dateA = a.publishedAt?.getTime() ?? a.fetchedAt.getTime();\n const dateB = b.publishedAt?.getTime() ?? b.fetchedAt.getTime();\n return dateB - dateA;\n });\n }\n // \"relevance\" — for now same as recency; could weight by content length / source priority later\n return [...articles].sort((a, b) => {\n const dateA = a.publishedAt?.getTime() ?? a.fetchedAt.getTime();\n const dateB = b.publishedAt?.getTime() ?? b.fetchedAt.getTime();\n return dateB - dateA;\n });\n};\n\n/**\n * Apply token budget — trim articles from the end until we're under budget.\n */\nconst applyTokenBudget = (\n articles: Article[],\n maxTokens: number,\n): Article[] => {\n let totalTokens = 0;\n const result: Article[] = [];\n\n for (const article of articles) {\n const text = [article.title, article.summary, article.content]\n .filter(Boolean)\n .join(\" \");\n const tokens = estimateTokens(text);\n\n if (totalTokens + tokens > maxTokens && result.length > 0) {\n break;\n }\n\n totalTokens += tokens;\n result.push(article);\n }\n\n return result;\n};\n\n/**\n * Full digest pipeline: dedup → sort → tag budget → truncate → token budget.\n */\nexport const buildDigest = (\n articles: readonly Article[],\n options?: DigestOptions,\n): { articles: Article[]; stats: { totalFetched: number; afterDedup: number; afterBudget: number; estimatedTokens: number } } => {\n const opts = { ...DEFAULT_DIGEST, ...options };\n const totalFetched = articles.length;\n\n let result = dedup(articles, opts.similarityThreshold);\n const afterDedup = result.length;\n\n result = sortArticles(result, opts.sort);\n\n result = applyTagBudget(result, opts.maxArticlesPerTag);\n const afterBudget = result.length;\n\n result = truncateArticles(result, opts.maxContentLength);\n\n result = applyTokenBudget(result, opts.maxTokens);\n\n const estimatedTokens = result.reduce((sum, a) => {\n const text = [a.title, a.summary, a.content].filter(Boolean).join(\" \");\n return sum + estimateTokens(text);\n }, 0);\n\n return {\n articles: result,\n stats: { totalFetched, afterDedup, afterBudget, estimatedTokens },\n };\n};\n","import type { SchedulerCallbacks, SourceConfig, Article } from \"./types.js\";\n\ninterface SchedulerEntry {\n source: SourceConfig;\n timer: ReturnType<typeof setInterval>;\n}\n\nexport const createScheduler = (\n sources: readonly SourceConfig[],\n fetchSource: (source: SourceConfig) => Promise<Article[]>,\n callbacks: SchedulerCallbacks,\n): { stop: () => void } => {\n const entries: SchedulerEntry[] = [];\n\n for (const source of sources) {\n if (source.enabled === false) continue;\n\n const intervalMs = (source.interval ?? 15) * 60_000;\n\n const tick = async (): Promise<void> => {\n try {\n const articles = await fetchSource(source);\n if (articles.length > 0) {\n await callbacks.onArticles(articles, source);\n }\n } catch (error) {\n callbacks.onError?.(error, source);\n }\n };\n\n void tick();\n\n const timer = setInterval(() => void tick(), intervalMs);\n entries.push({ source, timer });\n }\n\n return {\n stop: () => {\n for (const entry of entries) {\n clearInterval(entry.timer);\n }\n entries.length = 0;\n },\n };\n};\n","import type {\n Article,\n DigestOptions,\n DigestResult,\n Harvester,\n HarvesterOptions,\n HarvesterWarning,\n SchedulerCallbacks,\n SourceConfig,\n} from \"./types.js\";\nimport { fetchRss } from \"./rss.js\";\nimport { fetchHtml } from \"./html.js\";\nimport { buildDigest } from \"./digest.js\";\nimport { createScheduler } from \"./scheduler.js\";\nimport { ThrottleQueue } from \"./utils.js\";\n\ntype FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\nconst fetchSource = async (\n source: SourceConfig,\n throttle: ThrottleQueue,\n options: {\n readonly fetchFn?: FetchFn;\n readonly timeout?: number;\n readonly maxItemsPerSource?: number;\n readonly onWarning?: (warning: HarvesterWarning, source: SourceConfig) => void;\n } = {},\n): Promise<Article[]> => {\n return throttle.run(async () => {\n if (source.type === \"rss\") {\n const rssOptions: Parameters<typeof fetchRss>[1] = {\n ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),\n ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),\n ...(options.maxItemsPerSource !== undefined ? { maxItems: options.maxItemsPerSource } : {}),\n ...(options.onWarning\n ? { onWarning: (warning: HarvesterWarning) => options.onWarning?.(warning, source) }\n : {}),\n };\n return fetchRss(source, rssOptions);\n }\n const htmlOptions: Parameters<typeof fetchHtml>[1] = {\n ...(options.fetchFn ? { fetchFn: options.fetchFn } : {}),\n ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),\n ...(options.maxItemsPerSource !== undefined ? { maxItems: options.maxItemsPerSource } : {}),\n ...(options.onWarning\n ? { onWarning: (warning: HarvesterWarning) => options.onWarning?.(warning, source) }\n : {}),\n };\n return fetchHtml(source, htmlOptions);\n });\n};\n\nconst filterKnown = async (\n articles: Article[],\n knownFn?: () => Promise<readonly string[]> | readonly string[],\n): Promise<Article[]> => {\n if (!knownFn) return articles;\n const known = new Set(await knownFn());\n return articles.filter((a) => !known.has(a.hash));\n};\n\nexport const createHarvester = (options: HarvesterOptions): Harvester => {\n const {\n sources,\n dedup: dedupOpts,\n digest: defaultDigestOpts,\n requestTimeout = 15_000,\n requestGap = 1_000,\n maxItemsPerSource = 50,\n onError,\n onWarning,\n } = options;\n\n const fetchFn = options.fetch;\n const throttle = new ThrottleQueue(requestGap);\n\n let scheduler: { stop: () => void } | null = null;\n\n const getEnabledSources = (): SourceConfig[] =>\n sources.filter((s) => s.enabled !== false);\n\n const fetchSingle = async (source: SourceConfig): Promise<Article[]> => {\n const fetchOptions: Parameters<typeof fetchSource>[2] = {\n timeout: requestTimeout,\n maxItemsPerSource,\n ...(fetchFn ? { fetchFn } : {}),\n ...(onWarning ? { onWarning } : {}),\n };\n const articles = await fetchSource(source, throttle, fetchOptions);\n return filterKnown(articles, dedupOpts?.known);\n };\n\n const fetchMany = async (selectedSources: readonly SourceConfig[]): Promise<Article[]> => {\n const results: Article[] = [];\n for (const source of selectedSources) {\n try {\n const articles = await fetchSingle(source);\n results.push(...articles);\n } catch (error) {\n onError?.(error, source);\n }\n }\n return results;\n };\n\n return {\n async fetchAll(): Promise<Article[]> {\n return fetchMany(getEnabledSources());\n },\n\n async fetch(sourceId: string): Promise<Article[]> {\n const source = sources.find((s) => s.id === sourceId);\n if (!source) {\n throw new Error(`Source not found: ${sourceId}`);\n }\n return fetchSingle(source);\n },\n\n async fetchByTags(tags: readonly string[]): Promise<Article[]> {\n const tagSet = new Set(tags);\n const matching = getEnabledSources().filter((s) =>\n s.tags.some((t) => tagSet.has(t)),\n );\n return fetchMany(matching);\n },\n\n async digest(overrides?: DigestOptions): Promise<DigestResult> {\n const articles = await fetchMany(getEnabledSources());\n const opts = { ...defaultDigestOpts, ...overrides };\n const result = buildDigest(articles, opts);\n return {\n articles: result.articles,\n stats: result.stats,\n };\n },\n\n start(callbacks: SchedulerCallbacks): void {\n if (scheduler) {\n scheduler.stop();\n }\n const schedulerCallbacks: SchedulerCallbacks = {\n onArticles: callbacks.onArticles,\n ...(callbacks.onError ?? onError ? { onError: callbacks.onError ?? onError } : {}),\n ...(callbacks.onWarning ?? onWarning ? { onWarning: callbacks.onWarning ?? onWarning } : {}),\n };\n scheduler = createScheduler(\n getEnabledSources(),\n (source) => fetchSingle(source),\n schedulerCallbacks,\n );\n },\n\n stop(): void {\n scheduler?.stop();\n scheduler = null;\n },\n };\n};\n"],"mappings":"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","names":["DEFAULT_UA","fetchSource"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "osint-feed",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Config-driven news harvester for OSINT. RSS + HTML scraping with built-in dedup and LLM-ready digest.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"dev": "tsx src/dev.ts"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"osint",
|
|
26
|
+
"rss",
|
|
27
|
+
"scraper",
|
|
28
|
+
"news",
|
|
29
|
+
"feed",
|
|
30
|
+
"harvester",
|
|
31
|
+
"llm",
|
|
32
|
+
"digest"
|
|
33
|
+
],
|
|
34
|
+
"author": "DewXIT",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"cheerio": "^1.0.0",
|
|
41
|
+
"rss-parser": "^3.13.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.5.0",
|
|
47
|
+
"vitest": "^2.0.0",
|
|
48
|
+
"tsx": "^4.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|