@wenathlan/saddle 1.8.2 → 1.8.5

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.
Files changed (257) hide show
  1. package/README.md +9 -9
  2. package/browser/playwright.js +22 -0
  3. package/docs/.gitkeep +0 -0
  4. package/docs/gapmatrix.md +3 -3
  5. package/docs/libraryapi.md +1 -0
  6. package/docs/logs/.gitkeep +0 -0
  7. package/docs/packageaudit185.md +23 -0
  8. package/docs/plans/00.index.md +50 -0
  9. package/docs/plans/01.architecture.md +86 -0
  10. package/docs/plans/02.research.computer.use.md +58 -0
  11. package/docs/plans/03.research.captcha.bypass.md +68 -0
  12. package/docs/plans/04.research.sandbox.ai.md +52 -0
  13. package/docs/plans/05.capture.platform.md +57 -0
  14. package/docs/plans/06.dependencies.md +97 -0
  15. package/docs/plans/07.captcha.test.page.md +41 -0
  16. package/docs/plans/08.production.infra.md +70 -0
  17. package/docs/plans/09.database.schema.md +121 -0
  18. package/docs/plans/10.cloudinary.storage.md +57 -0
  19. package/docs/plans/11.movement.logs.json.md +72 -0
  20. package/docs/plans/12.research.atlas.agent.browser.md +79 -0
  21. package/docs/plans/13.research.anti.detection.md +898 -0
  22. package/docs/plans/14.research.proxy.md +1495 -0
  23. package/docs/plans/15.research.retry.rate.limit.md +1958 -0
  24. package/docs/plans/16.research.crawling.md +1417 -0
  25. package/docs/plans/17.research.caching.md +1610 -0
  26. package/docs/plans/18.research.content.extraction.md +1952 -0
  27. package/docs/plans/19.research.errors.events.md +1523 -0
  28. package/docs/plans/20.research.zod.validation.md +1350 -0
  29. package/docs/plans/21.research.batch.concurrency.md +1888 -0
  30. package/docs/plans/22.research.universal.runtime.md +944 -0
  31. package/docs/plans/23.research.ai.integration.md +1465 -0
  32. package/docs/plans/24.research.memory.persistence.md +1979 -0
  33. package/docs/plans/25.research.server.api.md +342 -0
  34. package/docs/plans/26.research.compilation.md +249 -0
  35. package/docs/plans/27.research.html.parsing.md +251 -0
  36. package/docs/plans/28.action.plan.md +50 -0
  37. package/docs/plans/29.api.reference.md +174 -0
  38. package/docs/plans/30.architecture.plan.md +94 -0
  39. package/docs/plans/31.auditoria.dados.md +163 -0
  40. package/docs/plans/32.bots.automacao.computacional.md +214 -0
  41. package/docs/plans/33.bots.codigo.revisao.md +220 -0
  42. package/docs/plans/34.bots.seguranca.cicd.md +366 -0
  43. package/docs/plans/35.comparativo.concorrencia.md +464 -0
  44. package/docs/plans/36.computational.memory.md +340 -0
  45. package/docs/plans/37.deploystrategy.md +394 -0
  46. package/docs/plans/38.flow.md +155 -0
  47. package/docs/plans/39.multi.platform.bot.md +252 -0
  48. package/docs/plans/40.npm.publish.md +250 -0
  49. package/docs/plans/41.o.que.falta.md +407 -0
  50. package/docs/plans/42.pesquisa.concorrencia.md +721 -0
  51. package/docs/plans/43.plan.universal.architecture.md +496 -0
  52. package/docs/plans/44.reference.md +100 -0
  53. package/docs/plans/45.robotarchitecture.md +237 -0
  54. package/docs/plans/46.scdnintegration.md +284 -0
  55. package/docs/plans/47.multiforge.readme.md +129 -0
  56. package/docs/plans/48.theory.v4.repo.os.md +152 -0
  57. package/docs/plans/49.third.party.infra.md +12 -0
  58. package/docs/plans/50.file.as.compute.md +39 -0
  59. package/docs/plans/51.architecture.virtual.processor.md +80 -0
  60. package/docs/plans/52.manifesto.v8.md +11 -0
  61. package/docs/plans/58.cdn.list.md +23 -0
  62. package/docs/plans/59.sql.frameworks.md +33 -0
  63. package/docs/plans/60.sql.thirdparty.md +26 -0
  64. package/docs/plans/61.objective.multiforge.md +63 -0
  65. package/docs/plans/62.huggingface.upload.md +26 -0
  66. package/docs/plans/63.kaggle.upload.md +24 -0
  67. package/docs/plans/64.npm.storage.md +30 -0
  68. package/docs/plans/65.rclone.terabox.md +32 -0
  69. package/docs/plans/66.buckets.and.models.todo.md +14 -0
  70. package/docs/plans/67.database.todo.md +13 -0
  71. package/docs/plans/68.deploy.packages.todo.md +12 -0
  72. package/docs/plans/69.report.human.operator.md +133 -0
  73. package/docs/plans/70.report.brain2qwerty.ems.md +135 -0
  74. package/docs/plans/71.report.hd.infinito.vram.md +155 -0
  75. package/docs/plans/72.plan.hd.infinito.node.md +146 -0
  76. package/docs/plans/73.plan.scifi.repos.md +125 -0
  77. package/docs/plans/74.000.manifesto.v8.flat.2..md +11 -0
  78. package/docs/plans/README.md +489 -0
  79. package/docs/plans/aggregate_platforms.mjs +146 -0
  80. package/docs/plans/examplesession.json +36 -0
  81. package/docs/plans/missing-facts.md +192 -0
  82. package/docs/plans/models.md +64 -0
  83. package/docs/plans/organize.cjs +270 -0
  84. package/docs/plans/platforms.md +2887 -0
  85. package/docs/plans/sites.md +31322 -0
  86. package/docs/platformpipelineaudit.md +18 -0
  87. package/docs/platformpipelines.md +13 -0
  88. package/docs/registryresearch.md +4 -0
  89. package/docs/release.md +4 -4
  90. package/docs/release182notes.md +2 -0
  91. package/docs/release184notes.md +7 -0
  92. package/docs/release185notes.md +7 -0
  93. package/docs/sources/farm.py +117 -0
  94. package/docs/sources/html/saddle1.html +132 -0
  95. package/docs/sources/html/saddle2.html +157 -0
  96. package/docs/sources/html/saddle3.html +119 -0
  97. package/docs/sources/html/saddle4.html +144 -0
  98. package/docs/sources/html/saddle5.html +72 -0
  99. package/docs/sources/html/saddle6.html +171 -0
  100. package/docs/sources/html/saddle7.html +236 -0
  101. package/docs/sources/saddle.ts +74 -0
  102. package/docs/sources/schema.prisma +88 -0
  103. package/docs/sources/script.sh +64 -0
  104. package/docs/sources/workflows.yml +458 -0
  105. package/docs/talks1/_body.txt +14 -0
  106. package/docs/talks1/_index.md +15 -0
  107. package/docs/talks1/_screenshot.png +0 -0
  108. package/docs/talks1/assistant-01.md +5 -0
  109. package/docs/talks1/assistant-02.md +5 -0
  110. package/docs/talks1/assistant-03.md +531 -0
  111. package/docs/talks1/assistant-04.md +26 -0
  112. package/docs/talks1/assistant-05.md +774 -0
  113. package/docs/talks1/assistant-06.md +1718 -0
  114. package/docs/talks1/scrape-share.cjs +185 -0
  115. package/docs/talks1/scrape-share.ts +183 -0
  116. package/docs/talks1/user-01.md +3 -0
  117. package/docs/talks1/user-02.md +3 -0
  118. package/docs/talks1/user-03.md +88 -0
  119. package/docs/talks1/user-04.md +3 -0
  120. package/docs/talks1/user-05.md +3 -0
  121. package/docs/talks1/user-06.md +88 -0
  122. package/docs/talks1/user-07.md +88 -0
  123. package/docs/talks2/_body.txt +14 -0
  124. package/docs/talks2/_index.md +16 -0
  125. package/docs/talks2/_screenshot.png +0 -0
  126. package/docs/talks2/assistant-01.md +5 -0
  127. package/docs/talks2/assistant-02.md +5 -0
  128. package/docs/talks2/assistant-03.md +424 -0
  129. package/docs/talks2/assistant-04.md +598 -0
  130. package/docs/talks2/assistant-05.md +1280 -0
  131. package/docs/talks2/assistant-06.md +1227 -0
  132. package/docs/talks2/assistant-07.md +1252 -0
  133. package/docs/talks2/user-01.md +3 -0
  134. package/docs/talks2/user-02.md +3 -0
  135. package/docs/talks2/user-03.md +88 -0
  136. package/docs/talks2/user-04.md +88 -0
  137. package/docs/talks2/user-05.md +88 -0
  138. package/docs/talks2/user-06.md +88 -0
  139. package/docs/talks2/user-07.md +3 -0
  140. package/docs/talks3/_body.txt +467 -0
  141. package/docs/talks3/_index.md +10 -0
  142. package/docs/talks3/_screenshot.png +0 -0
  143. package/docs/talks3/assistant-01.md +417 -0
  144. package/docs/talks3/assistant-02.md +417 -0
  145. package/docs/talks3/assistant-03.md +29 -0
  146. package/docs/talks3/assistant-04.md +727 -0
  147. package/docs/talks3/user-01.md +88 -0
  148. package/docs/talks3/user-02.md +88 -0
  149. package/docs/talks3/user-03.md +3 -0
  150. package/docs/talks3/user-04.md +3 -0
  151. package/docs/talks4/_body.txt +14 -0
  152. package/docs/talks4/_index.md +12 -0
  153. package/docs/talks4/_screenshot.png +0 -0
  154. package/docs/talks4/assistant-01.md +5 -0
  155. package/docs/talks4/assistant-02.md +5 -0
  156. package/docs/talks4/assistant-03.md +35 -0
  157. package/docs/talks4/assistant-04.md +512 -0
  158. package/docs/talks4/assistant-05.md +599 -0
  159. package/docs/talks4/user-01.md +3 -0
  160. package/docs/talks4/user-02.md +3 -0
  161. package/docs/talks4/user-03.md +88 -0
  162. package/docs/talks4/user-04.md +88 -0
  163. package/docs/talks4/user-05.md +7 -0
  164. package/docs/talks5/_body.txt +14 -0
  165. package/docs/talks5/_index.md +13 -0
  166. package/docs/talks5/_screenshot.png +0 -0
  167. package/docs/talks5/assistant-01.md +5 -0
  168. package/docs/talks5/assistant-02.md +5 -0
  169. package/docs/talks5/assistant-03.md +690 -0
  170. package/docs/talks5/assistant-04.md +758 -0
  171. package/docs/talks5/assistant-05.md +974 -0
  172. package/docs/talks5/user-01.md +3 -0
  173. package/docs/talks5/user-02.md +3 -0
  174. package/docs/talks5/user-03.md +105 -0
  175. package/docs/talks5/user-04.md +105 -0
  176. package/docs/talks5/user-05.md +63 -0
  177. package/docs/talks5/user-06.md +105 -0
  178. package/docs/talks6/_body.txt +14 -0
  179. package/docs/talks6/_index.md +9 -0
  180. package/docs/talks6/_screenshot.png +0 -0
  181. package/docs/talks6/assistant-01.md +5 -0
  182. package/docs/talks6/assistant-02.md +5 -0
  183. package/docs/talks6/assistant-03.md +1499 -0
  184. package/docs/talks6/user-01.md +3 -0
  185. package/docs/talks6/user-02.md +3 -0
  186. package/docs/talks6/user-03.md +88 -0
  187. package/docs/talks6/user-04.md +88 -0
  188. package/docs/talks7/_body.txt +14 -0
  189. package/docs/talks7/_index.md +10 -0
  190. package/docs/talks7/_screenshot.png +0 -0
  191. package/docs/talks7/assistant-01.md +5 -0
  192. package/docs/talks7/assistant-02.md +5 -0
  193. package/docs/talks7/assistant-03.md +523 -0
  194. package/docs/talks7/assistant-04.md +617 -0
  195. package/docs/talks7/user-01.md +3 -0
  196. package/docs/talks7/user-02.md +3 -0
  197. package/docs/talks7/user-03.md +105 -0
  198. package/docs/talks7/user-04.md +67 -0
  199. package/docs/talks8/conversa1.txt +1322 -0
  200. package/docs/talks8/conversa2.txt +237 -0
  201. package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +174 -0
  202. package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +59 -0
  203. package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +91 -0
  204. package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +116 -0
  205. package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +128 -0
  206. package/docs/talks9/README (2).md +489 -0
  207. package/docs/talks9/README.md +198 -0
  208. package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +80 -0
  209. package/docs/talks9/conversa.txt +544 -0
  210. package/docs/talks9/other (2).md +39 -0
  211. package/docs/talks9/other.md +57 -0
  212. package/docs/talks9/outro.txt +24 -0
  213. package/extension/README.md +3 -3
  214. package/extension/serviceworker.js +63 -4
  215. package/extension/worker.js +3 -1
  216. package/package.json +12 -2
  217. package/packager/manifest.js +1 -1
  218. package/scrape/agent.ts +122 -0
  219. package/scrape/batch.ts +79 -0
  220. package/scrape/biome.json +76 -0
  221. package/scrape/browser.ts +222 -0
  222. package/scrape/cache.ts +84 -0
  223. package/scrape/chunking.ts +193 -0
  224. package/scrape/cli.ts +105 -0
  225. package/scrape/crawler.ts +115 -0
  226. package/scrape/dev-server.ts +94 -0
  227. package/scrape/errors.ts +132 -0
  228. package/scrape/events.ts +26 -0
  229. package/scrape/extract.ts +165 -0
  230. package/scrape/fetch.ts +105 -0
  231. package/scrape/formats.ts +85 -0
  232. package/scrape/headers.ts +71 -0
  233. package/scrape/index.ts +92 -0
  234. package/scrape/jsdom.d.ts +6 -0
  235. package/scrape/llms-txt.ts +84 -0
  236. package/scrape/middleware.ts +90 -0
  237. package/scrape/package-lock.json +9397 -0
  238. package/scrape/package.json +1420 -0
  239. package/scrape/pool.ts +95 -0
  240. package/scrape/port.ts +18 -0
  241. package/scrape/proxy.ts +103 -0
  242. package/scrape/rate-limiter.ts +95 -0
  243. package/scrape/renderer.ts +194 -0
  244. package/scrape/retry.ts +64 -0
  245. package/scrape/robots.ts +137 -0
  246. package/scrape/scrape.ts +123 -0
  247. package/scrape/serialize.ts +310 -0
  248. package/scrape/server.ts +137 -0
  249. package/scrape/session.ts +109 -0
  250. package/scrape/sitemap.ts +131 -0
  251. package/scrape/tokens.ts +45 -0
  252. package/scrape/tsconfig.json +28 -0
  253. package/scrape/types.ts +214 -0
  254. package/scrape/utils.ts +77 -0
  255. package/scrape/vite.config.ts +55 -0
  256. package/scrape/vitest.config.ts +17 -0
  257. package/workflow/templates.js +4 -4
