@involvex/bun-crawl 0.0.0-stage → 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/.github/FUNDING.yml +10 -0
- package/.prettierignore +41 -0
- package/AGENTS.md +357 -0
- package/IMPLEMENTATION_PLAN.md +452 -0
- package/Plan.md +83 -0
- package/README.md +180 -2
- package/dist/bun-crawl.js +101 -0
- package/eslint.config.mjs +37 -0
- package/package.json +40 -5
- package/src/ai/interface.ts +7 -0
- package/src/ai/null-ai.ts +10 -0
- package/src/ai/zen-ai.ts +57 -0
- package/src/cli.ts +309 -0
- package/src/core/cleaner.ts +47 -0
- package/src/core/crawler.ts +146 -0
- package/src/core/fetcher.ts +64 -0
- package/src/core/markdown.ts +108 -0
- package/src/index.ts +13 -0
- package/src/types.ts +54 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# bun-crawl Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**Name:** `bun-crawl` (CLI binary name)
|
|
6
|
+
**Package Manager:** Bun
|
|
7
|
+
**CLI Framework:** `citty`
|
|
8
|
+
**Language:** TypeScript
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Directory Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
bun-crawl/
|
|
16
|
+
├── src/
|
|
17
|
+
│ ├── index.ts # Entry point, exports CLI
|
|
18
|
+
│ ├── cli.ts # CLI definition (citty)
|
|
19
|
+
│ ├── core/
|
|
20
|
+
│ │ ├── fetcher.ts # HTTP fetch wrapper with config
|
|
21
|
+
│ │ ├── cleaner.ts # linkedom + @mozilla/readability
|
|
22
|
+
│ │ ├── markdown.ts # turndown configuration
|
|
23
|
+
│ │ └── crawler.ts # HTMLRewriter-based link extraction & recursion
|
|
24
|
+
│ ├── ai/
|
|
25
|
+
│ │ ├── interface.ts # AIExtractor interface
|
|
26
|
+
│ │ ├── null-ai.ts # NoOpAIExtractor (pass-through)
|
|
27
|
+
│ │ └── zen-ai.ts # Template/Stub for OpenCode Zen (commented)
|
|
28
|
+
│ └── types.ts # Shared types/interfaces
|
|
29
|
+
├── package.json
|
|
30
|
+
├── bunfig.toml
|
|
31
|
+
├── tsconfig.json
|
|
32
|
+
└── README.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 1: Project Setup
|
|
38
|
+
|
|
39
|
+
### 1.1 Initialize Bun Project
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun init -y
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 1.2 Install Dependencies
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Production dependencies
|
|
49
|
+
bun add citty turndown @mozilla/readability linkedom
|
|
50
|
+
|
|
51
|
+
# Dev dependencies
|
|
52
|
+
bun add -d @types/turndown @types/mozilla-readability typescript
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 1.3 Configure `package.json`
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "bun-crawl",
|
|
60
|
+
"version": "0.1.0",
|
|
61
|
+
"type": "module",
|
|
62
|
+
"main": "src/index.ts",
|
|
63
|
+
"bin": {
|
|
64
|
+
"bun-crawl": "./src/cli.ts"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"dev": "bun run src/cli.ts",
|
|
68
|
+
"build": "bun build --compile --minify ./src/index.ts --outfile bun-crawl",
|
|
69
|
+
"test": "bun test"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"citty": "^0.1.6",
|
|
73
|
+
"turndown": "^7.2.0",
|
|
74
|
+
"@mozilla/readability": "^0.5.0",
|
|
75
|
+
"linkedom": "^0.18.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"@types/turndown": "^5.0.4",
|
|
79
|
+
"@types/mozilla-readability": "^0.1.0",
|
|
80
|
+
"typescript": "^5.5.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 1.4 Configure `tsconfig.json`
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"compilerOptions": {
|
|
90
|
+
"target": "ES2022",
|
|
91
|
+
"module": "ESNext",
|
|
92
|
+
"moduleResolution": "bundler",
|
|
93
|
+
"lib": ["ES2022"],
|
|
94
|
+
"strict": true,
|
|
95
|
+
"esModuleInterop": true,
|
|
96
|
+
"skipLibCheck": true,
|
|
97
|
+
"forceConsistentCasingInFileNames": true,
|
|
98
|
+
"resolveJsonModule": true,
|
|
99
|
+
"declaration": true,
|
|
100
|
+
"outDir": "./dist"
|
|
101
|
+
},
|
|
102
|
+
"include": ["src/**/*"],
|
|
103
|
+
"exclude": ["node_modules", "dist"]
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 1.5 Configure `bunfig.toml`
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
[install]
|
|
111
|
+
cache = true
|
|
112
|
+
|
|
113
|
+
[run]
|
|
114
|
+
env = { NODE_ENV = "development" }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Phase 2: Core Processing Pipeline
|
|
120
|
+
|
|
121
|
+
### 2.1 Shared Types (`src/types.ts`)
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export interface ScrapeResult {
|
|
125
|
+
url: string
|
|
126
|
+
title: string
|
|
127
|
+
markdown: string
|
|
128
|
+
html: string
|
|
129
|
+
metadata: {
|
|
130
|
+
description?: string
|
|
131
|
+
author?: string
|
|
132
|
+
publishedTime?: string
|
|
133
|
+
[key: string]: unknown
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface CrawlOptions {
|
|
138
|
+
depth: number
|
|
139
|
+
limit: number
|
|
140
|
+
sameDomain: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface CrawlResult {
|
|
144
|
+
pages: ScrapeResult[]
|
|
145
|
+
errors: Array<{url: string; error: string}>
|
|
146
|
+
stats: {
|
|
147
|
+
total: number
|
|
148
|
+
succeeded: number
|
|
149
|
+
failed: number
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface FetcherOptions {
|
|
154
|
+
timeout?: number
|
|
155
|
+
userAgent?: string
|
|
156
|
+
headers?: Record<string, string>
|
|
157
|
+
followRedirects?: boolean
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2.2 Fetcher (`src/core/fetcher.ts`)
|
|
162
|
+
|
|
163
|
+
- Wrapper around native `fetch()`
|
|
164
|
+
- Configurable timeout, user-agent, headers
|
|
165
|
+
- Error handling with descriptive messages
|
|
166
|
+
- Returns raw HTML + response metadata
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
export class Fetcher {
|
|
170
|
+
constructor(private options: FetcherOptions = {}) {}
|
|
171
|
+
|
|
172
|
+
async fetch(
|
|
173
|
+
url: string,
|
|
174
|
+
): Promise<{html: string; url: string; status: number}> {
|
|
175
|
+
// Implementation with AbortController for timeout
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 2.3 Cleaner (`src/core/cleaner.ts`)
|
|
181
|
+
|
|
182
|
+
- Uses `linkedom` to create DOM from HTML
|
|
183
|
+
- Uses `@mozilla/readability` to extract main content
|
|
184
|
+
- Returns cleaned HTML + metadata (title, byline, etc.)
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
export class Cleaner {
|
|
188
|
+
clean(
|
|
189
|
+
html: string,
|
|
190
|
+
url: string,
|
|
191
|
+
): {content: string; title: string; metadata: Record<string, unknown>} {
|
|
192
|
+
// JSDOM via linkedom + Readability
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 2.4 Markdown Converter (`src/core/markdown.ts`)
|
|
198
|
+
|
|
199
|
+
- Configures `turndown` with sensible defaults
|
|
200
|
+
- Handles: tables, code blocks (fenced), links, images, headings
|
|
201
|
+
- Options for: heading style, code block style, bullet list marker
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
export class MarkdownConverter {
|
|
205
|
+
private turndown: TurndownService
|
|
206
|
+
|
|
207
|
+
constructor(options?: TurndownOptions) {}
|
|
208
|
+
|
|
209
|
+
convert(html: string): string {
|
|
210
|
+
return this.turndown.turndown(html)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 2.5 Crawler (`src/core/crawler.ts`)
|
|
216
|
+
|
|
217
|
+
- Uses Bun's native `HTMLRewriter` for streaming link extraction
|
|
218
|
+
- Recursive crawling with depth/limit control
|
|
219
|
+
- Same-domain filtering
|
|
220
|
+
- Deduplication of URLs
|
|
221
|
+
- Concurrent fetching with configurable concurrency
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
export class Crawler {
|
|
225
|
+
constructor(
|
|
226
|
+
private fetcher: Fetcher,
|
|
227
|
+
private cleaner: Cleaner,
|
|
228
|
+
private converter: MarkdownConverter,
|
|
229
|
+
private options: CrawlOptions,
|
|
230
|
+
) {}
|
|
231
|
+
|
|
232
|
+
async crawl(startUrl: string): Promise<CrawlResult> {
|
|
233
|
+
// BFS with HTMLRewriter for link extraction
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private extractLinks(html: string, baseUrl: string): string[] {
|
|
237
|
+
// HTMLRewriter implementation
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Phase 3: AI Extractor Pattern
|
|
245
|
+
|
|
246
|
+
### 3.1 Interface (`src/ai/interface.ts`)
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
export interface AIExtractor {
|
|
250
|
+
extract<T = string>(markdown: string, schemaPrompt: string): Promise<T>
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface AIExtractorConfig {
|
|
254
|
+
// Base config for all extractors
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 3.2 NullAIExtractor (`src/ai/null-ai.ts`) - Default
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
export class NullAIExtractor implements AIExtractor {
|
|
262
|
+
async extract<T = string>(
|
|
263
|
+
markdown: string,
|
|
264
|
+
_schemaPrompt: string,
|
|
265
|
+
): Promise<T> {
|
|
266
|
+
return markdown as T // Pass-through
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 3.3 ZenAIExtractor Template (`src/ai/zen-ai.ts`) - Stub/Template
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
// TODO: Implement when OpenCode Zen API is available
|
|
275
|
+
// This is a template for future OpenAI-compatible Chat Completions integration
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
export interface ZenAIConfig {
|
|
279
|
+
endpoint: string; // e.g. "https://api.opencode.ai/v1/chat/completions"
|
|
280
|
+
apiKey: string;
|
|
281
|
+
model?: string; // e.g. "gpt-4o", "claude-3.5-sonnet"
|
|
282
|
+
temperature?: number;
|
|
283
|
+
maxTokens?: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export class ZenAIExtractor implements AIExtractor {
|
|
287
|
+
constructor(private config: ZenAIConfig) {}
|
|
288
|
+
|
|
289
|
+
async extract<T = string>(markdown: string, schemaPrompt: string): Promise<T> {
|
|
290
|
+
const response = await fetch(this.config.endpoint, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers: {
|
|
293
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
model: this.config.model || 'gpt-4o',
|
|
298
|
+
messages: [
|
|
299
|
+
{ role: 'system', content: schemaPrompt },
|
|
300
|
+
{ role: 'user', content: markdown }
|
|
301
|
+
],
|
|
302
|
+
temperature: this.config.temperature ?? 0,
|
|
303
|
+
max_tokens: this.config.maxTokens ?? 4000,
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const data = await response.json();
|
|
308
|
+
return data.choices[0]?.message?.content as T;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
*/
|
|
312
|
+
|
|
313
|
+
// Export placeholder for now
|
|
314
|
+
export class ZenAIExtractor {
|
|
315
|
+
constructor() {
|
|
316
|
+
throw new Error(
|
|
317
|
+
'ZenAIExtractor not yet implemented. See src/ai/zen-ai.ts for template.',
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Phase 4: CLI Interface (`src/cli.ts`)
|
|
326
|
+
|
|
327
|
+
### Commands Structure
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// bun-crawl scrape <url> [options]
|
|
331
|
+
// bun-crawl crawl <url> [options]
|
|
332
|
+
// bun-crawl --help / --version
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Scrape Command Options
|
|
336
|
+
|
|
337
|
+
| Flag | Short | Description | Default |
|
|
338
|
+
| -------------- | ----- | ------------------------------------ | --------------- |
|
|
339
|
+
| `--output` | `-o` | Output file path (stdout if omitted) | stdout |
|
|
340
|
+
| `--format` | `-f` | Output format: `markdown`, `json` | markdown |
|
|
341
|
+
| `--timeout` | `-t` | Request timeout in ms | 30000 |
|
|
342
|
+
| `--user-agent` | `-u` | Custom User-Agent | bun-crawl/0.1.0 |
|
|
343
|
+
|
|
344
|
+
### Crawl Command Options
|
|
345
|
+
|
|
346
|
+
| Flag | Short | Description | Default |
|
|
347
|
+
| --------------- | ----- | ----------------------------------- | -------------- |
|
|
348
|
+
| `--depth` | `-d` | Max crawl depth | 2 |
|
|
349
|
+
| `--limit` | `-l` | Max pages to crawl | 20 |
|
|
350
|
+
| `--out-dir` | `-o` | Output directory for markdown files | ./crawl-output |
|
|
351
|
+
| `--same-domain` | | Stay on same domain | true |
|
|
352
|
+
| `--concurrency` | `-c` | Concurrent requests | 5 |
|
|
353
|
+
|
|
354
|
+
### Global Options
|
|
355
|
+
|
|
356
|
+
| Flag | Description |
|
|
357
|
+
| ----------- | ---------------------------- |
|
|
358
|
+
| `--json` | Output machine-readable JSON |
|
|
359
|
+
| `--verbose` | Verbose logging |
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Phase 5: Entry Point (`src/index.ts`)
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// Main exports for programmatic use
|
|
367
|
+
export {Fetcher} from './core/fetcher'
|
|
368
|
+
export {Cleaner} from './core/cleaner'
|
|
369
|
+
export {MarkdownConverter} from './core/markdown'
|
|
370
|
+
export {Crawler} from './core/crawler'
|
|
371
|
+
export {NullAIExtractor} from './ai/null-ai'
|
|
372
|
+
export {ZenAIExtractor} from './ai/zen-ai'
|
|
373
|
+
export type {AIExtractor} from './ai/interface'
|
|
374
|
+
export type {
|
|
375
|
+
ScrapeResult,
|
|
376
|
+
CrawlResult,
|
|
377
|
+
CrawlOptions,
|
|
378
|
+
FetcherOptions,
|
|
379
|
+
} from './types'
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Phase 6: Build & Distribution
|
|
385
|
+
|
|
386
|
+
### Build Script
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
"scripts": {
|
|
390
|
+
"build": "bun build --compile --minify ./src/index.ts --outfile bun-crawl"
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Output
|
|
395
|
+
|
|
396
|
+
- Single binary: `./bun-crawl` (Linux/macOS) or `./bun-crawl.exe` (Windows)
|
|
397
|
+
- Self-contained, no Node.js/Bun runtime required
|
|
398
|
+
- Cross-compilation via `bun build --compile --target=...`
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Implementation Order
|
|
403
|
+
|
|
404
|
+
1. ✅ **Setup**: `package.json`, `tsconfig.json`, `bunfig.toml`
|
|
405
|
+
2. ✅ **Types**: `src/types.ts`
|
|
406
|
+
3. ✅ **Core**: `fetcher.ts` → `cleaner.ts` → `markdown.ts` → `crawler.ts`
|
|
407
|
+
4. ✅ **AI**: `interface.ts` → `null-ai.ts` → `zen-ai.ts` (stub)
|
|
408
|
+
5. ✅ **CLI**: `cli.ts` with both commands (custom parser replacing citty)
|
|
409
|
+
6. ✅ **Entry**: `index.ts` exports
|
|
410
|
+
7. ✅ **Build**: Test compilation - standalone binary works
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Testing Strategy
|
|
415
|
+
|
|
416
|
+
- Unit tests for each core module
|
|
417
|
+
- Integration test for scrape command
|
|
418
|
+
- Integration test for crawl command
|
|
419
|
+
- Snapshot tests for markdown output
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Future Extensions (Post-MVP)
|
|
424
|
+
|
|
425
|
+
1. **ZenAIExtractor** full implementation
|
|
426
|
+
2. **Playwright fallback** for JS-heavy sites
|
|
427
|
+
3. **Sitemap.xml** discovery
|
|
428
|
+
4. **Robots.txt** compliance
|
|
429
|
+
5. **Rate limiting** per domain
|
|
430
|
+
6. **Cache layer** (file-based or Redis)
|
|
431
|
+
7. **Plugin system** for custom extractors
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Clarifications for Review
|
|
436
|
+
|
|
437
|
+
1. ✅ **CLI Name**: `bun-crawl` (binary & command)
|
|
438
|
+
2. ✅ **Framework**: Custom argument parser (citty's option parsing had issues with subcommands)
|
|
439
|
+
3. ✅ **ZenAIExtractor**: Template/stub only (commented implementation)
|
|
440
|
+
4. ✅ **Default output format for crawl**: Individual `.md` files per page in output directory
|
|
441
|
+
5. ✅ **Concurrency default**: 5 (works well)
|
|
442
|
+
6. ✅ **User-Agent string**: `bun-crawl/0.1.0`
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Suggested Next Steps
|
|
447
|
+
|
|
448
|
+
1. Add unit tests for core modules
|
|
449
|
+
2. Add integration tests for CLI commands
|
|
450
|
+
3. Add README.md with usage examples
|
|
451
|
+
4. Set up CI/CD for automated builds
|
|
452
|
+
5. Consider adding Playwright fallback for JS-heavy sites
|
package/Plan.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Bun-Crawl
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Architektur & Modul-Design
|
|
6
|
+
|
|
7
|
+
Die Pipeline folgt dem **Strategy-Pattern**. Der Scraper-Core übergibt das Ergebnis an eine Pipeline, deren letzter Schritt optional die AI-Extraktion ist.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
[URL] -> [Fetcher] -> [Readability + Turndown] -> [Markdown Output]
|
|
11
|
+
│
|
|
12
|
+
▼ (Optional / Später)
|
|
13
|
+
[OpenCode Zen API] -> [JSON Output]
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Ordnerstruktur
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
firebun/
|
|
21
|
+
├── src/
|
|
22
|
+
│ ├── cli.ts # CLI Interface (Commander / Citty)
|
|
23
|
+
│ ├── core/
|
|
24
|
+
│ │ ├── fetcher.ts # HTTP Requests (optional Playwright Fallback)
|
|
25
|
+
│ │ ├── cleaner.ts # Linkedom + @mozilla/readability
|
|
26
|
+
│ │ ├── markdown.ts # Turndown Konvertierung
|
|
27
|
+
│ │ └── crawler.ts # HTMLRewriter für Rekursion
|
|
28
|
+
│ ├── ai/
|
|
29
|
+
│ │ ├── interface.ts # AI Extractor Interface
|
|
30
|
+
│ │ └── null-ai.ts # Standart-Pass-Through (ohne AI)
|
|
31
|
+
│ │ └── zen-ai.ts # (Später) OpenCode Zen Provider
|
|
32
|
+
│ └── index.ts
|
|
33
|
+
├── package.json
|
|
34
|
+
└── bunfig.toml
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Implementierungsplan
|
|
41
|
+
|
|
42
|
+
### Phase 1: Projekt-Setup
|
|
43
|
+
|
|
44
|
+
- Initalisierung: `bun init`
|
|
45
|
+
- Dependencies: `bun add turndown @mozilla/readability linkedom citty`
|
|
46
|
+
- Dev-Dependencies: `bun add -d @types/turndown @types/mozilla-readability`
|
|
47
|
+
|
|
48
|
+
### Phase 2: Core Processing Pipeline (Ohne AI)
|
|
49
|
+
|
|
50
|
+
- **Fetcher (`src/core/fetcher.ts`):** `fetch()`-Wrapper mit konfigurierbaren User-Agents und Timeouts.
|
|
51
|
+
- **Cleaner (`src/core/cleaner.ts`):** DOM-Parsing mit `linkedom` und Content-Extraktion via `@mozilla/readability`.
|
|
52
|
+
- **Markdown (`src/core/markdown.ts`):** Konfiguration von `turndown` zur sauberen Formatierung von Tabellen, Code-Blöcken und Links.
|
|
53
|
+
|
|
54
|
+
### Phase 3: Crawl-Engine & CLI
|
|
55
|
+
|
|
56
|
+
- **Link-Parser (`src/core/crawler.ts`):** Nutzung von Buns nativem `HTMLRewriter`, um im HTTP-Stream alle internen Links (`<a href="...">`) zu sammeln und Rekursion (Tiefe/Limit) zu steuern.
|
|
57
|
+
- **CLI-Interface (`src/cli.ts`):** Flag-Parsing (`--depth`, `--out-dir`, `--json`).
|
|
58
|
+
|
|
59
|
+
### Phase 4: Vorbereitung der OpenCode Zen Schnittstelle
|
|
60
|
+
|
|
61
|
+
- Erstellen von `src/ai/interface.ts`:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
export interface AIExtractor {
|
|
65
|
+
extract<T>(markdown: string, schemaPrompt: string): Promise<T | string>
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `NullAIExtractor` implementieren, der lediglich das rohe Markdown unverändert zurückgibt.
|
|
70
|
+
- Für die spätere Anbindung muss nur ein `ZenAIExtractor` hinzugefügt werden, der per `fetch()` an den OpenAI-kompatiblen OpenCode Zen Endpoint sendet (`/v1/chat/completions`).
|
|
71
|
+
|
|
72
|
+
### Phase 5: Compilation
|
|
73
|
+
|
|
74
|
+
- Script in `package.json`:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "bun build --compile --minify ./src/index.ts --outfile firebun"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
package/README.md
CHANGED
|
@@ -1,3 +1,181 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bun-crawl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Open-source Firecrawl alternative built with Bun. Scrape and crawl websites into clean Markdown.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fast** - Native Bun runtime, no Node.js required
|
|
8
|
+
- **Clean output** - Mozilla Readability + Turndown for article extraction
|
|
9
|
+
- **Recursive crawling** - Depth-limited, same-domain filtering, concurrent requests
|
|
10
|
+
- **Standalone binary** - Single executable, no runtime dependencies
|
|
11
|
+
- **AI-ready** - Extractor interface for LLM-based content processing
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Pre-built Binary (Recommended)
|
|
16
|
+
|
|
17
|
+
Download the latest release for your platform.
|
|
18
|
+
|
|
19
|
+
### From Source
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/yourusername/bun-crawl
|
|
23
|
+
cd bun-crawl
|
|
24
|
+
bun install
|
|
25
|
+
bun run build
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Scrape a Single URL
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Output to stdout (markdown)
|
|
34
|
+
bun-crawl scrape https://example.com
|
|
35
|
+
|
|
36
|
+
# Save to file
|
|
37
|
+
bun-crawl scrape https://example.com -o page.md
|
|
38
|
+
|
|
39
|
+
# JSON output with metadata
|
|
40
|
+
bun-crawl scrape https://example.com --format json
|
|
41
|
+
bun-crawl scrape https://example.com --json
|
|
42
|
+
|
|
43
|
+
# Custom timeout and user agent
|
|
44
|
+
bun-crawl scrape https://example.com -t 60000 -u "MyBot/1.0"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Crawl a Website Recursively
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Crawl with defaults (depth 2, limit 20 pages)
|
|
51
|
+
bun-crawl crawl https://example.com
|
|
52
|
+
|
|
53
|
+
# Custom depth and limit
|
|
54
|
+
bun-crawl crawl https://example.com -d 3 -l 50
|
|
55
|
+
|
|
56
|
+
# Custom output directory
|
|
57
|
+
bun-crawl crawl https://example.com -O ./my-output
|
|
58
|
+
|
|
59
|
+
# JSON summary only
|
|
60
|
+
bun-crawl crawl https://example.com --json
|
|
61
|
+
|
|
62
|
+
# Stay on same domain (default), custom concurrency
|
|
63
|
+
bun-crawl crawl https://example.com --same-domain -c 10
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Options Reference
|
|
67
|
+
|
|
68
|
+
#### `scrape` Command
|
|
69
|
+
|
|
70
|
+
| Flag | Short | Description | Default |
|
|
71
|
+
| -------------- | ----- | ------------------------------------ | --------------- |
|
|
72
|
+
| `--output` | `-o` | Output file path (stdout if omitted) | stdout |
|
|
73
|
+
| `--format` | `-f` | Output format: `markdown`, `json` | markdown |
|
|
74
|
+
| `--timeout` | `-t` | Request timeout in milliseconds | 30000 |
|
|
75
|
+
| `--user-agent` | `-u` | Custom User-Agent string | bun-crawl/0.1.0 |
|
|
76
|
+
| `--json` | | Output machine-readable JSON | false |
|
|
77
|
+
|
|
78
|
+
#### `crawl` Command
|
|
79
|
+
|
|
80
|
+
| Flag | Short | Description | Default |
|
|
81
|
+
| --------------- | ----- | ------------------------------------ | --------------- |
|
|
82
|
+
| `--depth` | `-d` | Maximum crawl depth | 2 |
|
|
83
|
+
| `--limit` | `-l` | Maximum pages to crawl | 20 |
|
|
84
|
+
| `--out-dir` | `-O` | Output directory for markdown files | ./crawl-output |
|
|
85
|
+
| `--same-domain` | | Stay on the same domain | true |
|
|
86
|
+
| `--concurrency` | `-c` | Concurrent requests | 5 |
|
|
87
|
+
| `--timeout` | `-t` | Request timeout in milliseconds | 30000 |
|
|
88
|
+
| `--user-agent` | `-u` | Custom User-Agent string | bun-crawl/0.1.0 |
|
|
89
|
+
| `--json` | | Output machine-readable JSON summary | false |
|
|
90
|
+
|
|
91
|
+
## Programmatic API
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import {
|
|
95
|
+
Fetcher,
|
|
96
|
+
Cleaner,
|
|
97
|
+
MarkdownConverter,
|
|
98
|
+
Crawler,
|
|
99
|
+
NullAIExtractor,
|
|
100
|
+
} from 'bun-crawl'
|
|
101
|
+
|
|
102
|
+
const fetcher = new Fetcher({timeout: 30000, userAgent: 'MyBot/1.0'})
|
|
103
|
+
const cleaner = new Cleaner()
|
|
104
|
+
const converter = new MarkdownConverter()
|
|
105
|
+
const ai = new NullAIExtractor()
|
|
106
|
+
|
|
107
|
+
// Scrape single page
|
|
108
|
+
const {html, url, status} = await fetcher.fetch('https://example.com')
|
|
109
|
+
const {content, title, metadata} = cleaner.clean(html, url)
|
|
110
|
+
const markdown = await ai.extract(converter.convert(content), '')
|
|
111
|
+
|
|
112
|
+
// Crawl multiple pages
|
|
113
|
+
const crawler = new Crawler(fetcher, cleaner, converter, {
|
|
114
|
+
depth: 2,
|
|
115
|
+
limit: 20,
|
|
116
|
+
sameDomain: true,
|
|
117
|
+
concurrency: 5,
|
|
118
|
+
})
|
|
119
|
+
const result = await crawler.crawl('https://example.com')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## AI Extractors
|
|
123
|
+
|
|
124
|
+
`bun-crawl` uses a pluggable AI extractor interface. The default `NullAIExtractor` passes content through unchanged.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import {AIExtractor} from 'bun-crawl'
|
|
128
|
+
|
|
129
|
+
class MyAIExtractor implements AIExtractor {
|
|
130
|
+
async extract<T = string>(
|
|
131
|
+
markdown: string,
|
|
132
|
+
schemaPrompt: string,
|
|
133
|
+
): Promise<T> {
|
|
134
|
+
// Call your LLM API here
|
|
135
|
+
return markdown as T
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A `ZenAIExtractor` template is included for future OpenCode Zen integration.
|
|
141
|
+
|
|
142
|
+
## Building
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Development
|
|
146
|
+
bun run dev scrape https://example.com
|
|
147
|
+
|
|
148
|
+
# Production binary
|
|
149
|
+
bun run build
|
|
150
|
+
# Output: ./bun-crawl (Linux/macOS) or ./bun-crawl.exe (Windows)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Cross-compilation
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bun build --compile --target=linux-x64 ./src/cli.ts --outfile bun-crawl-linux
|
|
157
|
+
bun build --compile --target=darwin-x64 ./src/cli.ts --outfile bun-crawl-macos
|
|
158
|
+
bun build --compile --target=windows-x64 ./src/cli.ts --outfile bun-crawl.exe
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Project Structure
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
src/
|
|
165
|
+
├── cli.ts # CLI entry point
|
|
166
|
+
├── index.ts # Public API exports
|
|
167
|
+
├── types.ts # TypeScript interfaces
|
|
168
|
+
├── core/
|
|
169
|
+
│ ├── fetcher.ts # HTTP fetch wrapper
|
|
170
|
+
│ ├── cleaner.ts # Readability content extraction
|
|
171
|
+
│ ├── markdown.ts # Turndown HTML→Markdown
|
|
172
|
+
│ └── crawler.ts # Recursive crawler (HTMLRewriter)
|
|
173
|
+
└── ai/
|
|
174
|
+
├── interface.ts # AIExtractor interface
|
|
175
|
+
├── null-ai.ts # Pass-through extractor
|
|
176
|
+
└── zen-ai.ts # Template for Zen integration
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|