codeply-cli 0.5.1 → 0.5.2

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 (387) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +65 -63
  3. package/bin/codeply.js +1 -1
  4. package/lib/agent.mjs +133 -24
  5. package/lib/ai.js +268 -9
  6. package/lib/ai.mjs +6 -6
  7. package/lib/apply-patch.js +217 -217
  8. package/lib/arity-table.json +138 -138
  9. package/lib/arity.js +70 -70
  10. package/lib/auth.mjs +6 -6
  11. package/lib/bots.js +82 -17
  12. package/lib/chatgpt.js +282 -0
  13. package/lib/cloud-browser.mjs +143 -143
  14. package/lib/cloud.mjs +1186 -1144
  15. package/lib/commands.js +103 -103
  16. package/lib/config.js +83 -3
  17. package/lib/design-library/assemble-artifact.mjs +21 -21
  18. package/lib/design-library/cleanup.mjs +45 -45
  19. package/lib/design-library/index.json +22379 -22379
  20. package/lib/design-library/make-table-data.mjs +39 -39
  21. package/lib/diagnostics.js +192 -192
  22. package/lib/edit-engine.js +364 -364
  23. package/lib/github-agent.mjs +311 -311
  24. package/lib/history.js +75 -75
  25. package/lib/mail-watch.js +504 -0
  26. package/lib/mcp.js +258 -258
  27. package/lib/model-sync.js +114 -0
  28. package/lib/native-tools.mjs +16 -1
  29. package/lib/oauth-connectors.js +146 -24
  30. package/lib/permissions.js +109 -109
  31. package/lib/plugins.js +353 -353
  32. package/lib/publish.js +916 -0
  33. package/lib/reach.js +67 -0
  34. package/lib/server.mjs +457 -457
  35. package/lib/session-db.js +220 -220
  36. package/lib/share.js +168 -168
  37. package/lib/skills.js +2 -0
  38. package/lib/snapshot.js +146 -146
  39. package/lib/telemetry.js +102 -0
  40. package/lib/terminal-hints.js +154 -154
  41. package/lib/tools.mjs +316 -14
  42. package/lib/typecheck-worker.js +147 -147
  43. package/lib/web-tools.js +209 -158
  44. package/package.json +49 -49
  45. package/skills/SOURCE.md +58 -58
  46. package/skills/accessibility/SKILL.md +147 -147
  47. package/skills/agent-architecture-audit/SKILL.md +257 -257
  48. package/skills/agent-eval/SKILL.md +147 -147
  49. package/skills/agent-harness-construction/SKILL.md +74 -74
  50. package/skills/agent-introspection-debugging/SKILL.md +154 -154
  51. package/skills/agent-payment-x402/SKILL.md +225 -225
  52. package/skills/agent-self-evaluation/SKILL.md +182 -182
  53. package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
  54. package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
  55. package/skills/agent-sort/SKILL.md +216 -216
  56. package/skills/agentic-engineering/SKILL.md +64 -64
  57. package/skills/agentic-os/SKILL.md +388 -388
  58. package/skills/ai-first-engineering/SKILL.md +52 -52
  59. package/skills/ai-regression-testing/SKILL.md +386 -386
  60. package/skills/android-clean-architecture/SKILL.md +340 -340
  61. package/skills/angular-developer/SKILL.md +155 -155
  62. package/skills/angular-developer/references/angular-animations.md +160 -160
  63. package/skills/angular-developer/references/angular-aria.md +410 -410
  64. package/skills/angular-developer/references/cli.md +86 -86
  65. package/skills/angular-developer/references/component-harnesses.md +59 -59
  66. package/skills/angular-developer/references/component-styling.md +91 -91
  67. package/skills/angular-developer/references/components.md +117 -117
  68. package/skills/angular-developer/references/creating-services.md +97 -97
  69. package/skills/angular-developer/references/data-resolvers.md +69 -69
  70. package/skills/angular-developer/references/define-routes.md +67 -67
  71. package/skills/angular-developer/references/defining-providers.md +72 -72
  72. package/skills/angular-developer/references/di-fundamentals.md +120 -120
  73. package/skills/angular-developer/references/e2e-testing.md +56 -56
  74. package/skills/angular-developer/references/effects.md +83 -83
  75. package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
  76. package/skills/angular-developer/references/host-elements.md +80 -80
  77. package/skills/angular-developer/references/injection-context.md +63 -63
  78. package/skills/angular-developer/references/inputs.md +101 -101
  79. package/skills/angular-developer/references/linked-signal.md +59 -59
  80. package/skills/angular-developer/references/loading-strategies.md +61 -61
  81. package/skills/angular-developer/references/mcp.md +108 -108
  82. package/skills/angular-developer/references/navigate-to-routes.md +69 -69
  83. package/skills/angular-developer/references/outputs.md +86 -86
  84. package/skills/angular-developer/references/reactive-forms.md +122 -122
  85. package/skills/angular-developer/references/rendering-strategies.md +44 -44
  86. package/skills/angular-developer/references/resource.md +77 -77
  87. package/skills/angular-developer/references/route-animations.md +56 -56
  88. package/skills/angular-developer/references/route-guards.md +52 -52
  89. package/skills/angular-developer/references/router-lifecycle.md +45 -45
  90. package/skills/angular-developer/references/router-testing.md +87 -87
  91. package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
  92. package/skills/angular-developer/references/signal-forms.md +795 -795
  93. package/skills/angular-developer/references/signals-overview.md +94 -94
  94. package/skills/angular-developer/references/tailwind-css.md +69 -69
  95. package/skills/angular-developer/references/template-driven-forms.md +114 -114
  96. package/skills/angular-developer/references/testing-fundamentals.md +65 -65
  97. package/skills/api-connector-builder/SKILL.md +121 -121
  98. package/skills/api-design/SKILL.md +524 -524
  99. package/skills/architecture-decision-records/SKILL.md +180 -180
  100. package/skills/article-writing/SKILL.md +80 -80
  101. package/skills/automation-audit-ops/SKILL.md +143 -143
  102. package/skills/autonomous-agent-harness/SKILL.md +274 -274
  103. package/skills/autonomous-loops/SKILL.md +611 -611
  104. package/skills/backend-patterns/SKILL.md +562 -562
  105. package/skills/benchmark/SKILL.md +95 -95
  106. package/skills/benchmark-methodology/SKILL.md +191 -191
  107. package/skills/benchmark-optimization-loop/SKILL.md +71 -71
  108. package/skills/blender-motion-state-inspection/SKILL.md +165 -165
  109. package/skills/blueprint/SKILL.md +106 -106
  110. package/skills/brand-discovery/SKILL.md +145 -145
  111. package/skills/brand-discovery/references/10_purpose-why.md +40 -40
  112. package/skills/brand-discovery/references/20_positioning.md +44 -44
  113. package/skills/brand-discovery/references/30_audience-niche.md +52 -52
  114. package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
  115. package/skills/brand-discovery/references/50_voice-tone.md +59 -59
  116. package/skills/brand-discovery/references/60_narrative-story.md +50 -50
  117. package/skills/brand-discovery/references/70_founder-tension.md +49 -49
  118. package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
  119. package/skills/brand-voice/SKILL.md +98 -98
  120. package/skills/brand-voice/references/voice-profile-schema.md +55 -55
  121. package/skills/browser-qa/SKILL.md +105 -105
  122. package/skills/bun-runtime/SKILL.md +85 -85
  123. package/skills/canary-watch/SKILL.md +108 -108
  124. package/skills/carrier-relationship-management/SKILL.md +212 -212
  125. package/skills/cisco-ios-patterns/SKILL.md +164 -164
  126. package/skills/ck/SKILL.md +148 -148
  127. package/skills/claude-devfleet/SKILL.md +112 -112
  128. package/skills/click-path-audit/SKILL.md +245 -245
  129. package/skills/clickhouse-io/SKILL.md +445 -445
  130. package/skills/code-tour/SKILL.md +254 -254
  131. package/skills/codebase-onboarding/SKILL.md +234 -234
  132. package/skills/codehealth-mcp/SKILL.md +167 -167
  133. package/skills/coding-standards/SKILL.md +551 -551
  134. package/skills/competitive-platform-analysis/SKILL.md +214 -214
  135. package/skills/competitive-report-structure/SKILL.md +162 -162
  136. package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
  137. package/skills/config-gc/SKILL.md +120 -120
  138. package/skills/configure-ecc/SKILL.md +385 -385
  139. package/skills/connections-optimizer/SKILL.md +190 -190
  140. package/skills/content-engine/SKILL.md +132 -132
  141. package/skills/content-hash-cache-pattern/SKILL.md +162 -162
  142. package/skills/context-budget/SKILL.md +136 -136
  143. package/skills/continuous-agent-loop/SKILL.md +46 -46
  144. package/skills/continuous-learning/SKILL.md +132 -132
  145. package/skills/continuous-learning-v2/SKILL.md +361 -361
  146. package/skills/contract-first/SKILL.md +287 -287
  147. package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
  148. package/skills/cost-tracking/SKILL.md +97 -97
  149. package/skills/council/SKILL.md +204 -204
  150. package/skills/cpp-coding-standards/SKILL.md +724 -724
  151. package/skills/cpp-testing/SKILL.md +325 -325
  152. package/skills/crosspost/SKILL.md +112 -112
  153. package/skills/csharp-testing/SKILL.md +322 -322
  154. package/skills/customer-billing-ops/SKILL.md +141 -141
  155. package/skills/customs-trade-compliance/SKILL.md +263 -263
  156. package/skills/dart-flutter-patterns/SKILL.md +564 -564
  157. package/skills/dashboard-builder/SKILL.md +109 -109
  158. package/skills/data-scraper-agent/SKILL.md +765 -765
  159. package/skills/data-throughput-accelerator/SKILL.md +74 -74
  160. package/skills/database-migrations/SKILL.md +430 -430
  161. package/skills/deep-research/SKILL.md +160 -160
  162. package/skills/defi-amm-security/SKILL.md +167 -167
  163. package/skills/delivery-gate/SKILL.md +126 -126
  164. package/skills/deployment-patterns/SKILL.md +428 -428
  165. package/skills/design-system/SKILL.md +83 -83
  166. package/skills/django-celery/SKILL.md +458 -458
  167. package/skills/django-patterns/SKILL.md +735 -735
  168. package/skills/django-security/SKILL.md +644 -644
  169. package/skills/django-tdd/SKILL.md +730 -730
  170. package/skills/django-verification/SKILL.md +470 -470
  171. package/skills/dmux-workflows/SKILL.md +192 -192
  172. package/skills/docker-patterns/SKILL.md +365 -365
  173. package/skills/documentation-lookup/SKILL.md +91 -91
  174. package/skills/dotnet-patterns/SKILL.md +322 -322
  175. package/skills/dynamic-workflow-mode/SKILL.md +124 -124
  176. package/skills/e2e-testing/SKILL.md +327 -327
  177. package/skills/ecc-guide/SKILL.md +190 -190
  178. package/skills/ecc-recipes/SKILL.md +149 -149
  179. package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
  180. package/skills/email-ops/SKILL.md +122 -122
  181. package/skills/energy-procurement/SKILL.md +228 -228
  182. package/skills/enterprise-agent-ops/SKILL.md +51 -51
  183. package/skills/error-handling/SKILL.md +377 -377
  184. package/skills/eval-harness/SKILL.md +271 -271
  185. package/skills/evm-token-decimals/SKILL.md +131 -131
  186. package/skills/exa-search/SKILL.md +108 -108
  187. package/skills/fal-ai-media/SKILL.md +289 -289
  188. package/skills/fastapi-patterns/SKILL.md +514 -514
  189. package/skills/finance-billing-ops/SKILL.md +128 -128
  190. package/skills/flox-environments/SKILL.md +497 -497
  191. package/skills/flutter-dart-code-review/SKILL.md +436 -436
  192. package/skills/foundation-models-on-device/SKILL.md +243 -243
  193. package/skills/frontend-a11y/SKILL.md +446 -446
  194. package/skills/frontend-design-direction/SKILL.md +93 -93
  195. package/skills/frontend-patterns/SKILL.md +657 -657
  196. package/skills/frontend-slides/SKILL.md +185 -185
  197. package/skills/fsharp-testing/SKILL.md +281 -281
  198. package/skills/gan-style-harness/SKILL.md +279 -279
  199. package/skills/gateguard/SKILL.md +133 -133
  200. package/skills/generating-python-installer/SKILL.md +820 -820
  201. package/skills/git-workflow/SKILL.md +716 -716
  202. package/skills/github-ops/SKILL.md +145 -145
  203. package/skills/golang-patterns/SKILL.md +676 -676
  204. package/skills/golang-testing/SKILL.md +721 -721
  205. package/skills/google-workspace-ops/SKILL.md +96 -96
  206. package/skills/growth-log/SKILL.md +128 -128
  207. package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
  208. package/skills/healthcare-emr-patterns/SKILL.md +160 -160
  209. package/skills/healthcare-eval-harness/SKILL.md +208 -208
  210. package/skills/healthcare-phi-compliance/SKILL.md +146 -146
  211. package/skills/hermes-imports/SKILL.md +89 -89
  212. package/skills/hexagonal-architecture/SKILL.md +277 -277
  213. package/skills/hipaa-compliance/SKILL.md +79 -79
  214. package/skills/homelab-network-readiness/SKILL.md +170 -170
  215. package/skills/homelab-network-setup/SKILL.md +130 -130
  216. package/skills/homelab-pihole-dns/SKILL.md +275 -275
  217. package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
  218. package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
  219. package/skills/hookify-rules/SKILL.md +128 -128
  220. package/skills/inherit-legacy-style/SKILL.md +157 -157
  221. package/skills/intent-driven-development/SKILL.md +360 -360
  222. package/skills/inventory-demand-planning/SKILL.md +247 -247
  223. package/skills/investor-materials/SKILL.md +97 -97
  224. package/skills/investor-outreach/SKILL.md +92 -92
  225. package/skills/ios-icon-gen/SKILL.md +158 -158
  226. package/skills/iterative-retrieval/SKILL.md +212 -212
  227. package/skills/ito-basket-compare/SKILL.md +64 -64
  228. package/skills/ito-compute/SKILL.md +129 -129
  229. package/skills/ito-data-atlas-agent/SKILL.md +64 -64
  230. package/skills/ito-market-intelligence/SKILL.md +61 -61
  231. package/skills/ito-trade-planner/SKILL.md +68 -68
  232. package/skills/java-coding-standards/SKILL.md +384 -384
  233. package/skills/jira-integration/SKILL.md +303 -303
  234. package/skills/jpa-patterns/SKILL.md +152 -152
  235. package/skills/knowledge-ops/SKILL.md +155 -155
  236. package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
  237. package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
  238. package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
  239. package/skills/kotlin-patterns/SKILL.md +712 -712
  240. package/skills/kotlin-testing/SKILL.md +825 -825
  241. package/skills/kubernetes-patterns/SKILL.md +756 -756
  242. package/skills/laravel-patterns/SKILL.md +416 -416
  243. package/skills/laravel-plugin-discovery/SKILL.md +230 -230
  244. package/skills/laravel-security/SKILL.md +948 -948
  245. package/skills/laravel-tdd/SKILL.md +675 -675
  246. package/skills/laravel-verification/SKILL.md +180 -180
  247. package/skills/latency-critical-systems/SKILL.md +75 -75
  248. package/skills/lead-intelligence/SKILL.md +322 -322
  249. package/skills/liquid-glass-design/SKILL.md +279 -279
  250. package/skills/llm-trading-agent-security/SKILL.md +147 -147
  251. package/skills/logistics-exception-management/SKILL.md +222 -222
  252. package/skills/loop-design-check/SKILL.md +143 -143
  253. package/skills/mailtrap-email-integration/SKILL.md +77 -77
  254. package/skills/make-interfaces-feel-better/SKILL.md +152 -152
  255. package/skills/manim-video/SKILL.md +90 -90
  256. package/skills/market-research/SKILL.md +76 -76
  257. package/skills/marketing-campaign/SKILL.md +114 -114
  258. package/skills/mcp-server-patterns/SKILL.md +70 -70
  259. package/skills/messages-ops/SKILL.md +105 -105
  260. package/skills/ml-adoption-playbook/SKILL.md +57 -57
  261. package/skills/mle-workflow/SKILL.md +348 -348
  262. package/skills/motion-advanced/SKILL.md +596 -596
  263. package/skills/motion-foundations/SKILL.md +299 -299
  264. package/skills/motion-patterns/SKILL.md +434 -434
  265. package/skills/motion-ui/SKILL.md +576 -576
  266. package/skills/mysql-patterns/SKILL.md +413 -413
  267. package/skills/nanoclaw-repl/SKILL.md +34 -34
  268. package/skills/nestjs-patterns/SKILL.md +231 -231
  269. package/skills/netmiko-ssh-automation/SKILL.md +174 -174
  270. package/skills/network-bgp-diagnostics/SKILL.md +168 -168
  271. package/skills/network-config-validation/SKILL.md +211 -211
  272. package/skills/network-interface-health/SKILL.md +153 -153
  273. package/skills/nextjs-turbopack/SKILL.md +58 -58
  274. package/skills/nodejs-keccak256/SKILL.md +103 -103
  275. package/skills/nutrient-document-processing/SKILL.md +168 -168
  276. package/skills/nuxt4-patterns/SKILL.md +101 -101
  277. package/skills/openclaw-persona-forge/SKILL.md +289 -289
  278. package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
  279. package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
  280. package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
  281. package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
  282. package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
  283. package/skills/openclaw-persona-forge/references/output-template.md +166 -166
  284. package/skills/opensource-pipeline/SKILL.md +256 -256
  285. package/skills/orch-add-feature/SKILL.md +45 -45
  286. package/skills/orch-build-mvp/SKILL.md +49 -49
  287. package/skills/orch-change-feature/SKILL.md +43 -43
  288. package/skills/orch-fix-defect/SKILL.md +43 -43
  289. package/skills/orch-pipeline/SKILL.md +121 -121
  290. package/skills/orch-refine-code/SKILL.md +44 -44
  291. package/skills/parallel-execution-optimizer/SKILL.md +74 -74
  292. package/skills/perl-patterns/SKILL.md +505 -505
  293. package/skills/perl-security/SKILL.md +504 -504
  294. package/skills/perl-testing/SKILL.md +476 -476
  295. package/skills/plan-canvas/SKILL.md +153 -153
  296. package/skills/plan-orchestrate/SKILL.md +263 -263
  297. package/skills/plankton-code-quality/SKILL.md +237 -237
  298. package/skills/postgres-patterns/SKILL.md +148 -148
  299. package/skills/prediction-market-oracle-research/SKILL.md +64 -64
  300. package/skills/prediction-market-risk-review/SKILL.md +61 -61
  301. package/skills/premium-web-design/SKILL.md +154 -154
  302. package/skills/prisma-patterns/SKILL.md +401 -401
  303. package/skills/product-capability/SKILL.md +142 -142
  304. package/skills/product-lens/SKILL.md +93 -93
  305. package/skills/production-audit/SKILL.md +207 -207
  306. package/skills/production-scheduling/SKILL.md +238 -238
  307. package/skills/project-flow-ops/SKILL.md +112 -112
  308. package/skills/prompt-optimizer/SKILL.md +398 -398
  309. package/skills/publish-website/SKILL.md +76 -0
  310. package/skills/python-patterns/SKILL.md +751 -751
  311. package/skills/python-testing/SKILL.md +817 -817
  312. package/skills/pytorch-patterns/SKILL.md +397 -397
  313. package/skills/quality-nonconformance/SKILL.md +260 -260
  314. package/skills/quarkus-patterns/SKILL.md +723 -723
  315. package/skills/quarkus-security/SKILL.md +468 -468
  316. package/skills/quarkus-tdd/SKILL.md +812 -812
  317. package/skills/quarkus-verification/SKILL.md +480 -480
  318. package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
  319. package/skills/react-native-patterns/SKILL.md +326 -326
  320. package/skills/react-patterns/SKILL.md +342 -342
  321. package/skills/react-performance/SKILL.md +575 -575
  322. package/skills/react-testing/SKILL.md +424 -424
  323. package/skills/recsys-pipeline-architect/SKILL.md +115 -115
  324. package/skills/recursive-decision-ledger/SKILL.md +81 -81
  325. package/skills/redis-patterns/SKILL.md +404 -404
  326. package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
  327. package/skills/remotion-video-creation/SKILL.md +43 -43
  328. package/skills/repo-scan/SKILL.md +79 -79
  329. package/skills/research-ops/SKILL.md +113 -113
  330. package/skills/returns-reverse-logistics/SKILL.md +240 -240
  331. package/skills/rules-distill/SKILL.md +265 -265
  332. package/skills/rust-patterns/SKILL.md +500 -500
  333. package/skills/rust-testing/SKILL.md +501 -501
  334. package/skills/safety-guard/SKILL.md +76 -76
  335. package/skills/santa-method/SKILL.md +307 -307
  336. package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
  337. package/skills/scientific-db-uspto-database/SKILL.md +178 -178
  338. package/skills/scientific-pkg-gget/SKILL.md +167 -167
  339. package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
  340. package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
  341. package/skills/search-first/SKILL.md +183 -183
  342. package/skills/security-bounty-hunter/SKILL.md +100 -100
  343. package/skills/security-review/SKILL.md +504 -504
  344. package/skills/security-scan/SKILL.md +166 -166
  345. package/skills/seo/SKILL.md +155 -155
  346. package/skills/skill-comply/SKILL.md +59 -59
  347. package/skills/skill-scout/SKILL.md +141 -141
  348. package/skills/skill-stocktake/SKILL.md +195 -195
  349. package/skills/social-graph-ranker/SKILL.md +155 -155
  350. package/skills/social-publisher/SKILL.md +130 -130
  351. package/skills/springboot-patterns/SKILL.md +315 -315
  352. package/skills/springboot-security/SKILL.md +273 -273
  353. package/skills/springboot-tdd/SKILL.md +159 -159
  354. package/skills/springboot-verification/SKILL.md +232 -232
  355. package/skills/strategic-compact/SKILL.md +142 -142
  356. package/skills/swift-actor-persistence/SKILL.md +144 -144
  357. package/skills/swift-concurrency-6-2/SKILL.md +216 -216
  358. package/skills/swift-protocol-di-testing/SKILL.md +191 -191
  359. package/skills/swiftui-patterns/SKILL.md +259 -259
  360. package/skills/taste/SKILL.md +264 -264
  361. package/skills/taste/references/genre-taxonomy.md +87 -87
  362. package/skills/tdd-workflow/SKILL.md +583 -583
  363. package/skills/team-agent-orchestration/SKILL.md +111 -111
  364. package/skills/team-builder/SKILL.md +169 -169
  365. package/skills/terminal-ops/SKILL.md +110 -110
  366. package/skills/tinystruct-patterns/SKILL.md +279 -279
  367. package/skills/tinystruct-patterns/references/architecture.md +90 -90
  368. package/skills/tinystruct-patterns/references/data-handling.md +60 -60
  369. package/skills/tinystruct-patterns/references/database.md +99 -99
  370. package/skills/tinystruct-patterns/references/routing.md +64 -64
  371. package/skills/tinystruct-patterns/references/system-usage.md +97 -97
  372. package/skills/tinystruct-patterns/references/testing.md +72 -72
  373. package/skills/token-budget-advisor/SKILL.md +134 -134
  374. package/skills/ui-demo/SKILL.md +466 -466
  375. package/skills/ui-to-vue/SKILL.md +135 -135
  376. package/skills/uncloud/SKILL.md +344 -344
  377. package/skills/unified-memory/SKILL.md +170 -170
  378. package/skills/unified-notifications-ops/SKILL.md +188 -188
  379. package/skills/verification-loop/SKILL.md +129 -129
  380. package/skills/video-editing/SKILL.md +311 -311
  381. package/skills/videodb/SKILL.md +375 -375
  382. package/skills/visa-doc-translate/SKILL.md +117 -117
  383. package/skills/vite-patterns/SKILL.md +450 -450
  384. package/skills/vue-patterns/SKILL.md +471 -471
  385. package/skills/windows-desktop-e2e/SKILL.md +888 -888
  386. package/skills/workspace-surface-audit/SKILL.md +126 -126
  387. package/skills/x-api/SKILL.md +235 -235
