codeply-cli 0.3.1 → 0.4.1

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 (375) hide show
  1. package/THIRD_PARTY_NOTICES.md +63 -63
  2. package/bin/codeply.js +127 -0
  3. package/lib/agent.mjs +2139 -2139
  4. package/lib/ai.mjs +6 -6
  5. package/lib/apply-patch.js +217 -217
  6. package/lib/arity-table.json +138 -138
  7. package/lib/arity.js +70 -70
  8. package/lib/auth.mjs +6 -6
  9. package/lib/cloud.mjs +796 -0
  10. package/lib/commands.js +103 -103
  11. package/lib/design-library/assemble-artifact.mjs +21 -21
  12. package/lib/design-library/cleanup.mjs +45 -45
  13. package/lib/design-library/index.json +22379 -22379
  14. package/lib/design-library/make-table-data.mjs +39 -39
  15. package/lib/diagnostics.js +192 -192
  16. package/lib/edit-engine.js +364 -364
  17. package/lib/github-agent.mjs +311 -307
  18. package/lib/history.js +75 -75
  19. package/lib/mcp.js +258 -258
  20. package/lib/native-tools.mjs +189 -189
  21. package/lib/oauth-connectors.js +2 -1
  22. package/lib/permissions.js +109 -109
  23. package/lib/plugins.js +353 -353
  24. package/lib/server.mjs +457 -457
  25. package/lib/session-db.js +220 -220
  26. package/lib/share.js +168 -168
  27. package/lib/skills.js +416 -416
  28. package/lib/snapshot.js +146 -146
  29. package/lib/terminal-hints.js +154 -154
  30. package/lib/tools.mjs +2012 -2012
  31. package/lib/typecheck-worker.js +147 -147
  32. package/lib/web-tools.js +158 -158
  33. package/package.json +49 -47
  34. package/skills/SOURCE.md +58 -58
  35. package/skills/accessibility/SKILL.md +147 -147
  36. package/skills/agent-architecture-audit/SKILL.md +257 -257
  37. package/skills/agent-eval/SKILL.md +147 -147
  38. package/skills/agent-harness-construction/SKILL.md +74 -74
  39. package/skills/agent-introspection-debugging/SKILL.md +154 -154
  40. package/skills/agent-payment-x402/SKILL.md +225 -225
  41. package/skills/agent-self-evaluation/SKILL.md +182 -182
  42. package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
  43. package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
  44. package/skills/agent-sort/SKILL.md +216 -216
  45. package/skills/agentic-engineering/SKILL.md +64 -64
  46. package/skills/agentic-os/SKILL.md +388 -388
  47. package/skills/ai-first-engineering/SKILL.md +52 -52
  48. package/skills/ai-regression-testing/SKILL.md +386 -386
  49. package/skills/android-clean-architecture/SKILL.md +340 -340
  50. package/skills/angular-developer/SKILL.md +155 -155
  51. package/skills/angular-developer/references/angular-animations.md +160 -160
  52. package/skills/angular-developer/references/angular-aria.md +410 -410
  53. package/skills/angular-developer/references/cli.md +86 -86
  54. package/skills/angular-developer/references/component-harnesses.md +59 -59
  55. package/skills/angular-developer/references/component-styling.md +91 -91
  56. package/skills/angular-developer/references/components.md +117 -117
  57. package/skills/angular-developer/references/creating-services.md +97 -97
  58. package/skills/angular-developer/references/data-resolvers.md +69 -69
  59. package/skills/angular-developer/references/define-routes.md +67 -67
  60. package/skills/angular-developer/references/defining-providers.md +72 -72
  61. package/skills/angular-developer/references/di-fundamentals.md +120 -120
  62. package/skills/angular-developer/references/e2e-testing.md +56 -56
  63. package/skills/angular-developer/references/effects.md +83 -83
  64. package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
  65. package/skills/angular-developer/references/host-elements.md +80 -80
  66. package/skills/angular-developer/references/injection-context.md +63 -63
  67. package/skills/angular-developer/references/inputs.md +101 -101
  68. package/skills/angular-developer/references/linked-signal.md +59 -59
  69. package/skills/angular-developer/references/loading-strategies.md +61 -61
  70. package/skills/angular-developer/references/mcp.md +108 -108
  71. package/skills/angular-developer/references/navigate-to-routes.md +69 -69
  72. package/skills/angular-developer/references/outputs.md +86 -86
  73. package/skills/angular-developer/references/reactive-forms.md +122 -122
  74. package/skills/angular-developer/references/rendering-strategies.md +44 -44
  75. package/skills/angular-developer/references/resource.md +77 -77
  76. package/skills/angular-developer/references/route-animations.md +56 -56
  77. package/skills/angular-developer/references/route-guards.md +52 -52
  78. package/skills/angular-developer/references/router-lifecycle.md +45 -45
  79. package/skills/angular-developer/references/router-testing.md +87 -87
  80. package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
  81. package/skills/angular-developer/references/signal-forms.md +795 -795
  82. package/skills/angular-developer/references/signals-overview.md +94 -94
  83. package/skills/angular-developer/references/tailwind-css.md +69 -69
  84. package/skills/angular-developer/references/template-driven-forms.md +114 -114
  85. package/skills/angular-developer/references/testing-fundamentals.md +65 -65
  86. package/skills/api-connector-builder/SKILL.md +121 -121
  87. package/skills/api-design/SKILL.md +524 -524
  88. package/skills/architecture-decision-records/SKILL.md +180 -180
  89. package/skills/article-writing/SKILL.md +80 -80
  90. package/skills/automation-audit-ops/SKILL.md +143 -143
  91. package/skills/autonomous-agent-harness/SKILL.md +274 -274
  92. package/skills/autonomous-loops/SKILL.md +611 -611
  93. package/skills/backend-patterns/SKILL.md +562 -562
  94. package/skills/benchmark/SKILL.md +95 -95
  95. package/skills/benchmark-methodology/SKILL.md +191 -191
  96. package/skills/benchmark-optimization-loop/SKILL.md +71 -71
  97. package/skills/blender-motion-state-inspection/SKILL.md +165 -165
  98. package/skills/blueprint/SKILL.md +106 -106
  99. package/skills/brand-discovery/SKILL.md +145 -145
  100. package/skills/brand-discovery/references/10_purpose-why.md +40 -40
  101. package/skills/brand-discovery/references/20_positioning.md +44 -44
  102. package/skills/brand-discovery/references/30_audience-niche.md +52 -52
  103. package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
  104. package/skills/brand-discovery/references/50_voice-tone.md +59 -59
  105. package/skills/brand-discovery/references/60_narrative-story.md +50 -50
  106. package/skills/brand-discovery/references/70_founder-tension.md +49 -49
  107. package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
  108. package/skills/brand-voice/SKILL.md +98 -98
  109. package/skills/brand-voice/references/voice-profile-schema.md +55 -55
  110. package/skills/browser-qa/SKILL.md +105 -105
  111. package/skills/bun-runtime/SKILL.md +85 -85
  112. package/skills/canary-watch/SKILL.md +108 -108
  113. package/skills/carrier-relationship-management/SKILL.md +212 -212
  114. package/skills/cisco-ios-patterns/SKILL.md +164 -164
  115. package/skills/ck/SKILL.md +148 -148
  116. package/skills/claude-devfleet/SKILL.md +112 -112
  117. package/skills/click-path-audit/SKILL.md +245 -245
  118. package/skills/clickhouse-io/SKILL.md +445 -445
  119. package/skills/code-tour/SKILL.md +254 -254
  120. package/skills/codebase-onboarding/SKILL.md +234 -234
  121. package/skills/codehealth-mcp/SKILL.md +167 -167
  122. package/skills/coding-standards/SKILL.md +551 -551
  123. package/skills/competitive-platform-analysis/SKILL.md +214 -214
  124. package/skills/competitive-report-structure/SKILL.md +162 -162
  125. package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
  126. package/skills/config-gc/SKILL.md +120 -120
  127. package/skills/configure-ecc/SKILL.md +385 -385
  128. package/skills/connections-optimizer/SKILL.md +190 -190
  129. package/skills/content-engine/SKILL.md +132 -132
  130. package/skills/content-hash-cache-pattern/SKILL.md +162 -162
  131. package/skills/context-budget/SKILL.md +136 -136
  132. package/skills/continuous-agent-loop/SKILL.md +46 -46
  133. package/skills/continuous-learning/SKILL.md +132 -132
  134. package/skills/continuous-learning-v2/SKILL.md +361 -361
  135. package/skills/contract-first/SKILL.md +287 -287
  136. package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
  137. package/skills/cost-tracking/SKILL.md +97 -97
  138. package/skills/council/SKILL.md +204 -204
  139. package/skills/cpp-coding-standards/SKILL.md +724 -724
  140. package/skills/cpp-testing/SKILL.md +325 -325
  141. package/skills/crosspost/SKILL.md +112 -112
  142. package/skills/csharp-testing/SKILL.md +322 -322
  143. package/skills/customer-billing-ops/SKILL.md +141 -141
  144. package/skills/customs-trade-compliance/SKILL.md +263 -263
  145. package/skills/dart-flutter-patterns/SKILL.md +564 -564
  146. package/skills/dashboard-builder/SKILL.md +109 -109
  147. package/skills/data-scraper-agent/SKILL.md +765 -765
  148. package/skills/data-throughput-accelerator/SKILL.md +74 -74
  149. package/skills/database-migrations/SKILL.md +430 -430
  150. package/skills/deep-research/SKILL.md +160 -160
  151. package/skills/defi-amm-security/SKILL.md +167 -167
  152. package/skills/delivery-gate/SKILL.md +126 -126
  153. package/skills/deployment-patterns/SKILL.md +428 -428
  154. package/skills/design-system/SKILL.md +83 -83
  155. package/skills/django-celery/SKILL.md +458 -458
  156. package/skills/django-patterns/SKILL.md +735 -735
  157. package/skills/django-security/SKILL.md +644 -644
  158. package/skills/django-tdd/SKILL.md +730 -730
  159. package/skills/django-verification/SKILL.md +470 -470
  160. package/skills/dmux-workflows/SKILL.md +192 -192
  161. package/skills/docker-patterns/SKILL.md +365 -365
  162. package/skills/documentation-lookup/SKILL.md +91 -91
  163. package/skills/dotnet-patterns/SKILL.md +322 -322
  164. package/skills/dynamic-workflow-mode/SKILL.md +124 -124
  165. package/skills/e2e-testing/SKILL.md +327 -327
  166. package/skills/ecc-guide/SKILL.md +190 -190
  167. package/skills/ecc-recipes/SKILL.md +149 -149
  168. package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
  169. package/skills/email-ops/SKILL.md +122 -122
  170. package/skills/energy-procurement/SKILL.md +228 -228
  171. package/skills/enterprise-agent-ops/SKILL.md +51 -51
  172. package/skills/error-handling/SKILL.md +377 -377
  173. package/skills/eval-harness/SKILL.md +271 -271
  174. package/skills/evm-token-decimals/SKILL.md +131 -131
  175. package/skills/exa-search/SKILL.md +108 -108
  176. package/skills/fal-ai-media/SKILL.md +289 -289
  177. package/skills/fastapi-patterns/SKILL.md +514 -514
  178. package/skills/finance-billing-ops/SKILL.md +128 -128
  179. package/skills/flox-environments/SKILL.md +497 -497
  180. package/skills/flutter-dart-code-review/SKILL.md +436 -436
  181. package/skills/foundation-models-on-device/SKILL.md +243 -243
  182. package/skills/frontend-a11y/SKILL.md +446 -446
  183. package/skills/frontend-design-direction/SKILL.md +93 -93
  184. package/skills/frontend-patterns/SKILL.md +657 -657
  185. package/skills/frontend-slides/SKILL.md +185 -185
  186. package/skills/fsharp-testing/SKILL.md +281 -281
  187. package/skills/gan-style-harness/SKILL.md +279 -279
  188. package/skills/gateguard/SKILL.md +133 -133
  189. package/skills/generating-python-installer/SKILL.md +820 -820
  190. package/skills/git-workflow/SKILL.md +716 -716
  191. package/skills/github-ops/SKILL.md +145 -145
  192. package/skills/golang-patterns/SKILL.md +676 -676
  193. package/skills/golang-testing/SKILL.md +721 -721
  194. package/skills/google-workspace-ops/SKILL.md +96 -96
  195. package/skills/growth-log/SKILL.md +128 -128
  196. package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
  197. package/skills/healthcare-emr-patterns/SKILL.md +160 -160
  198. package/skills/healthcare-eval-harness/SKILL.md +208 -208
  199. package/skills/healthcare-phi-compliance/SKILL.md +146 -146
  200. package/skills/hermes-imports/SKILL.md +89 -89
  201. package/skills/hexagonal-architecture/SKILL.md +277 -277
  202. package/skills/hipaa-compliance/SKILL.md +79 -79
  203. package/skills/homelab-network-readiness/SKILL.md +170 -170
  204. package/skills/homelab-network-setup/SKILL.md +130 -130
  205. package/skills/homelab-pihole-dns/SKILL.md +275 -275
  206. package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
  207. package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
  208. package/skills/hookify-rules/SKILL.md +128 -128
  209. package/skills/inherit-legacy-style/SKILL.md +157 -157
  210. package/skills/intent-driven-development/SKILL.md +360 -360
  211. package/skills/inventory-demand-planning/SKILL.md +247 -247
  212. package/skills/investor-materials/SKILL.md +97 -97
  213. package/skills/investor-outreach/SKILL.md +92 -92
  214. package/skills/ios-icon-gen/SKILL.md +158 -158
  215. package/skills/iterative-retrieval/SKILL.md +212 -212
  216. package/skills/ito-basket-compare/SKILL.md +64 -64
  217. package/skills/ito-compute/SKILL.md +129 -129
  218. package/skills/ito-data-atlas-agent/SKILL.md +64 -64
  219. package/skills/ito-market-intelligence/SKILL.md +61 -61
  220. package/skills/ito-trade-planner/SKILL.md +68 -68
  221. package/skills/java-coding-standards/SKILL.md +384 -384
  222. package/skills/jira-integration/SKILL.md +303 -303
  223. package/skills/jpa-patterns/SKILL.md +152 -152
  224. package/skills/knowledge-ops/SKILL.md +155 -155
  225. package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
  226. package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
  227. package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
  228. package/skills/kotlin-patterns/SKILL.md +712 -712
  229. package/skills/kotlin-testing/SKILL.md +825 -825
  230. package/skills/kubernetes-patterns/SKILL.md +756 -756
  231. package/skills/laravel-patterns/SKILL.md +416 -416
  232. package/skills/laravel-plugin-discovery/SKILL.md +230 -230
  233. package/skills/laravel-security/SKILL.md +948 -948
  234. package/skills/laravel-tdd/SKILL.md +675 -675
  235. package/skills/laravel-verification/SKILL.md +180 -180
  236. package/skills/latency-critical-systems/SKILL.md +75 -75
  237. package/skills/lead-intelligence/SKILL.md +322 -322
  238. package/skills/liquid-glass-design/SKILL.md +279 -279
  239. package/skills/llm-trading-agent-security/SKILL.md +147 -147
  240. package/skills/logistics-exception-management/SKILL.md +222 -222
  241. package/skills/loop-design-check/SKILL.md +143 -143
  242. package/skills/mailtrap-email-integration/SKILL.md +77 -77
  243. package/skills/make-interfaces-feel-better/SKILL.md +152 -152
  244. package/skills/manim-video/SKILL.md +90 -90
  245. package/skills/market-research/SKILL.md +76 -76
  246. package/skills/marketing-campaign/SKILL.md +114 -114
  247. package/skills/mcp-server-patterns/SKILL.md +70 -70
  248. package/skills/messages-ops/SKILL.md +105 -105
  249. package/skills/ml-adoption-playbook/SKILL.md +57 -57
  250. package/skills/mle-workflow/SKILL.md +348 -348
  251. package/skills/motion-advanced/SKILL.md +596 -596
  252. package/skills/motion-foundations/SKILL.md +299 -299
  253. package/skills/motion-patterns/SKILL.md +434 -434
  254. package/skills/motion-ui/SKILL.md +576 -576
  255. package/skills/mysql-patterns/SKILL.md +413 -413
  256. package/skills/nanoclaw-repl/SKILL.md +34 -34
  257. package/skills/nestjs-patterns/SKILL.md +231 -231
  258. package/skills/netmiko-ssh-automation/SKILL.md +174 -174
  259. package/skills/network-bgp-diagnostics/SKILL.md +168 -168
  260. package/skills/network-config-validation/SKILL.md +211 -211
  261. package/skills/network-interface-health/SKILL.md +153 -153
  262. package/skills/nextjs-turbopack/SKILL.md +58 -58
  263. package/skills/nodejs-keccak256/SKILL.md +103 -103
  264. package/skills/nutrient-document-processing/SKILL.md +168 -168
  265. package/skills/nuxt4-patterns/SKILL.md +101 -101
  266. package/skills/openclaw-persona-forge/SKILL.md +289 -289
  267. package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
  268. package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
  269. package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
  270. package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
  271. package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
  272. package/skills/openclaw-persona-forge/references/output-template.md +166 -166
  273. package/skills/opensource-pipeline/SKILL.md +256 -256
  274. package/skills/orch-add-feature/SKILL.md +45 -45
  275. package/skills/orch-build-mvp/SKILL.md +49 -49
  276. package/skills/orch-change-feature/SKILL.md +43 -43
  277. package/skills/orch-fix-defect/SKILL.md +43 -43
  278. package/skills/orch-pipeline/SKILL.md +121 -121
  279. package/skills/orch-refine-code/SKILL.md +44 -44
  280. package/skills/parallel-execution-optimizer/SKILL.md +74 -74
  281. package/skills/perl-patterns/SKILL.md +505 -505
  282. package/skills/perl-security/SKILL.md +504 -504
  283. package/skills/perl-testing/SKILL.md +476 -476
  284. package/skills/plan-canvas/SKILL.md +153 -153
  285. package/skills/plan-orchestrate/SKILL.md +263 -263
  286. package/skills/plankton-code-quality/SKILL.md +237 -237
  287. package/skills/postgres-patterns/SKILL.md +148 -148
  288. package/skills/prediction-market-oracle-research/SKILL.md +64 -64
  289. package/skills/prediction-market-risk-review/SKILL.md +61 -61
  290. package/skills/premium-web-design/SKILL.md +154 -154
  291. package/skills/prisma-patterns/SKILL.md +401 -401
  292. package/skills/product-capability/SKILL.md +142 -142
  293. package/skills/product-lens/SKILL.md +93 -93
  294. package/skills/production-audit/SKILL.md +207 -207
  295. package/skills/production-scheduling/SKILL.md +238 -238
  296. package/skills/project-flow-ops/SKILL.md +112 -112
  297. package/skills/prompt-optimizer/SKILL.md +398 -398
  298. package/skills/python-patterns/SKILL.md +751 -751
  299. package/skills/python-testing/SKILL.md +817 -817
  300. package/skills/pytorch-patterns/SKILL.md +397 -397
  301. package/skills/quality-nonconformance/SKILL.md +260 -260
  302. package/skills/quarkus-patterns/SKILL.md +723 -723
  303. package/skills/quarkus-security/SKILL.md +468 -468
  304. package/skills/quarkus-tdd/SKILL.md +812 -812
  305. package/skills/quarkus-verification/SKILL.md +480 -480
  306. package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
  307. package/skills/react-native-patterns/SKILL.md +326 -326
  308. package/skills/react-patterns/SKILL.md +342 -342
  309. package/skills/react-performance/SKILL.md +575 -575
  310. package/skills/react-testing/SKILL.md +424 -424
  311. package/skills/recsys-pipeline-architect/SKILL.md +115 -115
  312. package/skills/recursive-decision-ledger/SKILL.md +81 -81
  313. package/skills/redis-patterns/SKILL.md +404 -404
  314. package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
  315. package/skills/remotion-video-creation/SKILL.md +43 -43
  316. package/skills/repo-scan/SKILL.md +79 -79
  317. package/skills/research-ops/SKILL.md +113 -113
  318. package/skills/returns-reverse-logistics/SKILL.md +240 -240
  319. package/skills/rules-distill/SKILL.md +265 -265
  320. package/skills/rust-patterns/SKILL.md +500 -500
  321. package/skills/rust-testing/SKILL.md +501 -501
  322. package/skills/safety-guard/SKILL.md +76 -76
  323. package/skills/santa-method/SKILL.md +307 -307
  324. package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
  325. package/skills/scientific-db-uspto-database/SKILL.md +178 -178
  326. package/skills/scientific-pkg-gget/SKILL.md +167 -167
  327. package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
  328. package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
  329. package/skills/search-first/SKILL.md +183 -183
  330. package/skills/security-bounty-hunter/SKILL.md +100 -100
  331. package/skills/security-review/SKILL.md +504 -504
  332. package/skills/security-scan/SKILL.md +166 -166
  333. package/skills/seo/SKILL.md +155 -155
  334. package/skills/skill-comply/SKILL.md +59 -59
  335. package/skills/skill-scout/SKILL.md +141 -141
  336. package/skills/skill-stocktake/SKILL.md +195 -195
  337. package/skills/social-graph-ranker/SKILL.md +155 -155
  338. package/skills/social-publisher/SKILL.md +130 -130
  339. package/skills/springboot-patterns/SKILL.md +315 -315
  340. package/skills/springboot-security/SKILL.md +273 -273
  341. package/skills/springboot-tdd/SKILL.md +159 -159
  342. package/skills/springboot-verification/SKILL.md +232 -232
  343. package/skills/strategic-compact/SKILL.md +142 -142
  344. package/skills/swift-actor-persistence/SKILL.md +144 -144
  345. package/skills/swift-concurrency-6-2/SKILL.md +216 -216
  346. package/skills/swift-protocol-di-testing/SKILL.md +191 -191
  347. package/skills/swiftui-patterns/SKILL.md +259 -259
  348. package/skills/taste/SKILL.md +264 -264
  349. package/skills/taste/references/genre-taxonomy.md +87 -87
  350. package/skills/tdd-workflow/SKILL.md +583 -583
  351. package/skills/team-agent-orchestration/SKILL.md +111 -111
  352. package/skills/team-builder/SKILL.md +169 -169
  353. package/skills/terminal-ops/SKILL.md +110 -110
  354. package/skills/tinystruct-patterns/SKILL.md +279 -279
  355. package/skills/tinystruct-patterns/references/architecture.md +90 -90
  356. package/skills/tinystruct-patterns/references/data-handling.md +60 -60
  357. package/skills/tinystruct-patterns/references/database.md +99 -99
  358. package/skills/tinystruct-patterns/references/routing.md +64 -64
  359. package/skills/tinystruct-patterns/references/system-usage.md +97 -97
  360. package/skills/tinystruct-patterns/references/testing.md +72 -72
  361. package/skills/token-budget-advisor/SKILL.md +134 -134
  362. package/skills/ui-demo/SKILL.md +466 -466
  363. package/skills/ui-to-vue/SKILL.md +135 -135
  364. package/skills/uncloud/SKILL.md +344 -344
  365. package/skills/unified-memory/SKILL.md +170 -170
  366. package/skills/unified-notifications-ops/SKILL.md +188 -188
  367. package/skills/verification-loop/SKILL.md +129 -129
  368. package/skills/video-editing/SKILL.md +311 -311
  369. package/skills/videodb/SKILL.md +375 -375
  370. package/skills/visa-doc-translate/SKILL.md +117 -117
  371. package/skills/vite-patterns/SKILL.md +450 -450
  372. package/skills/vue-patterns/SKILL.md +471 -471
  373. package/skills/windows-desktop-e2e/SKILL.md +888 -888
  374. package/skills/workspace-surface-audit/SKILL.md +126 -126
  375. 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.