@@ -0,0 +1,123 @@
1
+ import { AgentBrowser } from './browser.js';
2
+ import { extractContent } from './extract.js';
3
+ import { fetchHtml, detectRenderingMode } from './fetch.js';
4
+ import type { ScrapeOptions, ScrapeResult } from './types.js';
5
+ import { WebScrapeError, ErrorCode } from './errors.js';
6
+
7
+ export async function scrapeUrl(url: string, options: ScrapeOptions = {}): Promise<ScrapeResult> {
8
+ if (!url || !url.startsWith('http')) {
9
+ throw new WebScrapeError(`Invalid URL: ${url}`, ErrorCode.VALIDATION_FAILED, 400, false);
10
+ }
11
+
12
+ const startTime = Date.now();
13
+ const mode = options.mode || 'auto';
14
+
15
+ if (mode === 'fetch' || mode === 'auto') {
16
+ try {
17
+ const fetchResult = await fetchHtml(url, {
18
+ timeout: options.timeout,
19
+ headers: options.headers,
20
+ userAgent: options.userAgent,
21
+ });
22
+
23
+ if (mode === 'auto') {
24
+ const renderMode = detectRenderingMode(fetchResult.html);
25
+ if (renderMode === 'static') {
26
+ return buildResult(fetchResult.html, fetchResult.url, options, startTime);
27
+ }
28
+ } else {
29
+ return buildResult(fetchResult.html, fetchResult.url, options, startTime);
30
+ }
31
+ } catch (error) {
32
+ if (mode === 'fetch') throw error;
33
+ if (!(error instanceof WebScrapeError) || error.isRetryable) {
34
+ // Fall through to browser mode
35
+ } else {
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+
41
+ const browser = new AgentBrowser({
42
+ headless: true,
43
+ timeout: options.timeout ?? 30000,
44
+ proxy: options.proxy ?? '',
45
+ userAgent: options.userAgent,
46
+ });
47
+
48
+ try {
49
+ await browser.launch();
50
+ await browser.navigate(url, (options.waitUntil as 'load' | 'domcontentloaded' | 'networkidle') ?? 'networkidle');
51
+
52
+ if (options.scroll) {
53
+ await browser.scrollToBottom(800, options.scrollDelay ?? 300, options.maxScrolls ?? 50);
54
+ }
55
+
56
+ const html = await browser.html();
57
+ const pageTitle = await browser.title();
58
+ const pageUrl = await browser.url();
59
+
60
+ return buildResult(html, pageUrl, options, startTime, pageTitle);
61
+ } finally {
62
+ await browser.close();
63
+ }
64
+ }
65
+
66
+ export async function scrapeHtml(html: string, options: ScrapeOptions = {}): Promise<ScrapeResult> {
67
+ const startTime = Date.now();
68
+ return buildResult(html, 'about:blank', options, startTime);
69
+ }
70
+
71
+ export async function scrapeWithBrowser(
72
+ browser: AgentBrowser,
73
+ url: string,
74
+ options: ScrapeOptions = {}
75
+ ): Promise<ScrapeResult> {
76
+ const startTime = Date.now();
77
+
78
+ await browser.navigate(url, (options.waitUntil as 'load' | 'domcontentloaded' | 'networkidle') ?? 'networkidle');
79
+
80
+ if (options.scroll) {
81
+ await browser.scrollToBottom(800, options.scrollDelay ?? 300, options.maxScrolls ?? 50);
82
+ }
83
+
84
+ const html = await browser.html();
85
+ const pageTitle = await browser.title();
86
+ const pageUrl = await browser.url();
87
+
88
+ return buildResult(html, pageUrl, options, startTime, pageTitle);
89
+ }
90
+
91
+ async function buildResult(
92
+ html: string,
93
+ url: string,
94
+ options: ScrapeOptions,
95
+ startTime: number,
96
+ title?: string,
97
+ ): Promise<ScrapeResult> {
98
+ const extracted = await extractContent(html, {
99
+ readable: true,
100
+ preserveLinks: options.extractLinks ?? true,
101
+ preserveImages: options.extractImages ?? true,
102
+ preserveTables: options.extractTables ?? true,
103
+ maxLength: options.maxContentLength,
104
+ removeSelectors: options.removeSelectors,
105
+ });
106
+
107
+ const duration = Date.now() - startTime;
108
+
109
+ return {
110
+ url,
111
+ title: title || extracted.metadata.title || '',
112
+ content: extracted.content,
113
+ format: options.format ?? 'markdown',
114
+ text: extracted.text,
115
+ links: extracted.links,
116
+ images: extracted.images,
117
+ tables: extracted.tables,
118
+ metadata: extracted.metadata,
119
+ extractedAt: new Date().toISOString(),
120
+ duration,
121
+ size: extracted.content.length + extracted.text.length,
122
+ };
123
+ }
@@ -0,0 +1,310 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import type { SerializeOptions, SerializedOutput, ScrapeResult } from './types.js';
3
+ import { chunkText } from './utils.js';
4
+
5
+ function htmlToMarkdown(html: string): string {
6
+ const dom = new JSDOM(html);
7
+ const doc = dom.window.document;
8
+ let md = '';
9
+
10
+ function processNode(node: globalThis.Node, depth = 0): void {
11
+ if (node.nodeType === 3) {
12
+ const text = (node as globalThis.Text).textContent?.trim();
13
+ if (text) md += text + ' ';
14
+ return;
15
+ }
16
+ if (node.nodeType !== 1) return;
17
+ const el = node as globalThis.Element;
18
+ const tag = el.tagName.toLowerCase();
19
+
20
+ switch (tag) {
21
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': {
22
+ const level = parseInt(tag[1]);
23
+ md += '\n' + '#'.repeat(level) + ' ' + el.textContent?.trim() + '\n\n';
24
+ break;
25
+ }
26
+ case 'p':
27
+ md += '\n' + el.textContent?.trim() + '\n\n';
28
+ break;
29
+ case 'br':
30
+ md += '\n';
31
+ break;
32
+ case 'hr':
33
+ md += '\n---\n\n';
34
+ break;
35
+ case 'ul': case 'ol': {
36
+ el.childNodes.forEach(li => {
37
+ if (li.nodeType === 1) {
38
+ const liEl = li as globalThis.Element;
39
+ if (liEl.tagName.toLowerCase() === 'li') {
40
+ md += ' '.repeat(depth) + '- ' + liEl.textContent?.trim() + '\n';
41
+ }
42
+ }
43
+ });
44
+ md += '\n';
45
+ break;
46
+ }
47
+ case 'a': {
48
+ const href = el.getAttribute('href') || '';
49
+ const text = el.textContent?.trim() || href;
50
+ if (href && href !== '#') md += `[${text}](${href}) `;
51
+ else md += text + ' ';
52
+ break;
53
+ }
54
+ case 'img': {
55
+ const src = el.getAttribute('src') || '';
56
+ const alt = el.getAttribute('alt') || '';
57
+ if (src) md += `![${alt}](${src}) `;
58
+ break;
59
+ }
60
+ case 'strong': case 'b':
61
+ md += '**' + el.textContent?.trim() + '** ';
62
+ break;
63
+ case 'em': case 'i':
64
+ md += '*' + el.textContent?.trim() + '* ';
65
+ break;
66
+ case 'code':
67
+ md += '`' + el.textContent?.trim() + '` ';
68
+ break;
69
+ case 'pre': {
70
+ const code = el.querySelector('code');
71
+ const lang = code?.getAttribute('class')?.replace(/^language-/, '') || '';
72
+ md += '\n```' + lang + '\n' + (code || el).textContent + '\n```\n\n';
73
+ break;
74
+ }
75
+ case 'blockquote':
76
+ md += '\n> ' + el.textContent?.trim().replace(/\n/g, '\n> ') + '\n\n';
77
+ break;
78
+ case 'table': {
79
+ const rows = el.querySelectorAll('tr');
80
+ rows.forEach((row, i) => {
81
+ const cells = row.querySelectorAll('td, th');
82
+ md += '| ' + Array.from(cells).map(c => c.textContent?.trim()).join(' | ') + ' |\n';
83
+ if (i === 0 && row.parentElement?.tagName === 'THEAD') {
84
+ md += '|' + Array.from(cells).map(() => ' --- ').join('|') + '|\n';
85
+ }
86
+ });
87
+ md += '\n';
88
+ break;
89
+ }
90
+ default:
91
+ el.childNodes.forEach(child => processNode(child, depth));
92
+ }
93
+ }
94
+
95
+ doc.body?.childNodes.forEach((child: globalThis.Node) => processNode(child));
96
+ return md.replace(/\n{3,}/g, '\n\n').trim();
97
+ }
98
+
99
+ function htmlToXml(html: string, root = 'document'): string {
100
+ const dom = new JSDOM(html);
101
+ const doc = dom.window.document;
102
+
103
+ function serialize(el: globalThis.Element, depth = 0): string {
104
+ const indent = ' '.repeat(depth);
105
+ const tag = el.tagName.toLowerCase();
106
+ if (el.childNodes.length === 0) {
107
+ return `${indent}<${tag} />\n`;
108
+ }
109
+ const text = el.textContent?.trim();
110
+ if (el.childNodes.length === 1 && el.firstChild?.nodeType === 3 && text) {
111
+ return `${indent}<${tag}>${escapeXml(text)}</${tag}>\n`;
112
+ }
113
+ let xml = `${indent}<${tag}>\n`;
114
+ el.childNodes.forEach(child => {
115
+ if (child.nodeType === 1) xml += serialize(child as globalThis.Element, depth + 1);
116
+ else if (child.nodeType === 3 && (child as globalThis.Text).textContent?.trim()) {
117
+ xml += `${indent} ${escapeXml((child as globalThis.Text).textContent!.trim())}\n`;
118
+ }
119
+ });
120
+ xml += `${indent}</${tag}>\n`;
121
+ return xml;
122
+ }
123
+
124
+ function escapeXml(s: string): string {
125
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
126
+ }
127
+
128
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<${root}>\n`;
129
+ doc.body?.childNodes.forEach((child: globalThis.Node) => {
130
+ if (child.nodeType === 1) xml += serialize(child as globalThis.Element, 1);
131
+ });
132
+ xml += `</${root}>\n`;
133
+ return xml;
134
+ }
135
+
136
+ function htmlToRedisJson(html: string, key = 'page'): string {
137
+ const dom = new JSDOM(html);
138
+ const doc = dom.window.document;
139
+
140
+ function extract(el: globalThis.Element): Record<string, unknown> {
141
+ const tag = el.tagName.toLowerCase();
142
+ const children = Array.from(el.children);
143
+ if (children.length === 0) return { _tag: tag, _text: el.textContent?.trim() || '' };
144
+ const result: Record<string, unknown> = { _tag: tag };
145
+ if (el.getAttribute('href')) result.href = el.getAttribute('href')!;
146
+ if (el.getAttribute('src')) result.src = el.getAttribute('src')!;
147
+ if (el.getAttribute('alt')) result.alt = el.getAttribute('alt')!;
148
+ if (el.getAttribute('class')) result.class = el.getAttribute('class')!;
149
+ children.forEach(child => {
150
+ const childTag = child.tagName.toLowerCase();
151
+ if (result[childTag]) {
152
+ if (!Array.isArray(result[childTag])) result[childTag] = [result[childTag]];
153
+ (result[childTag] as unknown[]).push(extract(child));
154
+ } else {
155
+ result[childTag] = extract(child);
156
+ }
157
+ });
158
+ return result;
159
+ }
160
+
161
+ const title = doc.title;
162
+ const bodyArr: Record<string, unknown>[] = [];
163
+ doc.body?.childNodes.forEach((child: globalThis.Node) => {
164
+ if (child.nodeType === 1) bodyArr.push(extract(child as globalThis.Element));
165
+ });
166
+
167
+ const json: Record<string, unknown> = {
168
+ title,
169
+ body: bodyArr,
170
+ links: Array.from(doc.querySelectorAll('a[href]')).map((a) => ({
171
+ href: (a as globalThis.Element).getAttribute('href'),
172
+ text: (a as globalThis.Element).textContent?.trim(),
173
+ })),
174
+ images: Array.from(doc.querySelectorAll('img[src]')).map((img) => ({
175
+ src: (img as globalThis.Element).getAttribute('src'),
176
+ alt: (img as globalThis.Element).getAttribute('alt'),
177
+ })),
178
+ };
179
+
180
+ return `JSON.SET ${key} $ ${JSON.stringify(json, null, 2)}`;
181
+ }
182
+
183
+ function plainText(html: string): string {
184
+ return html
185
+ .replace(/<style[^>]*>.*?<\/style>/gs, '')
186
+ .replace(/<script[^>]*>.*?<\/script>/gs, '')
187
+ .replace(/<br\s*\/?>/gi, '\n')
188
+ .replace(/<\/p>/gi, '\n\n')
189
+ .replace(/<\/h[1-6]>/gi, '\n')
190
+ .replace(/<\/li>/gi, '\n')
191
+ .replace(/<\/tr>/gi, '\n')
192
+ .replace(/<\/td>/gi, ' | ')
193
+ .replace(/<\/th>/gi, ' | ')
194
+ .replace(/<[^>]+>/g, '')
195
+ .replace(/&nbsp;/g, ' ')
196
+ .replace(/&amp;/g, '&')
197
+ .replace(/&lt;/g, '<')
198
+ .replace(/&gt;/g, '>')
199
+ .replace(/&quot;/g, '"')
200
+ .replace(/&#39;/g, "'")
201
+ .replace(/\n{3,}/g, '\n\n')
202
+ .trim();
203
+ }
204
+
205
+ function tableToMarkdown(headers: string[], rows: string[][], caption?: string): string {
206
+ let md = caption ? `### ${caption}\n\n` : '';
207
+ md += '| ' + headers.join(' | ') + ' |\n';
208
+ md += '|' + headers.map(() => ' --- ').join('|') + '|\n';
209
+ for (const row of rows) {
210
+ md += '| ' + row.join(' | ') + ' |\n';
211
+ }
212
+ return md + '\n';
213
+ }
214
+
215
+ export function serializeResult(result: ScrapeResult, options: SerializeOptions): SerializedOutput {
216
+ let content = '';
217
+
218
+ if (options.includeMetadata !== false) {
219
+ content += `# ${result.title}\n\n`;
220
+ content += `- **URL:** ${result.url}\n`;
221
+ content += `- **Extracted:** ${result.extractedAt}\n`;
222
+ if (result.metadata.author) content += `- **Author:** ${result.metadata.author}\n`;
223
+ if (result.metadata.publishedDate) content += `- **Published:** ${result.metadata.publishedDate}\n`;
224
+ content += `- **Size:** ${result.size} bytes\n\n---\n\n`;
225
+ }
226
+
227
+ switch (options.format) {
228
+ case 'markdown':
229
+ content += result.content;
230
+ if (result.tables.length) {
231
+ content += '\n\n## Tables\n\n';
232
+ for (const t of result.tables) {
233
+ content += tableToMarkdown(t.headers, t.rows, t.caption);
234
+ }
235
+ }
236
+ break;
237
+
238
+ case 'text':
239
+ content += result.text;
240
+ break;
241
+
242
+ case 'xml':
243
+ content = htmlToXml(`<html><head><title>${result.title}</title></head><body>${result.content}</body></html>`, options.xmlRoot || 'document');
244
+ break;
245
+
246
+ case 'json':
247
+ content = JSON.stringify({
248
+ title: result.title,
249
+ url: result.url,
250
+ metadata: result.metadata,
251
+ content: result.content,
252
+ links: result.links,
253
+ images: result.images,
254
+ tables: result.tables,
255
+ extractedAt: result.extractedAt,
256
+ duration: result.duration,
257
+ }, null, options.pretty ? 2 : undefined);
258
+ break;
259
+
260
+ case 'redis':
261
+ content = htmlToRedisJson(`<html><head><title>${result.title}</title></head><body>${result.content}</body></html>`, options.redisKey || `page:${encodeURIComponent(result.url)}`);
262
+ break;
263
+ }
264
+
265
+ const chunks = options.maxChunkSize ? chunkText(content, options.maxChunkSize) : [];
266
+
267
+ const output: SerializedOutput = {
268
+ format: options.format,
269
+ content,
270
+ chunks: chunks.length ? chunks : undefined,
271
+ size: content.length,
272
+ };
273
+
274
+ if (options.includeMetadata) {
275
+ output.metadata = {
276
+ title: result.title,
277
+ url: result.url,
278
+ format: result.format,
279
+ extractedAt: result.extractedAt,
280
+ duration: result.duration,
281
+ linkCount: result.links.length,
282
+ imageCount: result.images.length,
283
+ tableCount: result.tables.length,
284
+ };
285
+ }
286
+
287
+ return output;
288
+ }
289
+
290
+ export function serializeHtml(html: string, options: SerializeOptions): SerializedOutput {
291
+ const content = (() => {
292
+ switch (options.format) {
293
+ case 'markdown': return htmlToMarkdown(html);
294
+ case 'xml': return htmlToXml(html, options.xmlRoot || 'document');
295
+ case 'json': return JSON.stringify({ html }, null, options.pretty ? 2 : undefined);
296
+ case 'redis': return htmlToRedisJson(html, options.redisKey || 'page:1');
297
+ case 'text': return plainText(html);
298
+ default: return plainText(html);
299
+ }
300
+ })();
301
+
302
+ const chunks = options.maxChunkSize ? chunkText(content, options.maxChunkSize) : [];
303
+
304
+ return {
305
+ format: options.format,
306
+ content,
307
+ chunks: chunks.length ? chunks : undefined,
308
+ size: content.length,
309
+ };
310
+ }
@@ -0,0 +1,137 @@
1
+ import { Hono } from 'hono';
2
+ import { cors } from 'hono/cors';
3
+ import { scrapeUrl } from './scrape.js';
4
+ import { batchScrape } from './batch.js';
5
+ import { crawl } from './crawler.js';
6
+ import { formatForAgent } from './agent.js';
7
+ import { randomPort } from './utils/port.js';
8
+ import type { ScrapeOptions } from './types.js';
9
+
10
+ export interface ServerConfig {
11
+ port?: number;
12
+ host?: string;
13
+ apiKey?: string;
14
+ }
15
+
16
+ const app = new Hono();
17
+
18
+ app.use('*', cors());
19
+
20
+ const api = new Hono();
21
+
22
+ api.post('/scrape', async (c) => {
23
+ const body = await c.req.json();
24
+ const { url, options = {} } = body;
25
+
26
+ if (!url) {
27
+ return c.json({ error: { code: 'MISSING_URL', message: 'URL is required' } }, 400);
28
+ }
29
+
30
+ try {
31
+ const result = await scrapeUrl(url, options as ScrapeOptions);
32
+ return c.json({ status: 'ok', data: result });
33
+ } catch (error) {
34
+ return c.json({
35
+ error: {
36
+ code: (error as any).code || 'SCRAPE_ERROR',
37
+ message: (error as Error).message,
38
+ }
39
+ }, (error as any).statusCode || 500);
40
+ }
41
+ });
42
+
43
+ api.post('/scrape/agent', async (c) => {
44
+ const body = await c.req.json();
45
+ const { url, options = {} } = body;
46
+
47
+ if (!url) {
48
+ return c.json({ error: { code: 'MISSING_URL', message: 'URL is required' } }, 400);
49
+ }
50
+
51
+ try {
52
+ const result = await scrapeUrl(url, options as ScrapeOptions);
53
+ const agentOutput = formatForAgent(result);
54
+ return c.json({ status: 'ok', data: agentOutput });
55
+ } catch (error) {
56
+ return c.json({
57
+ error: {
58
+ code: (error as any).code || 'SCRAPE_ERROR',
59
+ message: (error as Error).message,
60
+ }
61
+ }, (error as any).statusCode || 500);
62
+ }
63
+ });
64
+
65
+ api.post('/batch', async (c) => {
66
+ const body = await c.req.json();
67
+ const { urls, options = {}, scrapeOptions = {} } = body;
68
+
69
+ if (!urls || !Array.isArray(urls) || urls.length === 0) {
70
+ return c.json({ error: { code: 'MISSING_URLS', message: 'urls array is required' } }, 400);
71
+ }
72
+
73
+ try {
74
+ const result = await batchScrape({ urls, ...options }, scrapeOptions as ScrapeOptions);
75
+ return c.json({
76
+ status: 'ok',
77
+ data: {
78
+ successful: result.successful,
79
+ failed: result.failed,
80
+ duration: result.duration,
81
+ results: result.results,
82
+ errors: result.errors.map(e => ({ url: e.url, error: e.error.message })),
83
+ },
84
+ });
85
+ } catch (error) {
86
+ return c.json({
87
+ error: {
88
+ code: 'BATCH_ERROR',
89
+ message: (error as Error).message,
90
+ }
91
+ }, 500);
92
+ }
93
+ });
94
+
95
+ api.post('/crawl', async (c) => {
96
+ const body = await c.req.json();
97
+ const { url, options = {} } = body;
98
+
99
+ if (!url) {
100
+ return c.json({ error: { code: 'MISSING_URL', message: 'URL is required' } }, 400);
101
+ }
102
+
103
+ try {
104
+ const result = await crawl(url, options);
105
+ return c.json({
106
+ status: 'ok',
107
+ data: {
108
+ stats: result.stats,
109
+ results: result.results.slice(0, 100),
110
+ },
111
+ });
112
+ } catch (error) {
113
+ return c.json({
114
+ error: {
115
+ code: 'CRAWL_ERROR',
116
+ message: (error as Error).message,
117
+ }
118
+ }, 500);
119
+ }
120
+ });
121
+
122
+ api.get('/health', (c) => {
123
+ return c.json({ status: 'ok', version: '2.0.0', uptime: process.uptime() });
124
+ });
125
+
126
+ app.route('/v1', api);
127
+
128
+ export function createServer(config: ServerConfig = {}) {
129
+ return {
130
+ fetch: app.fetch,
131
+ port: config.port || randomPort(),
132
+ host: config.host || '0.0.0.0',
133
+ };
134
+ }
135
+
136
+ export { app };
137
+ export default app;
@@ -0,0 +1,109 @@
1
+ export interface Cookie {
2
+ name: string;
3
+ value: string;
4
+ domain?: string;
5
+ path?: string;
6
+ expires?: number;
7
+ httpOnly?: boolean;
8
+ secure?: boolean;
9
+ }
10
+
11
+ export class CookieJar {
12
+ private cookies = new Map<string, Cookie>();
13
+
14
+ set(cookie: Cookie): void {
15
+ const key = `${cookie.domain || ''}:${cookie.path || '/'}:${cookie.name}`;
16
+ this.cookies.set(key, cookie);
17
+ }
18
+
19
+ get(name: string, domain?: string, _path?: string): Cookie | undefined {
20
+ for (const [key, cookie] of this.cookies) {
21
+ if (cookie.name !== name) continue;
22
+ if (domain && cookie.domain && !domain.includes(cookie.domain)) continue;
23
+ if (cookie.expires && cookie.expires < Date.now()) {
24
+ this.cookies.delete(key);
25
+ continue;
26
+ }
27
+ return cookie;
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ getAll(domain?: string): Cookie[] {
33
+ const result: Cookie[] = [];
34
+ const now = Date.now();
35
+ for (const [key, cookie] of this.cookies) {
36
+ if (cookie.expires && cookie.expires < now) {
37
+ this.cookies.delete(key);
38
+ continue;
39
+ }
40
+ if (domain && cookie.domain && !domain.includes(cookie.domain)) continue;
41
+ result.push(cookie);
42
+ }
43
+ return result;
44
+ }
45
+
46
+ toString(domain?: string): string {
47
+ return this.getAll(domain)
48
+ .map(c => `${c.name}=${c.value}`)
49
+ .join('; ');
50
+ }
51
+
52
+ parseSetCookie(header: string, defaultDomain?: string): void {
53
+ const parts = header.split(';').map(s => s.trim());
54
+ const [nameValue, ...attrs] = parts;
55
+ const [name, ...valueParts] = nameValue.split('=');
56
+ const value = valueParts.join('=');
57
+
58
+ const cookie: Cookie = {
59
+ name: name.trim(),
60
+ value: value.trim(),
61
+ domain: defaultDomain,
62
+ path: '/',
63
+ };
64
+
65
+ for (const attr of attrs) {
66
+ const [key, val] = attr.split('=').map(s => s.trim());
67
+ const lower = key.toLowerCase();
68
+ if (lower === 'domain') cookie.domain = val;
69
+ else if (lower === 'path') cookie.path = val;
70
+ else if (lower === 'expires') cookie.expires = new Date(val).getTime();
71
+ else if (lower === 'httponly') cookie.httpOnly = true;
72
+ else if (lower === 'secure') cookie.secure = true;
73
+ }
74
+
75
+ this.set(cookie);
76
+ }
77
+
78
+ clear(): void {
79
+ this.cookies.clear();
80
+ }
81
+ }
82
+
83
+ export class ScrapingSession {
84
+ public readonly cookieJar: CookieJar;
85
+ public readonly id: string;
86
+ private data = new Map<string, unknown>();
87
+
88
+ constructor(id?: string) {
89
+ this.id = id || crypto.randomUUID();
90
+ this.cookieJar = new CookieJar();
91
+ }
92
+
93
+ get<T = unknown>(key: string): T | undefined {
94
+ return this.data.get(key) as T;
95
+ }
96
+
97
+ set(key: string, value: unknown): void {
98
+ this.data.set(key, value);
99
+ }
100
+
101
+ clear(): void {
102
+ this.cookieJar.clear();
103
+ this.data.clear();
104
+ }
105
+ }
106
+
107
+ export function createSession(id?: string): ScrapingSession {
108
+ return new ScrapingSession(id);
109
+ }