@@ -1,765 +1,765 @@
1
- ---
2
- name: data-scraper-agent
3
- description: Build a fully automated AI-powered data collection agent for any public source — job boards, prices, news, GitHub, sports, anything. Runs on a schedule, enriches data with a free LLM (Gemini Flash), stores results in Notion/Sheets/Supabase, and learns from user feedback. Runs 100% free on GitHub Actions. Use when the user wants to monitor, collect, or track any public data automatically.
4
- metadata:
5
- origin: community
6
- ---
7
-
8
- # Data Scraper Agent
9
-
10
- Build a production-ready, AI-powered data collection agent for any public data source.
11
- Runs on a schedule, enriches results with a free LLM, stores to a database, and improves over time.
12
-
13
- **Stack: Python · Gemini Flash (free) · GitHub Actions (free) · Notion / Sheets / Supabase**
14
-
15
- ## When to Activate
16
-
17
- - User wants to gather or monitor any public website or API
18
- - User says "build a bot that checks...", "monitor X for me", "collect data from..."
19
- - User wants to track jobs, prices, news, repos, sports scores, events, listings
20
- - User asks how to automate data collection without paying for hosting
21
- - User wants an agent that gets smarter over time based on their decisions
22
-
23
- ## Core Concepts
24
-
25
- ### The Three Layers
26
-
27
- Every data collection agent has three layers:
28
-
29
- ```
30
- COLLECT → ENRICH → STORE
31
- │ │ │
32
- Scraper AI (LLM) Database
33
- runs on scores/ Notion /
34
- schedule summarises Sheets /
35
- & classifies Supabase
36
- ```
37
-
38
- ### Free Stack
39
-
40
- | Layer | Tool | Why |
41
- |---|---|---|
42
- | **Scraping** | `requests` + `BeautifulSoup` | No cost, covers 80% of public sites |
43
- | **JS-rendered sites** | `playwright` (free) | When HTML fetching fails |
44
- | **AI enrichment** | Gemini Flash via REST API | 500 req/day, 1M tokens/day — free |
45
- | **Storage** | Notion API | Free tier, great UI for review |
46
- | **Schedule** | GitHub Actions cron | Free for public repos |
47
- | **Learning** | JSON feedback file in repo | Zero infra, persists in git |
48
-
49
- ### AI Model Fallback Chain
50
-
51
- Build agents to auto-fallback across Gemini models on quota exhaustion:
52
-
53
- ```
54
- gemini-2.0-flash-lite (30 RPM) →
55
- gemini-2.0-flash (15 RPM) →
56
- gemini-2.5-flash (10 RPM) →
57
- gemini-flash-lite-latest (fallback)
58
- ```
59
-
60
- ### Batch API Calls for Efficiency
61
-
62
- Never call the LLM once per item. Always batch:
63
-
64
- ```python
65
- # BAD: 33 API calls for 33 items
66
- for item in items:
67
- result = call_ai(item) # 33 calls → hits rate limit
68
-
69
- # GOOD: 7 API calls for 33 items (batch size 5)
70
- for batch in chunks(items, size=5):
71
- results = call_ai(batch) # 7 calls → stays within free tier
72
- ```
73
-
74
- ---
75
-
76
- ## Workflow
77
-
78
- ### Step 1: Understand the Goal
79
-
80
- Ask the user:
81
-
82
- 1. **What to collect:** "What data source? URL / API / RSS / public endpoint?"
83
- 2. **What to extract:** "What fields matter? Title, price, URL, date, score?"
84
- 3. **How to store:** "Where should results go? Notion, Google Sheets, Supabase, or local file?"
85
- 4. **How to enrich:** "Do you want AI to score, summarise, classify, or match each item?"
86
- 5. **Frequency:** "How often should it run? Every hour, daily, weekly?"
87
-
88
- Common examples to prompt:
89
- - Job boards → score relevance to resume
90
- - Product prices → alert on drops
91
- - GitHub repos → summarise new releases
92
- - News feeds → classify by topic + sentiment
93
- - Sports results → extract stats to tracker
94
- - Events calendar → filter by interest
95
-
96
- ---
97
-
98
- ### Step 2: Design the Collection Architecture
99
-
100
- Generate this directory structure for the user:
101
-
102
- ```
103
- my-agent/
104
- ├── config.yaml # User customises this (keywords, filters, preferences)
105
- ├── profile/
106
- │ └── context.md # User context the AI uses (resume, interests, criteria)
107
- ├── scraper/
108
- │ ├── __init__.py
109
- │ ├── main.py # Orchestrator: scrape → enrich → store
110
- │ ├── filters.py # Rule-based pre-filter (fast, before AI)
111
- │ └── sources/
112
- │ ├── __init__.py
113
- │ └── source_name.py # One file per data source
114
- ├── ai/
115
- │ ├── __init__.py
116
- │ ├── client.py # Gemini REST client with model fallback
117
- │ ├── pipeline.py # Batch AI analysis
118
- │ ├── jd_fetcher.py # Fetch full content from URLs (optional)
119
- │ └── memory.py # Learn from user feedback
120
- ├── storage/
121
- │ ├── __init__.py
122
- │ └── notion_sync.py # Or sheets_sync.py / supabase_sync.py
123
- ├── data/
124
- │ └── feedback.json # User decision history (auto-updated)
125
- ├── .env.example
126
- ├── setup.py # One-time DB/schema creation
127
- ├── enrich_existing.py # Backfill AI scores on old rows
128
- ├── requirements.txt
129
- └── .github/
130
- └── workflows/
131
- └── scraper.yml # GitHub Actions schedule
132
- ```
133
-
134
- ---
135
-
136
- ### Step 3: Build the Source Connector
137
-
138
- Template for any data source:
139
-
140
- ```python
141
- # scraper/sources/my_source.py
142
- """
143
- [Source Name] — gathers [what] from [where].
144
- Method: [REST API / HTML scraping / RSS feed]
145
- """
146
- import requests
147
- from bs4 import BeautifulSoup
148
- from datetime import datetime, timezone
149
- from scraper.filters import is_relevant
150
-
151
- HEADERS = {
152
- "User-Agent": "Mozilla/5.0 (compatible; research-bot/1.0)",
153
- }
154
-
155
-
156
- def fetch() -> list[dict]:
157
- """
158
- Returns a list of items with consistent schema.
159
- Each item must have at minimum: name, url, date_found.
160
- """
161
- results = []
162
-
163
- # ---- REST API source ----
164
- resp = requests.get("https://api.example.com/items", headers=HEADERS, timeout=15)
165
- if resp.status_code == 200:
166
- for item in resp.json().get("results", []):
167
- if not is_relevant(item.get("title", "")):
168
- continue
169
- results.append(_normalise(item))
170
-
171
- return results
172
-
173
-
174
- def _normalise(raw: dict) -> dict:
175
- """Convert raw API/HTML data to the standard schema."""
176
- return {
177
- "name": raw.get("title", ""),
178
- "url": raw.get("link", ""),
179
- "source": "MySource",
180
- "date_found": datetime.now(timezone.utc).date().isoformat(),
181
- # add domain-specific fields here
182
- }
183
- ```
184
-
185
- **HTML fetch pattern:**
186
- ```python
187
- soup = BeautifulSoup(resp.text, "lxml")
188
- for card in soup.select("[class*='listing']"):
189
- title = card.select_one("h2, h3").get_text(strip=True)
190
- link = card.select_one("a")["href"]
191
- if not link.startswith("http"):
192
- link = f"https://example.com{link}"
193
- ```
194
-
195
- **RSS feed pattern:**
196
- ```python
197
- import xml.etree.ElementTree as ET
198
- root = ET.fromstring(resp.text)
199
- for item in root.findall(".//item"):
200
- title = item.findtext("title", "")
201
- link = item.findtext("link", "")
202
- ```
203
-
204
- ---
205
-
206
- ### Step 4: Build the Gemini AI Client
207
-
208
- ```python
209
- # ai/client.py
210
- import os, json, time, requests
211
-
212
- _last_call = 0.0
213
-
214
- MODEL_FALLBACK = [
215
- "gemini-2.0-flash-lite",
216
- "gemini-2.0-flash",
217
- "gemini-2.5-flash",
218
- "gemini-flash-lite-latest",
219
- ]
220
-
221
-
222
- def generate(prompt: str, model: str = "", rate_limit: float = 7.0) -> dict:
223
- """Call Gemini with auto-fallback on 429. Returns parsed JSON or {}."""
224
- global _last_call
225
-
226
- api_key = os.environ.get("GEMINI_API_KEY", "")
227
- if not api_key:
228
- return {}
229
-
230
- elapsed = time.time() - _last_call
231
- if elapsed < rate_limit:
232
- time.sleep(rate_limit - elapsed)
233
-
234
- models = [model] + [m for m in MODEL_FALLBACK if m != model] if model else MODEL_FALLBACK
235
- _last_call = time.time()
236
-
237
- for m in models:
238
- url = f"https://generativelanguage.googleapis.com/v1beta/models/{m}:generateContent?key={api_key}"
239
- payload = {
240
- "contents": [{"parts": [{"text": prompt}]}],
241
- "generationConfig": {
242
- "responseMimeType": "application/json",
243
- "temperature": 0.3,
244
- "maxOutputTokens": 2048,
245
- },
246
- }
247
- try:
248
- resp = requests.post(url, json=payload, timeout=30)
249
- if resp.status_code == 200:
250
- return _parse(resp)
251
- if resp.status_code in (429, 404):
252
- time.sleep(1)
253
- continue
254
- return {}
255
- except requests.RequestException:
256
- return {}
257
-
258
- return {}
259
-
260
-
261
- def _parse(resp) -> dict:
262
- try:
263
- text = (
264
- resp.json()
265
- .get("candidates", [{}])[0]
266
- .get("content", {})
267
- .get("parts", [{}])[0]
268
- .get("text", "")
269
- .strip()
270
- )
271
- if text.startswith("```"):
272
- text = text.split("\n", 1)[-1].rsplit("```", 1)[0]
273
- return json.loads(text)
274
- except (json.JSONDecodeError, KeyError):
275
- return {}
276
- ```
277
-
278
- ---
279
-
280
- ### Step 5: Build the AI Pipeline (Batch)
281
-
282
- ```python
283
- # ai/pipeline.py
284
- import json
285
- import yaml
286
- from pathlib import Path
287
- from ai.client import generate
288
-
289
- def analyse_batch(items: list[dict], context: str = "", preference_prompt: str = "") -> list[dict]:
290
- """Analyse items in batches. Returns items enriched with AI fields."""
291
- config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
292
- model = config.get("ai", {}).get("model", "gemini-2.5-flash")
293
- rate_limit = config.get("ai", {}).get("rate_limit_seconds", 7.0)
294
- min_score = config.get("ai", {}).get("min_score", 0)
295
- batch_size = config.get("ai", {}).get("batch_size", 5)
296
-
297
- batches = [items[i:i + batch_size] for i in range(0, len(items), batch_size)]
298
- print(f" [AI] {len(items)} items → {len(batches)} API calls")
299
-
300
- enriched = []
301
- for i, batch in enumerate(batches):
302
- print(f" [AI] Batch {i + 1}/{len(batches)}...")
303
- prompt = _build_prompt(batch, context, preference_prompt, config)
304
- result = generate(prompt, model=model, rate_limit=rate_limit)
305
-
306
- analyses = result.get("analyses", [])
307
- for j, item in enumerate(batch):
308
- ai = analyses[j] if j < len(analyses) else {}
309
- if ai:
310
- score = max(0, min(100, int(ai.get("score", 0))))
311
- if min_score and score < min_score:
312
- continue
313
- enriched.append({**item, "ai_score": score, "ai_summary": ai.get("summary", ""), "ai_notes": ai.get("notes", "")})
314
- else:
315
- enriched.append(item)
316
-
317
- return enriched
318
-
319
-
320
- def _build_prompt(batch, context, preference_prompt, config):
321
- priorities = config.get("priorities", [])
322
- items_text = "\n\n".join(
323
- f"Item {i+1}: {json.dumps({k: v for k, v in item.items() if not k.startswith('_')})}"
324
- for i, item in enumerate(batch)
325
- )
326
-
327
- return f"""Analyse these {len(batch)} items and return a JSON object.
328
-
329
- # Items
330
- {items_text}
331
-
332
- # User Context
333
- {context[:800] if context else "Not provided"}
334
-
335
- # User Priorities
336
- {chr(10).join(f"- {p}" for p in priorities)}
337
-
338
- {preference_prompt}
339
-
340
- # Instructions
341
- Return: {{"analyses": [{{"score": <0-100>, "summary": "<2 sentences>", "notes": "<why this matches or doesn't>"}} for each item in order]}}
342
- Be concise. Score 90+=excellent match, 70-89=good, 50-69=ok, <50=weak."""
343
- ```
344
-
345
- ---
346
-
347
- ### Step 6: Build the Feedback Learning System
348
-
349
- ```python
350
- # ai/memory.py
351
- """Learn from user decisions to improve future scoring."""
352
- import json
353
- from pathlib import Path
354
-
355
- FEEDBACK_PATH = Path(__file__).parent.parent / "data" / "feedback.json"
356
-
357
-
358
- def load_feedback() -> dict:
359
- if FEEDBACK_PATH.exists():
360
- try:
361
- return json.loads(FEEDBACK_PATH.read_text())
362
- except (json.JSONDecodeError, OSError):
363
- pass
364
- return {"positive": [], "negative": []}
365
-
366
-
367
- def save_feedback(fb: dict):
368
- FEEDBACK_PATH.parent.mkdir(parents=True, exist_ok=True)
369
- FEEDBACK_PATH.write_text(json.dumps(fb, indent=2))
370
-
371
-
372
- def build_preference_prompt(feedback: dict, max_examples: int = 15) -> str:
373
- """Convert feedback history into a prompt bias section."""
374
- lines = []
375
- if feedback.get("positive"):
376
- lines.append("# Items the user LIKED (positive signal):")
377
- for e in feedback["positive"][-max_examples:]:
378
- lines.append(f"- {e}")
379
- if feedback.get("negative"):
380
- lines.append("\n# Items the user SKIPPED/REJECTED (negative signal):")
381
- for e in feedback["negative"][-max_examples:]:
382
- lines.append(f"- {e}")
383
- if lines:
384
- lines.append("\nUse these patterns to bias scoring on new items.")
385
- return "\n".join(lines)
386
- ```
387
-
388
- **Integration with your storage layer:** after each run, query your DB for items with positive/negative status and call `save_feedback()` with the extracted patterns.
389
-
390
- ---
391
-
392
- ### Step 7: Build Storage (Notion example)
393
-
394
- ```python
395
- # storage/notion_sync.py
396
- import os
397
- from notion_client import Client
398
- from notion_client.errors import APIResponseError
399
-
400
- _client = None
401
-
402
- def get_client():
403
- global _client
404
- if _client is None:
405
- _client = Client(auth=os.environ["NOTION_TOKEN"])
406
- return _client
407
-
408
- def get_existing_urls(db_id: str) -> set[str]:
409
- """Fetch all URLs already stored — used for deduplication."""
410
- client, seen, cursor = get_client(), set(), None
411
- while True:
412
- resp = client.databases.query(database_id=db_id, page_size=100, **{"start_cursor": cursor} if cursor else {})
413
- for page in resp["results"]:
414
- url = page["properties"].get("URL", {}).get("url", "")
415
- if url: seen.add(url)
416
- if not resp["has_more"]: break
417
- cursor = resp["next_cursor"]
418
- return seen
419
-
420
- def push_item(db_id: str, item: dict) -> bool:
421
- """Push one item to Notion. Returns True on success."""
422
- props = {
423
- "Name": {"title": [{"text": {"content": item.get("name", "")[:100]}}]},
424
- "URL": {"url": item.get("url")},
425
- "Source": {"select": {"name": item.get("source", "Unknown")}},
426
- "Date Found": {"date": {"start": item.get("date_found")}},
427
- "Status": {"select": {"name": "New"}},
428
- }
429
- # AI fields
430
- if item.get("ai_score") is not None:
431
- props["AI Score"] = {"number": item["ai_score"]}
432
- if item.get("ai_summary"):
433
- props["Summary"] = {"rich_text": [{"text": {"content": item["ai_summary"][:2000]}}]}
434
- if item.get("ai_notes"):
435
- props["Notes"] = {"rich_text": [{"text": {"content": item["ai_notes"][:2000]}}]}
436
-
437
- try:
438
- get_client().pages.create(parent={"database_id": db_id}, properties=props)
439
- return True
440
- except APIResponseError as e:
441
- print(f"[notion] Push failed: {e}")
442
- return False
443
-
444
- def sync(db_id: str, items: list[dict]) -> tuple[int, int]:
445
- existing = get_existing_urls(db_id)
446
- added = skipped = 0
447
- for item in items:
448
- if item.get("url") in existing:
449
- skipped += 1; continue
450
- if push_item(db_id, item):
451
- added += 1; existing.add(item["url"])
452
- else:
453
- skipped += 1
454
- return added, skipped
455
- ```
456
-
457
- ---
458
-
459
- ### Step 8: Orchestrate in main.py
460
-
461
- ```python
462
- # scraper/main.py
463
- import os, sys, yaml
464
- from pathlib import Path
465
- from dotenv import load_dotenv
466
-
467
- load_dotenv()
468
-
469
- from scraper.sources import my_source # add your sources
470
-
471
- # NOTE: This example uses Notion. If storage.provider is "sheets" or "supabase",
472
- # replace this import with storage.sheets_sync or storage.supabase_sync and update
473
- # the env var and sync() call accordingly.
474
- from storage.notion_sync import sync
475
-
476
- SOURCES = [
477
- ("My Source", my_source.fetch),
478
- ]
479
-
480
- def ai_enabled():
481
- return bool(os.environ.get("GEMINI_API_KEY"))
482
-
483
- def main():
484
- config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
485
- provider = config.get("storage", {}).get("provider", "notion")
486
-
487
- # Resolve the storage target identifier from env based on provider
488
- if provider == "notion":
489
- db_id = os.environ.get("NOTION_DATABASE_ID")
490
- if not db_id:
491
- print("ERROR: NOTION_DATABASE_ID not set"); sys.exit(1)
492
- else:
493
- # Extend here for sheets (SHEET_ID) or supabase (SUPABASE_TABLE) etc.
494
- print(f"ERROR: provider '{provider}' not yet wired in main.py"); sys.exit(1)
495
-
496
- config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
497
- all_items = []
498
-
499
- for name, fetch_fn in SOURCES:
500
- try:
501
- items = fetch_fn()
502
- print(f"[{name}] {len(items)} items")
503
- all_items.extend(items)
504
- except Exception as e:
505
- print(f"[{name}] FAILED: {e}")
506
-
507
- # Deduplicate by URL
508
- seen, deduped = set(), []
509
- for item in all_items:
510
- if (url := item.get("url", "")) and url not in seen:
511
- seen.add(url); deduped.append(item)
512
-
513
- print(f"Unique items: {len(deduped)}")
514
-
515
- if ai_enabled() and deduped:
516
- from ai.memory import load_feedback, build_preference_prompt
517
- from ai.pipeline import analyse_batch
518
-
519
- # load_feedback() reads data/feedback.json written by your feedback sync script.
520
- # To keep it current, implement a separate feedback_sync.py that queries your
521
- # storage provider for items with positive/negative statuses and calls save_feedback().
522
- feedback = load_feedback()
523
- preference = build_preference_prompt(feedback)
524
- context_path = Path(__file__).parent.parent / "profile" / "context.md"
525
- context = context_path.read_text() if context_path.exists() else ""
526
- deduped = analyse_batch(deduped, context=context, preference_prompt=preference)
527
- else:
528
- print("[AI] Skipped — GEMINI_API_KEY not set")
529
-
530
- added, skipped = sync(db_id, deduped)
531
- print(f"Done — {added} new, {skipped} existing")
532
-
533
- if __name__ == "__main__":
534
- main()
535
- ```
536
-
537
- ---
538
-
539
- ### Step 9: GitHub Actions Workflow
540
-
541
- ```yaml
542
- # .github/workflows/scraper.yml
543
- name: Data Scraper Agent
544
-
545
- on:
546
- schedule:
547
- - cron: "0 */3 * * *" # every 3 hours — adjust to your needs
548
- workflow_dispatch: # allow manual trigger
549
-
550
- permissions:
551
- contents: write # required for the feedback-history commit step
552
-
553
- jobs:
554
- scrape:
555
- runs-on: ubuntu-latest
556
- timeout-minutes: 20
557
-
558
- steps:
559
- - uses: actions/checkout@v4
560
-
561
- - uses: actions/setup-python@v5
562
- with:
563
- python-version: "3.11"
564
- cache: "pip"
565
-
566
- - run: pip install -r requirements.txt
567
-
568
- # Uncomment if Playwright is enabled in requirements.txt
569
- # - name: Install Playwright browsers
570
- # run: python -m playwright install chromium --with-deps
571
-
572
- - name: Run agent
573
- env:
574
- NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
575
- NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
576
- GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
577
- run: python -m scraper.main
578
-
579
- - name: Commit feedback history
580
- run: |
581
- git config user.name "github-actions[bot]"
582
- git config user.email "github-actions[bot]@users.noreply.github.com"
583
- git add data/feedback.json || true
584
- git diff --cached --quiet || git commit -m "chore: update feedback history"
585
- git push
586
- ```
587
-
588
- ---
589
-
590
- ### Step 10: config.yaml Template
591
-
592
- ```yaml
593
- # Customise this file — no code changes needed
594
-
595
- # What to collect (pre-filter before AI)
596
- filters:
597
- required_keywords: [] # item must contain at least one
598
- blocked_keywords: [] # item must not contain any
599
-
600
- # Your priorities — AI uses these for scoring
601
- priorities:
602
- - "example priority 1"
603
- - "example priority 2"
604
-
605
- # Storage
606
- storage:
607
- provider: "notion" # notion | sheets | supabase | sqlite
608
-
609
- # Feedback learning
610
- feedback:
611
- positive_statuses: ["Saved", "Applied", "Interested"]
612
- negative_statuses: ["Skip", "Rejected", "Not relevant"]
613
-
614
- # AI settings
615
- ai:
616
- enabled: true
617
- model: "gemini-2.5-flash"
618
- min_score: 0 # filter out items below this score
619
- rate_limit_seconds: 7 # seconds between API calls
620
- batch_size: 5 # items per API call
621
- ```
622
-
623
- ---
624
-
625
- ## Common Scraping Patterns
626
-
627
- ### Pattern 1: REST API (easiest)
628
- ```python
629
- resp = requests.get(url, params={"q": query}, headers=HEADERS, timeout=15)
630
- items = resp.json().get("results", [])
631
- ```
632
-
633
- ### Pattern 2: HTML Scraping
634
- ```python
635
- soup = BeautifulSoup(resp.text, "lxml")
636
- for card in soup.select(".listing-card"):
637
- title = card.select_one("h2").get_text(strip=True)
638
- href = card.select_one("a")["href"]
639
- ```
640
-
641
- ### Pattern 3: RSS Feed
642
- ```python
643
- import xml.etree.ElementTree as ET
644
- root = ET.fromstring(resp.text)
645
- for item in root.findall(".//item"):
646
- title = item.findtext("title", "")
647
- link = item.findtext("link", "")
648
- pub_date = item.findtext("pubDate", "")
649
- ```
650
-
651
- ### Pattern 4: Paginated API
652
- ```python
653
- page = 1
654
- while True:
655
- resp = requests.get(url, params={"page": page, "limit": 50}, timeout=15)
656
- data = resp.json()
657
- items = data.get("results", [])
658
- if not items:
659
- break
660
- for item in items:
661
- results.append(_normalise(item))
662
- if not data.get("has_more"):
663
- break
664
- page += 1
665
- ```
666
-
667
- ### Pattern 5: JS-Rendered Pages (Playwright)
668
- ```python
669
- from playwright.sync_api import sync_playwright
670
-
671
- with sync_playwright() as p:
672
- browser = p.chromium.launch()
673
- page = browser.new_page()
674
- page.goto(url)
675
- page.wait_for_selector(".listing")
676
- html = page.content()
677
- browser.close()
678
-
679
- soup = BeautifulSoup(html, "lxml")
680
- ```
681
-
682
- ---
683
-
684
- ## Anti-Patterns to Avoid
685
-
686
- | Anti-pattern | Problem | Fix |
687
- |---|---|---|
688
- | One LLM call per item | Hits rate limits instantly | Batch 5 items per call |
689
- | Hardcoded keywords in code | Not reusable | Move all config to `config.yaml` |
690
- | Scraping without rate limit | IP ban | Add `time.sleep(1)` between requests |
691
- | Storing secrets in code | Security risk | Always use `.env` + GitHub Secrets |
692
- | No deduplication | Duplicate rows pile up | Always check URL before pushing |
693
- | Ignoring `robots.txt` | Legal/ethical risk | Respect crawl rules; use public APIs when available |
694
- | JS-rendered sites with `requests` | Empty response | Use Playwright or look for the underlying API |
695
- | `maxOutputTokens` too low | Truncated JSON, parse error | Use 2048+ for batch responses |
696
-
697
- ---
698
-
699
- ## Free Tier Limits Reference
700
-
701
- | Service | Free Limit | Typical Usage |
702
- |---|---|---|
703
- | Gemini Flash Lite | 30 RPM, 1500 RPD | ~56 req/day at 3-hr intervals |
704
- | Gemini 2.0 Flash | 15 RPM, 1500 RPD | Good fallback |
705
- | Gemini 2.5 Flash | 10 RPM, 500 RPD | Use sparingly |
706
- | GitHub Actions | Unlimited (public repos) | ~20 min/day |
707
- | Notion API | Unlimited | ~200 writes/day |
708
- | Supabase | 500MB DB, 2GB transfer | Fine for most agents |
709
- | Google Sheets API | 300 req/min | Works for small agents |
710
-
711
- ---
712
-
713
- ## Requirements Template
714
-
715
- ```
716
- requests==2.31.0
717
- beautifulsoup4==4.12.3
718
- lxml==5.1.0
719
- python-dotenv==1.0.1
720
- pyyaml==6.0.2
721
- notion-client==2.2.1 # if using Notion
722
- # playwright==1.40.0 # uncomment for JS-rendered sites
723
- ```
724
-
725
- ---
726
-
727
- ## Quality Checklist
728
-
729
- Before marking the agent complete:
730
-
731
- - [ ] `config.yaml` controls all user-facing settings — no hardcoded values
732
- - [ ] `profile/context.md` holds user-specific context for AI matching
733
- - [ ] Deduplication by URL before every storage push
734
- - [ ] Gemini client has model fallback chain (4 models)
735
- - [ ] Batch size ≤ 5 items per API call
736
- - [ ] `maxOutputTokens` ≥ 2048
737
- - [ ] `.env` is in `.gitignore`
738
- - [ ] `.env.example` provided for onboarding
739
- - [ ] `setup.py` creates DB schema on first run
740
- - [ ] `enrich_existing.py` backfills AI scores on old rows
741
- - [ ] GitHub Actions workflow commits `feedback.json` after each run
742
- - [ ] README covers: setup in < 5 minutes, required secrets, customisation
743
-
744
- ---
745
-
746
- ## Real-World Examples
747
-
748
- ```
749
- "Build me an agent that monitors Hacker News for AI startup funding news"
750
- "Scrape product prices from 3 e-commerce sites and alert when they drop"
751
- "Track new GitHub repos tagged with 'llm' or 'agents' — summarise each one"
752
- "Collect Chief of Staff job listings from LinkedIn and Cutshort into Notion"
753
- "Monitor a subreddit for posts mentioning my company — classify sentiment"
754
- "Scrape new academic papers from arXiv on a topic I care about daily"
755
- "Track sports fixture results and keep a running table in Google Sheets"
756
- "Build a real estate listing watcher — alert on new properties under ₹1 Cr"
757
- ```
758
-
759
- ---
760
-
761
- ## Reference Implementation
762
-
763
- A complete working agent built with this exact architecture would collect from 4+ sources,
764
- batch Gemini calls, learn from Applied/Rejected decisions stored in Notion, and run
765
- 100% free on GitHub Actions. Follow Steps 1–9 above to build your own.
1
+ ---
2
+ name: data-scraper-agent
3
+ description: Build a fully automated AI-powered data collection agent for any public source — job boards, prices, news, GitHub, sports, anything. Runs on a schedule, enriches data with a free LLM (Gemini Flash), stores results in Notion/Sheets/Supabase, and learns from user feedback. Runs 100% free on GitHub Actions. Use when the user wants to monitor, collect, or track any public data automatically.
4
+ metadata:
5
+ origin: community
6
+ ---
7
+
8
+ # Data Scraper Agent
9
+
10
+ Build a production-ready, AI-powered data collection agent for any public data source.
11
+ Runs on a schedule, enriches results with a free LLM, stores to a database, and improves over time.
12
+
13
+ **Stack: Python · Gemini Flash (free) · GitHub Actions (free) · Notion / Sheets / Supabase**
14
+
15
+ ## When to Activate
16
+
17
+ - User wants to gather or monitor any public website or API
18
+ - User says "build a bot that checks...", "monitor X for me", "collect data from..."
19
+ - User wants to track jobs, prices, news, repos, sports scores, events, listings
20
+ - User asks how to automate data collection without paying for hosting
21
+ - User wants an agent that gets smarter over time based on their decisions
22
+
23
+ ## Core Concepts
24
+
25
+ ### The Three Layers
26
+
27
+ Every data collection agent has three layers:
28
+
29
+ ```
30
+ COLLECT → ENRICH → STORE
31
+ │ │ │
32
+ Scraper AI (LLM) Database
33
+ runs on scores/ Notion /
34
+ schedule summarises Sheets /
35
+ & classifies Supabase
36
+ ```
37
+
38
+ ### Free Stack
39
+
40
+ | Layer | Tool | Why |
41
+ |---|---|---|
42
+ | **Scraping** | `requests` + `BeautifulSoup` | No cost, covers 80% of public sites |
43
+ | **JS-rendered sites** | `playwright` (free) | When HTML fetching fails |
44
+ | **AI enrichment** | Gemini Flash via REST API | 500 req/day, 1M tokens/day — free |
45
+ | **Storage** | Notion API | Free tier, great UI for review |
46
+ | **Schedule** | GitHub Actions cron | Free for public repos |
47
+ | **Learning** | JSON feedback file in repo | Zero infra, persists in git |
48
+
49
+ ### AI Model Fallback Chain
50
+
51
+ Build agents to auto-fallback across Gemini models on quota exhaustion:
52
+
53
+ ```
54
+ gemini-2.0-flash-lite (30 RPM) →
55
+ gemini-2.0-flash (15 RPM) →
56
+ gemini-2.5-flash (10 RPM) →
57
+ gemini-flash-lite-latest (fallback)
58
+ ```
59
+
60
+ ### Batch API Calls for Efficiency
61
+
62
+ Never call the LLM once per item. Always batch:
63
+
64
+ ```python
65
+ # BAD: 33 API calls for 33 items
66
+ for item in items:
67
+ result = call_ai(item) # 33 calls → hits rate limit
68
+
69
+ # GOOD: 7 API calls for 33 items (batch size 5)
70
+ for batch in chunks(items, size=5):
71
+ results = call_ai(batch) # 7 calls → stays within free tier
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Workflow
77
+
78
+ ### Step 1: Understand the Goal
79
+
80
+ Ask the user:
81
+
82
+ 1. **What to collect:** "What data source? URL / API / RSS / public endpoint?"
83
+ 2. **What to extract:** "What fields matter? Title, price, URL, date, score?"
84
+ 3. **How to store:** "Where should results go? Notion, Google Sheets, Supabase, or local file?"
85
+ 4. **How to enrich:** "Do you want AI to score, summarise, classify, or match each item?"
86
+ 5. **Frequency:** "How often should it run? Every hour, daily, weekly?"
87
+
88
+ Common examples to prompt:
89
+ - Job boards → score relevance to resume
90
+ - Product prices → alert on drops
91
+ - GitHub repos → summarise new releases
92
+ - News feeds → classify by topic + sentiment
93
+ - Sports results → extract stats to tracker
94
+ - Events calendar → filter by interest
95
+
96
+ ---
97
+
98
+ ### Step 2: Design the Collection Architecture
99
+
100
+ Generate this directory structure for the user:
101
+
102
+ ```
103
+ my-agent/
104
+ ├── config.yaml # User customises this (keywords, filters, preferences)
105
+ ├── profile/
106
+ │ └── context.md # User context the AI uses (resume, interests, criteria)
107
+ ├── scraper/
108
+ │ ├── __init__.py
109
+ │ ├── main.py # Orchestrator: scrape → enrich → store
110
+ │ ├── filters.py # Rule-based pre-filter (fast, before AI)
111
+ │ └── sources/
112
+ │ ├── __init__.py
113
+ │ └── source_name.py # One file per data source
114
+ ├── ai/
115
+ │ ├── __init__.py
116
+ │ ├── client.py # Gemini REST client with model fallback
117
+ │ ├── pipeline.py # Batch AI analysis
118
+ │ ├── jd_fetcher.py # Fetch full content from URLs (optional)
119
+ │ └── memory.py # Learn from user feedback
120
+ ├── storage/
121
+ │ ├── __init__.py
122
+ │ └── notion_sync.py # Or sheets_sync.py / supabase_sync.py
123
+ ├── data/
124
+ │ └── feedback.json # User decision history (auto-updated)
125
+ ├── .env.example
126
+ ├── setup.py # One-time DB/schema creation
127
+ ├── enrich_existing.py # Backfill AI scores on old rows
128
+ ├── requirements.txt
129
+ └── .github/
130
+ └── workflows/
131
+ └── scraper.yml # GitHub Actions schedule
132
+ ```
133
+
134
+ ---
135
+
136
+ ### Step 3: Build the Source Connector
137
+
138
+ Template for any data source:
139
+
140
+ ```python
141
+ # scraper/sources/my_source.py
142
+ """
143
+ [Source Name] — gathers [what] from [where].
144
+ Method: [REST API / HTML scraping / RSS feed]
145
+ """
146
+ import requests
147
+ from bs4 import BeautifulSoup
148
+ from datetime import datetime, timezone
149
+ from scraper.filters import is_relevant
150
+
151
+ HEADERS = {
152
+ "User-Agent": "Mozilla/5.0 (compatible; research-bot/1.0)",
153
+ }
154
+
155
+
156
+ def fetch() -> list[dict]:
157
+ """
158
+ Returns a list of items with consistent schema.
159
+ Each item must have at minimum: name, url, date_found.
160
+ """
161
+ results = []
162
+
163
+ # ---- REST API source ----
164
+ resp = requests.get("https://api.example.com/items", headers=HEADERS, timeout=15)
165
+ if resp.status_code == 200:
166
+ for item in resp.json().get("results", []):
167
+ if not is_relevant(item.get("title", "")):
168
+ continue
169
+ results.append(_normalise(item))
170
+
171
+ return results
172
+
173
+
174
+ def _normalise(raw: dict) -> dict:
175
+ """Convert raw API/HTML data to the standard schema."""
176
+ return {
177
+ "name": raw.get("title", ""),
178
+ "url": raw.get("link", ""),
179
+ "source": "MySource",
180
+ "date_found": datetime.now(timezone.utc).date().isoformat(),
181
+ # add domain-specific fields here
182
+ }
183
+ ```
184
+
185
+ **HTML fetch pattern:**
186
+ ```python
187
+ soup = BeautifulSoup(resp.text, "lxml")
188
+ for card in soup.select("[class*='listing']"):
189
+ title = card.select_one("h2, h3").get_text(strip=True)
190
+ link = card.select_one("a")["href"]
191
+ if not link.startswith("http"):
192
+ link = f"https://example.com{link}"
193
+ ```
194
+
195
+ **RSS feed pattern:**
196
+ ```python
197
+ import xml.etree.ElementTree as ET
198
+ root = ET.fromstring(resp.text)
199
+ for item in root.findall(".//item"):
200
+ title = item.findtext("title", "")
201
+ link = item.findtext("link", "")
202
+ ```
203
+
204
+ ---
205
+
206
+ ### Step 4: Build the Gemini AI Client
207
+
208
+ ```python
209
+ # ai/client.py
210
+ import os, json, time, requests
211
+
212
+ _last_call = 0.0
213
+
214
+ MODEL_FALLBACK = [
215
+ "gemini-2.0-flash-lite",
216
+ "gemini-2.0-flash",
217
+ "gemini-2.5-flash",
218
+ "gemini-flash-lite-latest",
219
+ ]
220
+
221
+
222
+ def generate(prompt: str, model: str = "", rate_limit: float = 7.0) -> dict:
223
+ """Call Gemini with auto-fallback on 429. Returns parsed JSON or {}."""
224
+ global _last_call
225
+
226
+ api_key = os.environ.get("GEMINI_API_KEY", "")
227
+ if not api_key:
228
+ return {}
229
+
230
+ elapsed = time.time() - _last_call
231
+ if elapsed < rate_limit:
232
+ time.sleep(rate_limit - elapsed)
233
+
234
+ models = [model] + [m for m in MODEL_FALLBACK if m != model] if model else MODEL_FALLBACK
235
+ _last_call = time.time()
236
+
237
+ for m in models:
238
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{m}:generateContent?key={api_key}"
239
+ payload = {
240
+ "contents": [{"parts": [{"text": prompt}]}],
241
+ "generationConfig": {
242
+ "responseMimeType": "application/json",
243
+ "temperature": 0.3,
244
+ "maxOutputTokens": 2048,
245
+ },
246
+ }
247
+ try:
248
+ resp = requests.post(url, json=payload, timeout=30)
249
+ if resp.status_code == 200:
250
+ return _parse(resp)
251
+ if resp.status_code in (429, 404):
252
+ time.sleep(1)
253
+ continue
254
+ return {}
255
+ except requests.RequestException:
256
+ return {}
257
+
258
+ return {}
259
+
260
+
261
+ def _parse(resp) -> dict:
262
+ try:
263
+ text = (
264
+ resp.json()
265
+ .get("candidates", [{}])[0]
266
+ .get("content", {})
267
+ .get("parts", [{}])[0]
268
+ .get("text", "")
269
+ .strip()
270
+ )
271
+ if text.startswith("```"):
272
+ text = text.split("\n", 1)[-1].rsplit("```", 1)[0]
273
+ return json.loads(text)
274
+ except (json.JSONDecodeError, KeyError):
275
+ return {}
276
+ ```
277
+
278
+ ---
279
+
280
+ ### Step 5: Build the AI Pipeline (Batch)
281
+
282
+ ```python
283
+ # ai/pipeline.py
284
+ import json
285
+ import yaml
286
+ from pathlib import Path
287
+ from ai.client import generate
288
+
289
+ def analyse_batch(items: list[dict], context: str = "", preference_prompt: str = "") -> list[dict]:
290
+ """Analyse items in batches. Returns items enriched with AI fields."""
291
+ config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
292
+ model = config.get("ai", {}).get("model", "gemini-2.5-flash")
293
+ rate_limit = config.get("ai", {}).get("rate_limit_seconds", 7.0)
294
+ min_score = config.get("ai", {}).get("min_score", 0)
295
+ batch_size = config.get("ai", {}).get("batch_size", 5)
296
+
297
+ batches = [items[i:i + batch_size] for i in range(0, len(items), batch_size)]
298
+ print(f" [AI] {len(items)} items → {len(batches)} API calls")
299
+
300
+ enriched = []
301
+ for i, batch in enumerate(batches):
302
+ print(f" [AI] Batch {i + 1}/{len(batches)}...")
303
+ prompt = _build_prompt(batch, context, preference_prompt, config)
304
+ result = generate(prompt, model=model, rate_limit=rate_limit)
305
+
306
+ analyses = result.get("analyses", [])
307
+ for j, item in enumerate(batch):
308
+ ai = analyses[j] if j < len(analyses) else {}
309
+ if ai:
310
+ score = max(0, min(100, int(ai.get("score", 0))))
311
+ if min_score and score < min_score:
312
+ continue
313
+ enriched.append({**item, "ai_score": score, "ai_summary": ai.get("summary", ""), "ai_notes": ai.get("notes", "")})
314
+ else:
315
+ enriched.append(item)
316
+
317
+ return enriched
318
+
319
+
320
+ def _build_prompt(batch, context, preference_prompt, config):
321
+ priorities = config.get("priorities", [])
322
+ items_text = "\n\n".join(
323
+ f"Item {i+1}: {json.dumps({k: v for k, v in item.items() if not k.startswith('_')})}"
324
+ for i, item in enumerate(batch)
325
+ )
326
+
327
+ return f"""Analyse these {len(batch)} items and return a JSON object.
328
+
329
+ # Items
330
+ {items_text}
331
+
332
+ # User Context
333
+ {context[:800] if context else "Not provided"}
334
+
335
+ # User Priorities
336
+ {chr(10).join(f"- {p}" for p in priorities)}
337
+
338
+ {preference_prompt}
339
+
340
+ # Instructions
341
+ Return: {{"analyses": [{{"score": <0-100>, "summary": "<2 sentences>", "notes": "<why this matches or doesn't>"}} for each item in order]}}
342
+ Be concise. Score 90+=excellent match, 70-89=good, 50-69=ok, <50=weak."""
343
+ ```
344
+
345
+ ---
346
+
347
+ ### Step 6: Build the Feedback Learning System
348
+
349
+ ```python
350
+ # ai/memory.py
351
+ """Learn from user decisions to improve future scoring."""
352
+ import json
353
+ from pathlib import Path
354
+
355
+ FEEDBACK_PATH = Path(__file__).parent.parent / "data" / "feedback.json"
356
+
357
+
358
+ def load_feedback() -> dict:
359
+ if FEEDBACK_PATH.exists():
360
+ try:
361
+ return json.loads(FEEDBACK_PATH.read_text())
362
+ except (json.JSONDecodeError, OSError):
363
+ pass
364
+ return {"positive": [], "negative": []}
365
+
366
+
367
+ def save_feedback(fb: dict):
368
+ FEEDBACK_PATH.parent.mkdir(parents=True, exist_ok=True)
369
+ FEEDBACK_PATH.write_text(json.dumps(fb, indent=2))
370
+
371
+
372
+ def build_preference_prompt(feedback: dict, max_examples: int = 15) -> str:
373
+ """Convert feedback history into a prompt bias section."""
374
+ lines = []
375
+ if feedback.get("positive"):
376
+ lines.append("# Items the user LIKED (positive signal):")
377
+ for e in feedback["positive"][-max_examples:]:
378
+ lines.append(f"- {e}")
379
+ if feedback.get("negative"):
380
+ lines.append("\n# Items the user SKIPPED/REJECTED (negative signal):")
381
+ for e in feedback["negative"][-max_examples:]:
382
+ lines.append(f"- {e}")
383
+ if lines:
384
+ lines.append("\nUse these patterns to bias scoring on new items.")
385
+ return "\n".join(lines)
386
+ ```
387
+
388
+ **Integration with your storage layer:** after each run, query your DB for items with positive/negative status and call `save_feedback()` with the extracted patterns.
389
+
390
+ ---
391
+
392
+ ### Step 7: Build Storage (Notion example)
393
+
394
+ ```python
395
+ # storage/notion_sync.py
396
+ import os
397
+ from notion_client import Client
398
+ from notion_client.errors import APIResponseError
399
+
400
+ _client = None
401
+
402
+ def get_client():
403
+ global _client
404
+ if _client is None:
405
+ _client = Client(auth=os.environ["NOTION_TOKEN"])
406
+ return _client
407
+
408
+ def get_existing_urls(db_id: str) -> set[str]:
409
+ """Fetch all URLs already stored — used for deduplication."""
410
+ client, seen, cursor = get_client(), set(), None
411
+ while True:
412
+ resp = client.databases.query(database_id=db_id, page_size=100, **{"start_cursor": cursor} if cursor else {})
413
+ for page in resp["results"]:
414
+ url = page["properties"].get("URL", {}).get("url", "")
415
+ if url: seen.add(url)
416
+ if not resp["has_more"]: break
417
+ cursor = resp["next_cursor"]
418
+ return seen
419
+
420
+ def push_item(db_id: str, item: dict) -> bool:
421
+ """Push one item to Notion. Returns True on success."""
422
+ props = {
423
+ "Name": {"title": [{"text": {"content": item.get("name", "")[:100]}}]},
424
+ "URL": {"url": item.get("url")},
425
+ "Source": {"select": {"name": item.get("source", "Unknown")}},
426
+ "Date Found": {"date": {"start": item.get("date_found")}},
427
+ "Status": {"select": {"name": "New"}},
428
+ }
429
+ # AI fields
430
+ if item.get("ai_score") is not None:
431
+ props["AI Score"] = {"number": item["ai_score"]}
432
+ if item.get("ai_summary"):
433
+ props["Summary"] = {"rich_text": [{"text": {"content": item["ai_summary"][:2000]}}]}
434
+ if item.get("ai_notes"):
435
+ props["Notes"] = {"rich_text": [{"text": {"content": item["ai_notes"][:2000]}}]}
436
+
437
+ try:
438
+ get_client().pages.create(parent={"database_id": db_id}, properties=props)
439
+ return True
440
+ except APIResponseError as e:
441
+ print(f"[notion] Push failed: {e}")
442
+ return False
443
+
444
+ def sync(db_id: str, items: list[dict]) -> tuple[int, int]:
445
+ existing = get_existing_urls(db_id)
446
+ added = skipped = 0
447
+ for item in items:
448
+ if item.get("url") in existing:
449
+ skipped += 1; continue
450
+ if push_item(db_id, item):
451
+ added += 1; existing.add(item["url"])
452
+ else:
453
+ skipped += 1
454
+ return added, skipped
455
+ ```
456
+
457
+ ---
458
+
459
+ ### Step 8: Orchestrate in main.py
460
+
461
+ ```python
462
+ # scraper/main.py
463
+ import os, sys, yaml
464
+ from pathlib import Path
465
+ from dotenv import load_dotenv
466
+
467
+ load_dotenv()
468
+
469
+ from scraper.sources import my_source # add your sources
470
+
471
+ # NOTE: This example uses Notion. If storage.provider is "sheets" or "supabase",
472
+ # replace this import with storage.sheets_sync or storage.supabase_sync and update
473
+ # the env var and sync() call accordingly.
474
+ from storage.notion_sync import sync
475
+
476
+ SOURCES = [
477
+ ("My Source", my_source.fetch),
478
+ ]
479
+
480
+ def ai_enabled():
481
+ return bool(os.environ.get("GEMINI_API_KEY"))
482
+
483
+ def main():
484
+ config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
485
+ provider = config.get("storage", {}).get("provider", "notion")
486
+
487
+ # Resolve the storage target identifier from env based on provider
488
+ if provider == "notion":
489
+ db_id = os.environ.get("NOTION_DATABASE_ID")
490
+ if not db_id:
491
+ print("ERROR: NOTION_DATABASE_ID not set"); sys.exit(1)
492
+ else:
493
+ # Extend here for sheets (SHEET_ID) or supabase (SUPABASE_TABLE) etc.
494
+ print(f"ERROR: provider '{provider}' not yet wired in main.py"); sys.exit(1)
495
+
496
+ config = yaml.safe_load((Path(__file__).parent.parent / "config.yaml").read_text())
497
+ all_items = []
498
+
499
+ for name, fetch_fn in SOURCES:
500
+ try:
501
+ items = fetch_fn()
502
+ print(f"[{name}] {len(items)} items")
503
+ all_items.extend(items)
504
+ except Exception as e:
505
+ print(f"[{name}] FAILED: {e}")
506
+
507
+ # Deduplicate by URL
508
+ seen, deduped = set(), []
509
+ for item in all_items:
510
+ if (url := item.get("url", "")) and url not in seen:
511
+ seen.add(url); deduped.append(item)
512
+
513
+ print(f"Unique items: {len(deduped)}")
514
+
515
+ if ai_enabled() and deduped:
516
+ from ai.memory import load_feedback, build_preference_prompt
517
+ from ai.pipeline import analyse_batch
518
+
519
+ # load_feedback() reads data/feedback.json written by your feedback sync script.
520
+ # To keep it current, implement a separate feedback_sync.py that queries your
521
+ # storage provider for items with positive/negative statuses and calls save_feedback().
522
+ feedback = load_feedback()
523
+ preference = build_preference_prompt(feedback)
524
+ context_path = Path(__file__).parent.parent / "profile" / "context.md"
525
+ context = context_path.read_text() if context_path.exists() else ""
526
+ deduped = analyse_batch(deduped, context=context, preference_prompt=preference)
527
+ else:
528
+ print("[AI] Skipped — GEMINI_API_KEY not set")
529
+
530
+ added, skipped = sync(db_id, deduped)
531
+ print(f"Done — {added} new, {skipped} existing")
532
+
533
+ if __name__ == "__main__":
534
+ main()
535
+ ```
536
+
537
+ ---
538
+
539
+ ### Step 9: GitHub Actions Workflow
540
+
541
+ ```yaml
542
+ # .github/workflows/scraper.yml
543
+ name: Data Scraper Agent
544
+
545
+ on:
546
+ schedule:
547
+ - cron: "0 */3 * * *" # every 3 hours — adjust to your needs
548
+ workflow_dispatch: # allow manual trigger
549
+
550
+ permissions:
551
+ contents: write # required for the feedback-history commit step
552
+
553
+ jobs:
554
+ scrape:
555
+ runs-on: ubuntu-latest
556
+ timeout-minutes: 20
557
+
558
+ steps:
559
+ - uses: actions/checkout@v4
560
+
561
+ - uses: actions/setup-python@v5
562
+ with:
563
+ python-version: "3.11"
564
+ cache: "pip"
565
+
566
+ - run: pip install -r requirements.txt
567
+
568
+ # Uncomment if Playwright is enabled in requirements.txt
569
+ # - name: Install Playwright browsers
570
+ # run: python -m playwright install chromium --with-deps
571
+
572
+ - name: Run agent
573
+ env:
574
+ NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
575
+ NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
576
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
577
+ run: python -m scraper.main
578
+
579
+ - name: Commit feedback history
580
+ run: |
581
+ git config user.name "github-actions[bot]"
582
+ git config user.email "github-actions[bot]@users.noreply.github.com"
583
+ git add data/feedback.json || true
584
+ git diff --cached --quiet || git commit -m "chore: update feedback history"
585
+ git push
586
+ ```
587
+
588
+ ---
589
+
590
+ ### Step 10: config.yaml Template
591
+
592
+ ```yaml
593
+ # Customise this file — no code changes needed
594
+
595
+ # What to collect (pre-filter before AI)
596
+ filters:
597
+ required_keywords: [] # item must contain at least one
598
+ blocked_keywords: [] # item must not contain any
599
+
600
+ # Your priorities — AI uses these for scoring
601
+ priorities:
602
+ - "example priority 1"
603
+ - "example priority 2"
604
+
605
+ # Storage
606
+ storage:
607
+ provider: "notion" # notion | sheets | supabase | sqlite
608
+
609
+ # Feedback learning
610
+ feedback:
611
+ positive_statuses: ["Saved", "Applied", "Interested"]
612
+ negative_statuses: ["Skip", "Rejected", "Not relevant"]
613
+
614
+ # AI settings
615
+ ai:
616
+ enabled: true
617
+ model: "gemini-2.5-flash"
618
+ min_score: 0 # filter out items below this score
619
+ rate_limit_seconds: 7 # seconds between API calls
620
+ batch_size: 5 # items per API call
621
+ ```
622
+
623
+ ---
624
+
625
+ ## Common Scraping Patterns
626
+
627
+ ### Pattern 1: REST API (easiest)
628
+ ```python
629
+ resp = requests.get(url, params={"q": query}, headers=HEADERS, timeout=15)
630
+ items = resp.json().get("results", [])
631
+ ```
632
+
633
+ ### Pattern 2: HTML Scraping
634
+ ```python
635
+ soup = BeautifulSoup(resp.text, "lxml")
636
+ for card in soup.select(".listing-card"):
637
+ title = card.select_one("h2").get_text(strip=True)
638
+ href = card.select_one("a")["href"]
639
+ ```
640
+
641
+ ### Pattern 3: RSS Feed
642
+ ```python
643
+ import xml.etree.ElementTree as ET
644
+ root = ET.fromstring(resp.text)
645
+ for item in root.findall(".//item"):
646
+ title = item.findtext("title", "")
647
+ link = item.findtext("link", "")
648
+ pub_date = item.findtext("pubDate", "")
649
+ ```
650
+
651
+ ### Pattern 4: Paginated API
652
+ ```python
653
+ page = 1
654
+ while True:
655
+ resp = requests.get(url, params={"page": page, "limit": 50}, timeout=15)
656
+ data = resp.json()
657
+ items = data.get("results", [])
658
+ if not items:
659
+ break
660
+ for item in items:
661
+ results.append(_normalise(item))
662
+ if not data.get("has_more"):
663
+ break
664
+ page += 1
665
+ ```
666
+
667
+ ### Pattern 5: JS-Rendered Pages (Playwright)
668
+ ```python
669
+ from playwright.sync_api import sync_playwright
670
+
671
+ with sync_playwright() as p:
672
+ browser = p.chromium.launch()
673
+ page = browser.new_page()
674
+ page.goto(url)
675
+ page.wait_for_selector(".listing")
676
+ html = page.content()
677
+ browser.close()
678
+
679
+ soup = BeautifulSoup(html, "lxml")
680
+ ```
681
+
682
+ ---
683
+
684
+ ## Anti-Patterns to Avoid
685
+
686
+ | Anti-pattern | Problem | Fix |
687
+ |---|---|---|
688
+ | One LLM call per item | Hits rate limits instantly | Batch 5 items per call |
689
+ | Hardcoded keywords in code | Not reusable | Move all config to `config.yaml` |
690
+ | Scraping without rate limit | IP ban | Add `time.sleep(1)` between requests |
691
+ | Storing secrets in code | Security risk | Always use `.env` + GitHub Secrets |
692
+ | No deduplication | Duplicate rows pile up | Always check URL before pushing |
693
+ | Ignoring `robots.txt` | Legal/ethical risk | Respect crawl rules; use public APIs when available |
694
+ | JS-rendered sites with `requests` | Empty response | Use Playwright or look for the underlying API |
695
+ | `maxOutputTokens` too low | Truncated JSON, parse error | Use 2048+ for batch responses |
696
+
697
+ ---
698
+
699
+ ## Free Tier Limits Reference
700
+
701
+ | Service | Free Limit | Typical Usage |
702
+ |---|---|---|
703
+ | Gemini Flash Lite | 30 RPM, 1500 RPD | ~56 req/day at 3-hr intervals |
704
+ | Gemini 2.0 Flash | 15 RPM, 1500 RPD | Good fallback |
705
+ | Gemini 2.5 Flash | 10 RPM, 500 RPD | Use sparingly |
706
+ | GitHub Actions | Unlimited (public repos) | ~20 min/day |
707
+ | Notion API | Unlimited | ~200 writes/day |
708
+ | Supabase | 500MB DB, 2GB transfer | Fine for most agents |
709
+ | Google Sheets API | 300 req/min | Works for small agents |
710
+
711
+ ---
712
+
713
+ ## Requirements Template
714
+
715
+ ```
716
+ requests==2.31.0
717
+ beautifulsoup4==4.12.3
718
+ lxml==5.1.0
719
+ python-dotenv==1.0.1
720
+ pyyaml==6.0.2
721
+ notion-client==2.2.1 # if using Notion
722
+ # playwright==1.40.0 # uncomment for JS-rendered sites
723
+ ```
724
+
725
+ ---
726
+
727
+ ## Quality Checklist
728
+
729
+ Before marking the agent complete:
730
+
731
+ - [ ] `config.yaml` controls all user-facing settings — no hardcoded values
732
+ - [ ] `profile/context.md` holds user-specific context for AI matching
733
+ - [ ] Deduplication by URL before every storage push
734
+ - [ ] Gemini client has model fallback chain (4 models)
735
+ - [ ] Batch size ≤ 5 items per API call
736
+ - [ ] `maxOutputTokens` ≥ 2048
737
+ - [ ] `.env` is in `.gitignore`
738
+ - [ ] `.env.example` provided for onboarding
739
+ - [ ] `setup.py` creates DB schema on first run
740
+ - [ ] `enrich_existing.py` backfills AI scores on old rows
741
+ - [ ] GitHub Actions workflow commits `feedback.json` after each run
742
+ - [ ] README covers: setup in < 5 minutes, required secrets, customisation
743
+
744
+ ---
745
+
746
+ ## Real-World Examples
747
+
748
+ ```
749
+ "Build me an agent that monitors Hacker News for AI startup funding news"
750
+ "Scrape product prices from 3 e-commerce sites and alert when they drop"
751
+ "Track new GitHub repos tagged with 'llm' or 'agents' — summarise each one"
752
+ "Collect Chief of Staff job listings from LinkedIn and Cutshort into Notion"
753
+ "Monitor a subreddit for posts mentioning my company — classify sentiment"
754
+ "Scrape new academic papers from arXiv on a topic I care about daily"
755
+ "Track sports fixture results and keep a running table in Google Sheets"
756
+ "Build a real estate listing watcher — alert on new properties under ₹1 Cr"
757
+ ```
758
+
759
+ ---
760
+
761
+ ## Reference Implementation
762
+
763
+ A complete working agent built with this exact architecture would collect from 4+ sources,
764
+ batch Gemini calls, learn from Applied/Rejected decisions stored in Notion, and run
765
+ 100% free on GitHub Actions. Follow Steps 1–9 above to build your own.