@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,131 @@
1
+ import { WebScrapeError, ErrorCode } from './errors.js';
2
+
3
+ export interface SitemapUrl {
4
+ loc: string;
5
+ lastmod?: string;
6
+ changefreq?: string;
7
+ priority?: number;
8
+ }
9
+
10
+ export interface SitemapResult {
11
+ urls: SitemapUrl[];
12
+ sitemaps: string[];
13
+ duration: number;
14
+ }
15
+
16
+ function parseSitemapXml(xml: string): SitemapUrl[] {
17
+ const urls: SitemapUrl[] = [];
18
+ const urlMatches = xml.match(/<url[^>]*>([\s\S]*?)<\/url>/gi) || [];
19
+
20
+ for (const urlBlock of urlMatches) {
21
+ const loc = urlBlock.match(/<loc>([\s\S]*?)<\/loc>/i)?.[1]?.trim();
22
+ if (!loc) continue;
23
+
24
+ const lastmod = urlBlock.match(/<lastmod>([\s\S]*?)<\/lastmod>/i)?.[1]?.trim();
25
+ const changefreq = urlBlock.match(/<changefreq>([\s\S]*?)<\/changefreq>/i)?.[1]?.trim();
26
+ const priorityStr = urlBlock.match(/<priority>([\s\S]*?)<\/priority>/i)?.[1]?.trim();
27
+ const priority = priorityStr ? parseFloat(priorityStr) : undefined;
28
+
29
+ urls.push({ loc, lastmod, changefreq, priority });
30
+ }
31
+
32
+ return urls;
33
+ }
34
+
35
+ function extractSitemapUrls(xml: string): string[] {
36
+ const urls: string[] = [];
37
+ const sitemapMatches = xml.match(/<sitemap[^>]*>[\s\S]*?<loc>([\s\S]*?)<\/loc>[\s\S]*?<\/sitemap>/gi) || [];
38
+ for (const match of sitemapMatches) {
39
+ const loc = match.match(/<loc>([\s\S]*?)<\/loc>/i)?.[1]?.trim();
40
+ if (loc) urls.push(loc);
41
+ }
42
+ return urls;
43
+ }
44
+
45
+ export async function fetchSitemap(url: string, options: { timeout?: number; headers?: Record<string, string> } = {}): Promise<SitemapResult> {
46
+ const startTime = Date.now();
47
+
48
+ try {
49
+ const response = await fetch(url, {
50
+ headers: {
51
+ 'User-Agent': 'DevThink-WebScrape/2.0 (sitemap parser)',
52
+ ...options.headers,
53
+ },
54
+ signal: AbortSignal.timeout(options.timeout || 10000),
55
+ });
56
+
57
+ if (!response.ok) {
58
+ throw new WebScrapeError(`Failed to fetch sitemap: ${response.status}`, ErrorCode.NETWORK_ERROR, response.status, false, { url });
59
+ }
60
+
61
+ const text = await response.text();
62
+ const isIndex = text.includes('<sitemapindex');
63
+ const urls = isIndex ? [] : parseSitemapXml(text);
64
+ const sitemaps = isIndex ? extractSitemapUrls(text) : [];
65
+
66
+ return {
67
+ urls,
68
+ sitemaps,
69
+ duration: Date.now() - startTime,
70
+ };
71
+ } catch (error) {
72
+ if (error instanceof WebScrapeError) throw error;
73
+ throw new WebScrapeError(`Failed to parse sitemap: ${(error as Error).message}`, ErrorCode.PARSE_ERROR, 422, false, { url }, error as Error);
74
+ }
75
+ }
76
+
77
+ export async function discoverSitemaps(siteUrl: string): Promise<string[]> {
78
+ const candidates = [
79
+ '/sitemap.xml',
80
+ '/sitemap_index.xml',
81
+ '/sitemap-index.xml',
82
+ '/sitemaps.xml',
83
+ '/robots.txt',
84
+ ];
85
+
86
+ const found: string[] = [];
87
+ const base = siteUrl.replace(/\/$/, '');
88
+
89
+ for (const path of candidates) {
90
+ try {
91
+ const url = path === '/robots.txt' ? `${base}/robots.txt` : `${base}${path}`;
92
+ const response = await fetch(url, {
93
+ headers: { 'User-Agent': 'DevThink-WebScrape/2.0' },
94
+ signal: AbortSignal.timeout(5000),
95
+ });
96
+ if (response.ok) {
97
+ const text = await response.text();
98
+ if (path === '/robots.txt') {
99
+ const sitemapUrls = text.match(/Sitemap:\s*(https?:\/\/[^\s]+)/gi);
100
+ if (sitemapUrls) {
101
+ for (const s of sitemapUrls) {
102
+ found.push(s.replace(/Sitemap:\s*/i, '').trim());
103
+ }
104
+ }
105
+ } else {
106
+ found.push(url);
107
+ }
108
+ }
109
+ } catch {}
110
+ }
111
+
112
+ return [...new Set(found)];
113
+ }
114
+
115
+ export async function parseSitemap(url: string, options: { timeout?: number; followIndexes?: boolean; maxUrls?: number } = {}): Promise<SitemapUrl[]> {
116
+ const { followIndexes = true, maxUrls = 10000 } = options;
117
+ const result = await fetchSitemap(url, options);
118
+ let allUrls = [...result.urls];
119
+
120
+ if (followIndexes && result.sitemaps.length > 0) {
121
+ for (const sitemapUrl of result.sitemaps) {
122
+ if (allUrls.length >= maxUrls) break;
123
+ try {
124
+ const subResult = await fetchSitemap(sitemapUrl, options);
125
+ allUrls.push(...subResult.urls);
126
+ } catch {}
127
+ }
128
+ }
129
+
130
+ return allUrls.slice(0, maxUrls);
131
+ }
@@ -0,0 +1,45 @@
1
+ export type ModelType = 'gpt-4o' | 'gpt-4' | 'gpt-3.5-turbo' | 'claude' | 'gemini' | 'default';
2
+
3
+ const MODEL_TOKEN_RATIOS: Record<ModelType, number> = {
4
+ 'gpt-4o': 3.5,
5
+ 'gpt-4': 3.5,
6
+ 'gpt-3.5-turbo': 4,
7
+ 'claude': 3.2,
8
+ 'gemini': 3.5,
9
+ 'default': 4,
10
+ };
11
+
12
+ export function estimateTokens(text: string, model: ModelType = 'default'): number {
13
+ const ratio = MODEL_TOKEN_RATIOS[model] || 4;
14
+ return Math.ceil(text.length / ratio);
15
+ }
16
+
17
+ export function countTokens(text: string, model: ModelType = 'default'): number {
18
+ return estimateTokens(text, model);
19
+ }
20
+
21
+ export function fitsInContext(text: string, maxTokens: number, model: ModelType = 'default'): boolean {
22
+ return estimateTokens(text, model) <= maxTokens;
23
+ }
24
+
25
+ export function truncateToTokens(text: string, maxTokens: number, model: ModelType = 'default'): string {
26
+ const estimatedTokens = estimateTokens(text, model);
27
+ if (estimatedTokens <= maxTokens) return text;
28
+ const charLimit = Math.floor(maxTokens * (MODEL_TOKEN_RATIOS[model] || 4));
29
+ return text.slice(0, charLimit) + '...';
30
+ }
31
+
32
+ export function tokenCost(text: string, model: ModelType, pricePer1kTokens: number): number {
33
+ const tokens = estimateTokens(text, model);
34
+ return (tokens / 1000) * pricePer1kTokens;
35
+ }
36
+
37
+ export const MODEL_PRICES: Record<string, { input: number; output: number }> = {
38
+ 'gpt-4o': { input: 0.0025, output: 0.01 },
39
+ 'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
40
+ 'gpt-4-turbo': { input: 0.01, output: 0.03 },
41
+ 'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
42
+ 'claude-3-haiku': { input: 0.00025, output: 0.00125 },
43
+ 'gemini-1.5-pro': { input: 0.00125, output: 0.005 },
44
+ 'gemini-1.5-flash': { input: 0.000075, output: 0.0003 },
45
+ };
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "ESNext",
5
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "outDir": "./dist",
12
+ "rootDir": ".",
13
+ "noEmit": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "resolveJsonModule": true,
18
+ "isolatedModules": true,
19
+ "verbatimModuleSyntax": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noImplicitReturns": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "types": ["node"]
25
+ },
26
+ "include": ["*.ts"],
27
+ "exclude": ["node_modules", "dist", "web", "vite.config.ts", "vitest.config.ts", "tests"]
28
+ }
@@ -0,0 +1,214 @@
1
+ import { z } from 'zod';
2
+
3
+ export const ScrapeFormatSchema = z.enum(['markdown', 'html', 'text', 'json', 'xml', 'redis']);
4
+ export type ScrapeFormat = z.infer<typeof ScrapeFormatSchema>;
5
+
6
+ export const SerializeTargetSchema = z.enum(['markdown', 'xml', 'json', 'redis', 'text']);
7
+ export type SerializeTarget = z.infer<typeof SerializeTargetSchema>;
8
+
9
+ export const ScrapeOptionsSchema = z.strictObject({
10
+ url: z.string().url().optional(),
11
+ html: z.string().optional(),
12
+ mode: z.enum(['auto', 'fetch', 'browser']).prefault('auto').optional(),
13
+ waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle', 'commit']).prefault('networkidle').optional(),
14
+ timeout: z.number().positive().prefault(30000).optional(),
15
+ scroll: z.boolean().prefault(false).optional(),
16
+ scrollDelay: z.number().nonnegative().prefault(300).optional(),
17
+ maxScrolls: z.number().positive().prefault(50).optional(),
18
+ removeSelectors: z.array(z.string()).prefault([]).optional(),
19
+ extractImages: z.boolean().prefault(true).optional(),
20
+ extractLinks: z.boolean().prefault(true).optional(),
21
+ extractTables: z.boolean().prefault(true).optional(),
22
+ maxContentLength: z.number().positive().optional(),
23
+ format: ScrapeFormatSchema.prefault('markdown').optional(),
24
+ language: z.string().optional(),
25
+ proxy: z.string().optional(),
26
+ retries: z.number().nonnegative().prefault(3).optional(),
27
+ retryDelay: z.number().positive().prefault(1000).optional(),
28
+ userAgent: z.string().optional(),
29
+ headers: z.record(z.string(), z.string()).prefault({}).optional(),
30
+ cache: z.boolean().prefault(false).optional(),
31
+ cacheTtlMs: z.number().positive().prefault(300000).optional(),
32
+ readable: z.boolean().optional(),
33
+ });
34
+ export type ScrapeOptions = z.infer<typeof ScrapeOptionsSchema>;
35
+
36
+ export interface ScrapeResult {
37
+ url: string;
38
+ title: string;
39
+ content: string;
40
+ format: ScrapeFormat;
41
+ text: string;
42
+ links: LinkInfo[];
43
+ images: ImageInfo[];
44
+ tables: TableInfo[];
45
+ metadata: PageMetadata;
46
+ extractedAt: string;
47
+ duration: number;
48
+ size: number;
49
+ }
50
+
51
+ export interface PageMetadata {
52
+ title: string;
53
+ description?: string;
54
+ favicon?: string;
55
+ charset?: string;
56
+ language?: string;
57
+ author?: string;
58
+ publishedDate?: string;
59
+ ogImage?: string;
60
+ ogType?: string;
61
+ keywords?: string[];
62
+ }
63
+
64
+ export interface LinkInfo {
65
+ href: string;
66
+ text: string;
67
+ isInternal: boolean;
68
+ isExternal: boolean;
69
+ }
70
+
71
+ export interface ImageInfo {
72
+ src: string;
73
+ alt: string;
74
+ width?: number;
75
+ height?: number;
76
+ }
77
+
78
+ export interface TableInfo {
79
+ headers: string[];
80
+ rows: string[][];
81
+ caption?: string;
82
+ }
83
+
84
+ export const BrowserAgentConfigSchema = z.strictObject({
85
+ headless: z.boolean().prefault(true).optional(),
86
+ userAgent: z.string().optional(),
87
+ viewport: z
88
+ .strictObject({
89
+ width: z.number().positive().prefault(1280),
90
+ height: z.number().positive().prefault(720),
91
+ })
92
+ .prefault({ width: 1280, height: 720 })
93
+ .optional(),
94
+ locale: z.string().prefault('en-US').optional(),
95
+ geolocation: z
96
+ .strictObject({
97
+ latitude: z.number(),
98
+ longitude: z.number(),
99
+ })
100
+ .optional(),
101
+ timeout: z.number().positive().prefault(30000).optional(),
102
+ recordVideo: z.boolean().prefault(false).optional(),
103
+ proxy: z.string().prefault('').optional(),
104
+ storageState: z.string().prefault('').optional(),
105
+ blockAds: z.boolean().prefault(true).optional(),
106
+ stealth: z.boolean().prefault(true).optional(),
107
+ });
108
+ export type BrowserAgentConfig = z.infer<typeof BrowserAgentConfigSchema>;
109
+
110
+ export interface ExtractOptions {
111
+ readable?: boolean;
112
+ stripTags?: string[];
113
+ preserveLinks?: boolean;
114
+ preserveImages?: boolean;
115
+ preserveTables?: boolean;
116
+ maxLength?: number;
117
+ removeSelectors?: string[];
118
+ baseUrl?: string;
119
+ }
120
+
121
+ export interface SerializeOptions {
122
+ format: SerializeTarget;
123
+ pretty?: boolean;
124
+ maxChunkSize?: number;
125
+ includeMetadata?: boolean;
126
+ redisKey?: string;
127
+ xmlRoot?: string;
128
+ }
129
+
130
+ export interface SerializedOutput {
131
+ format: SerializeTarget;
132
+ content: string;
133
+ chunks?: string[];
134
+ size: number;
135
+ metadata?: Record<string, unknown>;
136
+ }
137
+
138
+ export interface AgentOutput {
139
+ summary: string;
140
+ content: string;
141
+ keyPoints: string[];
142
+ relevantUrls: string[];
143
+ structured?: Record<string, unknown>;
144
+ tokens: number;
145
+ }
146
+
147
+ export interface RenderOptions {
148
+ width?: number;
149
+ height?: number;
150
+ fullPage?: boolean;
151
+ type?: 'png' | 'jpeg';
152
+ quality?: number;
153
+ output?: string;
154
+ }
155
+
156
+ export interface ChromeCommand {
157
+ action: 'goto' | 'click' | 'type' | 'screenshot' | 'evaluate' | 'wait' | 'scroll' | 'extract';
158
+ args?: Record<string, unknown>;
159
+ }
160
+
161
+ export interface CliOptions {
162
+ url?: string;
163
+ file?: string;
164
+ format?: ScrapeFormat;
165
+ output?: string;
166
+ 'no-headless'?: boolean;
167
+ timeout?: number;
168
+ scroll?: boolean;
169
+ readable?: boolean;
170
+ agent?: boolean;
171
+ pretty?: boolean;
172
+ proxy?: string;
173
+ retries?: number;
174
+ mode?: 'auto' | 'fetch' | 'browser';
175
+ }
176
+
177
+ export interface RetryConfig {
178
+ retries: number;
179
+ delay: number;
180
+ backoff: 'exponential' | 'linear' | 'constant';
181
+ maxDelay?: number;
182
+ statusCodes?: number[];
183
+ }
184
+
185
+ export interface ProxyConfig {
186
+ url: string;
187
+ username?: string;
188
+ password?: string;
189
+ }
190
+
191
+ export interface CacheConfig {
192
+ enabled: boolean;
193
+ ttlMs?: number;
194
+ maxEntries?: number;
195
+ storage?: 'memory' | 'redis' | 'sqlite' | 'indexeddb';
196
+ }
197
+
198
+ export interface CrawlStats {
199
+ totalUrls: number;
200
+ successful: number;
201
+ failed: number;
202
+ skipped: number;
203
+ duration: number;
204
+ avgResponseTime: number;
205
+ }
206
+
207
+ export interface BatchOptions {
208
+ urls: string[];
209
+ concurrency?: number;
210
+ mode?: 'auto' | 'fetch' | 'browser';
211
+ format?: ScrapeFormat;
212
+ onProgress?: (completed: number, total: number, currentUrl: string) => void;
213
+ onError?: (url: string, error: Error) => void;
214
+ }
@@ -0,0 +1,77 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+
4
+ export function ensureDir(path: string): void {
5
+ const dir = dirname(path);
6
+ if (!existsSync(dir)) {
7
+ mkdirSync(dir, { recursive: true });
8
+ }
9
+ }
10
+
11
+ export function writeOutput(path: string, content: string): void {
12
+ ensureDir(path);
13
+ writeFileSync(path, content, 'utf-8');
14
+ }
15
+
16
+ export function slugify(text: string): string {
17
+ return text
18
+ .toLowerCase()
19
+ .replace(/[^\w\s-]/g, '')
20
+ .replace(/[\s_]+/g, '-')
21
+ .replace(/^-+|-+$/g, '');
22
+ }
23
+
24
+ export function truncate(text: string, max: number, suffix = '...'): string {
25
+ if (text.length <= max) return text;
26
+ return text.slice(0, max - suffix.length) + suffix;
27
+ }
28
+
29
+ export function estimateTokens(text: string): number {
30
+ return Math.ceil(text.length / 4);
31
+ }
32
+
33
+ export function chunkText(text: string, chunkSize: number): string[] {
34
+ const chunks: string[] = [];
35
+ let i = 0;
36
+ while (i < text.length) {
37
+ let end = i + chunkSize;
38
+ if (end < text.length) {
39
+ const breakIdx = text.lastIndexOf('\n', end);
40
+ if (breakIdx > i) end = breakIdx + 1;
41
+ }
42
+ chunks.push(text.slice(i, end).trim());
43
+ i = end;
44
+ }
45
+ return chunks;
46
+ }
47
+
48
+ export function delay(ms: number): Promise<void> {
49
+ return new Promise(r => setTimeout(r, ms));
50
+ }
51
+
52
+ export function isValidUrl(url: string): boolean {
53
+ try {
54
+ new URL(url);
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ export function normalizeUrl(url: string, base?: string): string {
62
+ try {
63
+ return new URL(url, base).href;
64
+ } catch {
65
+ return url;
66
+ }
67
+ }
68
+
69
+ export function isInternalUrl(href: string, base: string): boolean {
70
+ try {
71
+ const baseUrl = new URL(base);
72
+ const target = new URL(href, base);
73
+ return target.hostname === baseUrl.hostname;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
@@ -0,0 +1,55 @@
1
+ import { defineConfig } from "vite";
2
+ import dts from "vite-plugin-dts";
3
+ import { resolve } from "node:path";
4
+ import { randomPort } from './utils/port.js';
5
+
6
+ const VITE_PORT = randomPort();
7
+
8
+ export default defineConfig({
9
+ root: "web",
10
+ build: {
11
+ lib: {
12
+ entry: resolve(__dirname, "index.ts"),
13
+ name: "DevThinkWebScrape",
14
+ formats: ["es", "cjs"],
15
+ fileName: (format) => `index.${format === "es" ? "mjs" : "js"}`,
16
+ },
17
+ rollupOptions: {
18
+ external: [
19
+ "playwright",
20
+ "cheerio",
21
+ "jsdom",
22
+ "turndown",
23
+ "turndown-plugin-gfm",
24
+ "@mozilla/readability",
25
+ "zod",
26
+ "p-retry",
27
+ "p-limit",
28
+ "p-queue",
29
+ "robots-parser",
30
+ "sitemapper",
31
+ "lru-cache",
32
+ "keyv",
33
+ "hono",
34
+ "hono/cors",
35
+ "commander",
36
+ "emittery",
37
+ "normalize-url",
38
+ "node:fs",
39
+ "node:fs/promises",
40
+ "node:path",
41
+ "node:child_process",
42
+ "node:http",
43
+ "node:crypto",
44
+ ],
45
+ },
46
+ sourcemap: true,
47
+ minify: false,
48
+ },
49
+ plugins: [dts()],
50
+ server: {
51
+ port: VITE_PORT,
52
+ host: "127.0.0.1",
53
+ cors: true,
54
+ },
55
+ });
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "jsdom",
6
+ include: ["tests/**/*.{test,spec}.{ts,mjs}", "tests/**/*.test.ts"],
7
+ globals: true,
8
+ testTimeout: 10000,
9
+ coverage: {
10
+ provider: "v8",
11
+ reporter: ["text", "json", "html"],
12
+ include: ["*.ts"],
13
+ exclude: ["tests/**", "scripts/**", "web/**", "ink-components/**", "node_modules/**", "dist/**", "coverage/**"],
14
+ thresholds: { lines: 60, functions: 50, branches: 50, statements: 60 },
15
+ },
16
+ },
17
+ });
@@ -5,14 +5,14 @@ import { workflowinputs } from "./manifest.js";
5
5
 
6
6
  export function githubworkflow(manifest) {
7
7
  const input = workflowinputs(manifest);
8
- return `name: ${manifest.name}\non:\n workflow_dispatch:\n inputs:\n jobid:\n required: true\n type: string\n command:\n required: true\n type: string\n default: ${manifest.command}\n repository_dispatch:\n types: [saddle-job]\npermissions:\n contents: read\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${input.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm ci\n - run: \${{ github.event.inputs.command || '${manifest.command}' }}\n env:\n SBOT_JOB_ID: \${{ github.event.inputs.jobid || github.event.client_payload.jobid }}\n - uses: actions/upload-artifact@v4\n with:\n name: saddle-results\n path: ${manifest.artifacts.join("\n ")}\n if-no-files-found: ignore\n`;
8
+ return `name: ${manifest.name}\non:\n workflow_dispatch:\n inputs:\n jobid:\n required: true\n type: string\n command:\n required: true\n type: string\n default: ${manifest.command}\n repository_dispatch:\n types: [saddle-job]\npermissions:\n contents: read\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${input.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 26.7.0\n - run: npm ci\n - run: \${{ github.event.inputs.command || '${manifest.command}' }}\n env:\n SBOT_JOB_ID: \${{ github.event.inputs.jobid || github.event.client_payload.jobid }}\n - uses: actions/upload-artifact@v4\n with:\n name: saddle-results\n path: ${manifest.artifacts.join("\n ")}\n if-no-files-found: ignore\n`;
9
9
  }
10
10
 
11
11
  export function forgejoworkflow(manifest) { return genericworkflow(manifest, "forgejo"); }
12
12
  export function giteaworkflow(manifest) { return genericworkflow(manifest, "gitea"); }
13
- export function woodpeckerworkflow(manifest) { return `when:\n - event: push\n - event: manual\nsteps:\n process:\n image: node:22\n commands:\n - npm ci\n - ${manifest.command}\n`;
13
+ export function woodpeckerworkflow(manifest) { return `when:\n - event: push\n - event: manual\nsteps:\n process:\n image: node:26.7.0\n commands:\n - npm ci\n - ${manifest.command}\n`;
14
14
  }
15
- export function gitlabworkflow(manifest) { return `stages:\n - process\nprocess:\n stage: process\n image: node:22\n script:\n - npm ci\n - ${manifest.command}\n artifacts:\n when: always\n paths:\n${manifest.artifacts.map((item) => ` - ${item}`).join("\n")}\n`; }
15
+ export function gitlabworkflow(manifest) { return `stages:\n - process\nprocess:\n stage: process\n image: node:26.7.0\n script:\n - npm ci\n - ${manifest.command}\n artifacts:\n when: always\n paths:\n${manifest.artifacts.map((item) => ` - ${item}`).join("\n")}\n`; }
16
16
 
17
- function genericworkflow(manifest, name) { return `name: ${manifest.name}\non:\n workflow_dispatch:\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${manifest.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npm ci\n - run: ${manifest.command}\n`;
17
+ function genericworkflow(manifest, name) { return `name: ${manifest.name}\non:\n workflow_dispatch:\njobs:\n process:\n runs-on: ubuntu-latest\n timeout-minutes: ${manifest.timeoutminutes}\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 26.7.0\n - run: npm ci\n - run: ${manifest.command}\n`;
18
18
  }