codeply-cli 0.5.5 → 0.6.0

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