xtrm-tools 0.7.3 → 0.7.7

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 (533) hide show
  1. package/.xtrm/config/hooks.json +3 -0
  2. package/.xtrm/config/pi/extensions/xtrm-ui/format.ts +189 -0
  3. package/.xtrm/config/pi/extensions/xtrm-ui/index.ts +76 -17
  4. package/.xtrm/config/pi/extensions/xtrm-ui/package.json +16 -5
  5. package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.combined.log +7 -0
  6. package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.stderr.log +0 -0
  7. package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.stdout.log +7 -0
  8. package/.xtrm/ext-src/xtrm-ui/format.ts +282 -0
  9. package/.xtrm/{extensions → ext-src}/xtrm-ui/index.ts +76 -17
  10. package/.xtrm/ext-src/xtrm-ui/package.json +21 -0
  11. package/.xtrm/hooks/specialists/specialists-complete.mjs +70 -0
  12. package/.xtrm/hooks/specialists/specialists-session-start.mjs +105 -0
  13. package/.xtrm/registry.json +397 -409
  14. package/.xtrm/skills/default/README.txt +31 -0
  15. package/.xtrm/skills/default/clean-code/SKILL.md +201 -0
  16. package/.xtrm/skills/default/creating-service-skills/SKILL.md +433 -0
  17. package/.xtrm/skills/default/creating-service-skills/references/script_quality_standards.md +425 -0
  18. package/.xtrm/skills/default/creating-service-skills/references/service_skill_system_guide.md +278 -0
  19. package/.xtrm/skills/default/creating-service-skills/scripts/bootstrap.py +326 -0
  20. package/.xtrm/skills/default/creating-service-skills/scripts/deep_dive.py +304 -0
  21. package/.xtrm/skills/default/creating-service-skills/scripts/scaffolder.py +482 -0
  22. package/.xtrm/skills/default/deepwiki/SKILL.md +50 -0
  23. package/.xtrm/skills/default/delegating/SKILL.md +196 -0
  24. package/.xtrm/skills/default/delegating/config.yaml +210 -0
  25. package/.xtrm/skills/default/delegating/references/orchestration-protocols.md +41 -0
  26. package/.xtrm/skills/default/documenting/CHANGELOG.md +23 -0
  27. package/.xtrm/skills/default/documenting/README.md +148 -0
  28. package/.xtrm/skills/default/documenting/SKILL.md +113 -0
  29. package/.xtrm/skills/default/documenting/examples/example_pattern.md +70 -0
  30. package/.xtrm/skills/default/documenting/examples/example_reference.md +70 -0
  31. package/.xtrm/skills/default/documenting/examples/example_ssot_analytics.md +64 -0
  32. package/.xtrm/skills/default/documenting/examples/example_workflow.md +141 -0
  33. package/.xtrm/skills/default/documenting/references/changelog-format.md +97 -0
  34. package/.xtrm/skills/default/documenting/references/metadata-schema.md +136 -0
  35. package/.xtrm/skills/default/documenting/references/taxonomy.md +81 -0
  36. package/.xtrm/skills/default/documenting/references/versioning-rules.md +78 -0
  37. package/.xtrm/skills/default/documenting/scripts/bump_version.sh +60 -0
  38. package/.xtrm/skills/default/documenting/scripts/changelog/__init__.py +0 -0
  39. package/.xtrm/skills/default/documenting/scripts/changelog/add_entry.py +216 -0
  40. package/.xtrm/skills/default/documenting/scripts/changelog/bump_release.py +117 -0
  41. package/.xtrm/skills/default/documenting/scripts/changelog/init_changelog.py +54 -0
  42. package/.xtrm/skills/default/documenting/scripts/changelog/validate_changelog.py +128 -0
  43. package/.xtrm/skills/default/documenting/scripts/drift_detector.py +266 -0
  44. package/.xtrm/skills/default/documenting/scripts/generate_template.py +311 -0
  45. package/.xtrm/skills/default/documenting/scripts/list_by_category.sh +84 -0
  46. package/.xtrm/skills/default/documenting/scripts/orchestrator.py +255 -0
  47. package/.xtrm/skills/default/documenting/scripts/validate_metadata.py +242 -0
  48. package/.xtrm/skills/default/documenting/templates/CHANGELOG.md.template +13 -0
  49. package/.xtrm/skills/default/find-docs/SKILL.md +175 -0
  50. package/.xtrm/skills/default/find-skills/SKILL.md +133 -0
  51. package/.xtrm/skills/default/github-search/SKILL.md +49 -0
  52. package/.xtrm/skills/default/gitnexus-debugging/SKILL.md +89 -0
  53. package/.xtrm/skills/default/gitnexus-impact-analysis/SKILL.md +97 -0
  54. package/.xtrm/skills/default/gitnexus-pr-review/SKILL.md +163 -0
  55. package/.xtrm/skills/default/gitnexus-refactoring/SKILL.md +121 -0
  56. package/.xtrm/skills/default/hook-development/SKILL.md +797 -0
  57. package/.xtrm/skills/default/hook-development/examples/load-context.sh +55 -0
  58. package/.xtrm/skills/default/hook-development/examples/quality-check.js +1168 -0
  59. package/.xtrm/skills/default/hook-development/examples/validate-bash.sh +43 -0
  60. package/.xtrm/skills/default/hook-development/examples/validate-write.sh +38 -0
  61. package/.xtrm/skills/default/hook-development/references/advanced.md +527 -0
  62. package/.xtrm/skills/default/hook-development/references/migration.md +369 -0
  63. package/.xtrm/skills/default/hook-development/references/patterns.md +412 -0
  64. package/.xtrm/skills/default/hook-development/scripts/README.md +164 -0
  65. package/.xtrm/skills/default/hook-development/scripts/hook-linter.sh +153 -0
  66. package/.xtrm/skills/default/hook-development/scripts/test-hook.sh +252 -0
  67. package/.xtrm/skills/default/hook-development/scripts/validate-hook-schema.sh +159 -0
  68. package/.xtrm/skills/default/init-session/SKILL.md +69 -0
  69. package/.xtrm/skills/default/last30days/SKILL.md +881 -0
  70. package/.xtrm/skills/default/last30days/scripts/briefing.py +260 -0
  71. package/.xtrm/skills/default/last30days/scripts/evaluate-synthesis.py +120 -0
  72. package/.xtrm/skills/default/last30days/scripts/evaluate_search_quality.py +641 -0
  73. package/.xtrm/skills/default/last30days/scripts/generate-synthesis-inputs.py +53 -0
  74. package/.xtrm/skills/default/last30days/scripts/last30days.py +2137 -0
  75. package/.xtrm/skills/default/last30days/scripts/lib/__init__.py +1 -0
  76. package/.xtrm/skills/default/last30days/scripts/lib/bird_x.py +458 -0
  77. package/.xtrm/skills/default/last30days/scripts/lib/bluesky.py +225 -0
  78. package/.xtrm/skills/default/last30days/scripts/lib/brave_search.py +329 -0
  79. package/.xtrm/skills/default/last30days/scripts/lib/cache.py +165 -0
  80. package/.xtrm/skills/default/last30days/scripts/lib/chrome_cookies.py +265 -0
  81. package/.xtrm/skills/default/last30days/scripts/lib/cookie_extract.py +295 -0
  82. package/.xtrm/skills/default/last30days/scripts/lib/dates.py +124 -0
  83. package/.xtrm/skills/default/last30days/scripts/lib/dedupe.py +290 -0
  84. package/.xtrm/skills/default/last30days/scripts/lib/entity_extract.py +127 -0
  85. package/.xtrm/skills/default/last30days/scripts/lib/env.py +807 -0
  86. package/.xtrm/skills/default/last30days/scripts/lib/exa_search.py +176 -0
  87. package/.xtrm/skills/default/last30days/scripts/lib/hackernews.py +266 -0
  88. package/.xtrm/skills/default/last30days/scripts/lib/http.py +174 -0
  89. package/.xtrm/skills/default/last30days/scripts/lib/instagram.py +365 -0
  90. package/.xtrm/skills/default/last30days/scripts/lib/models.py +221 -0
  91. package/.xtrm/skills/default/last30days/scripts/lib/normalize.py +489 -0
  92. package/.xtrm/skills/default/last30days/scripts/lib/openai_reddit.py +631 -0
  93. package/.xtrm/skills/default/last30days/scripts/lib/openrouter_search.py +216 -0
  94. package/.xtrm/skills/default/last30days/scripts/lib/parallel_search.py +139 -0
  95. package/.xtrm/skills/default/last30days/scripts/lib/polymarket.py +580 -0
  96. package/.xtrm/skills/default/last30days/scripts/lib/quality_nudge.py +201 -0
  97. package/.xtrm/skills/default/last30days/scripts/lib/query.py +117 -0
  98. package/.xtrm/skills/default/last30days/scripts/lib/query_type.py +111 -0
  99. package/.xtrm/skills/default/last30days/scripts/lib/reddit.py +617 -0
  100. package/.xtrm/skills/default/last30days/scripts/lib/reddit_enrich.py +325 -0
  101. package/.xtrm/skills/default/last30days/scripts/lib/reddit_public.py +259 -0
  102. package/.xtrm/skills/default/last30days/scripts/lib/relevance.py +148 -0
  103. package/.xtrm/skills/default/last30days/scripts/lib/render.py +1018 -0
  104. package/.xtrm/skills/default/last30days/scripts/lib/safari_cookies.py +182 -0
  105. package/.xtrm/skills/default/last30days/scripts/lib/schema.py +843 -0
  106. package/.xtrm/skills/default/last30days/scripts/lib/score.py +775 -0
  107. package/.xtrm/skills/default/last30days/scripts/lib/scrapecreators_x.py +182 -0
  108. package/.xtrm/skills/default/last30days/scripts/lib/setup_wizard.py +186 -0
  109. package/.xtrm/skills/default/last30days/scripts/lib/tiktok.py +349 -0
  110. package/.xtrm/skills/default/last30days/scripts/lib/truthsocial.py +183 -0
  111. package/.xtrm/skills/default/last30days/scripts/lib/ui.py +620 -0
  112. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/LICENSE +21 -0
  113. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/bird-search.mjs +134 -0
  114. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/cookies.js +191 -0
  115. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/features.json +17 -0
  116. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/paginate-cursor.js +37 -0
  117. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/query-ids.json +20 -0
  118. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/runtime-features.js +151 -0
  119. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js +264 -0
  120. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-base.js +129 -0
  121. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js +50 -0
  122. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-features.js +347 -0
  123. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-search.js +157 -0
  124. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-types.js +2 -0
  125. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js +511 -0
  126. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/LICENSE +22 -0
  127. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/README.md +29 -0
  128. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.d.ts +3 -0
  129. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.d.ts.map +1 -0
  130. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.js +2 -0
  131. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.js.map +1 -0
  132. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.d.ts +8 -0
  133. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.d.ts.map +1 -0
  134. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.js +27 -0
  135. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.js.map +1 -0
  136. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts +11 -0
  137. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts.map +1 -0
  138. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.js +100 -0
  139. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.js.map +1 -0
  140. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts +25 -0
  141. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts.map +1 -0
  142. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js +104 -0
  143. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js.map +1 -0
  144. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts +10 -0
  145. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts.map +1 -0
  146. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.js +293 -0
  147. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.js.map +1 -0
  148. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts +10 -0
  149. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts.map +1 -0
  150. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js +26 -0
  151. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js.map +1 -0
  152. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts +7 -0
  153. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts.map +1 -0
  154. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.js +51 -0
  155. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.js.map +1 -0
  156. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.d.ts +7 -0
  157. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.d.ts.map +1 -0
  158. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.js +60 -0
  159. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.js.map +1 -0
  160. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts +7 -0
  161. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts.map +1 -0
  162. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.js +38 -0
  163. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.js.map +1 -0
  164. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts +5 -0
  165. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts.map +1 -0
  166. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.js +33 -0
  167. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.js.map +1 -0
  168. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts +24 -0
  169. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts.map +1 -0
  170. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.js +30 -0
  171. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.js.map +1 -0
  172. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.d.ts +11 -0
  173. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.d.ts.map +1 -0
  174. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.js +43 -0
  175. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.js.map +1 -0
  176. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts +8 -0
  177. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts.map +1 -0
  178. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.js +41 -0
  179. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.js.map +1 -0
  180. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts +8 -0
  181. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts.map +1 -0
  182. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.js +53 -0
  183. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.js.map +1 -0
  184. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.d.ts +8 -0
  185. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.d.ts.map +1 -0
  186. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.js +27 -0
  187. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.js.map +1 -0
  188. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts +7 -0
  189. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts.map +1 -0
  190. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.js +53 -0
  191. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.js.map +1 -0
  192. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.d.ts +8 -0
  193. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.d.ts.map +1 -0
  194. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.js +60 -0
  195. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.js.map +1 -0
  196. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts +7 -0
  197. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts.map +1 -0
  198. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.js +38 -0
  199. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.js.map +1 -0
  200. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.d.ts +6 -0
  201. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.d.ts.map +1 -0
  202. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.js +257 -0
  203. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.js.map +1 -0
  204. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.d.ts +8 -0
  205. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.d.ts.map +1 -0
  206. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.js +71 -0
  207. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.js.map +1 -0
  208. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.d.ts +6 -0
  209. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.d.ts.map +1 -0
  210. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.js +173 -0
  211. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.js.map +1 -0
  212. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.d.ts +26 -0
  213. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.d.ts.map +1 -0
  214. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.js +195 -0
  215. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.js.map +1 -0
  216. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.d.ts +121 -0
  217. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.d.ts.map +1 -0
  218. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.js +2 -0
  219. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.js.map +1 -0
  220. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.d.ts +2 -0
  221. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.d.ts.map +1 -0
  222. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.js +18 -0
  223. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.js.map +1 -0
  224. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.d.ts +8 -0
  225. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.d.ts.map +1 -0
  226. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.js +110 -0
  227. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.js.map +1 -0
  228. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.d.ts +2 -0
  229. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.d.ts.map +1 -0
  230. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.js +32 -0
  231. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.js.map +1 -0
  232. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.d.ts +2 -0
  233. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.d.ts.map +1 -0
  234. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.js +13 -0
  235. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.js.map +1 -0
  236. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.d.ts +2 -0
  237. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.d.ts.map +1 -0
  238. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.js +7 -0
  239. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.js.map +1 -0
  240. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.d.ts +5 -0
  241. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.d.ts.map +1 -0
  242. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.js +58 -0
  243. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.js.map +1 -0
  244. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.d.ts +2 -0
  245. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.d.ts.map +1 -0
  246. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.js +27 -0
  247. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.js.map +1 -0
  248. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.d.ts +2 -0
  249. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.d.ts.map +1 -0
  250. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.js +8 -0
  251. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.js.map +1 -0
  252. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/package.json +40 -0
  253. package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/package.json +13 -0
  254. package/.xtrm/skills/default/last30days/scripts/lib/websearch.py +401 -0
  255. package/.xtrm/skills/default/last30days/scripts/lib/xai_x.py +217 -0
  256. package/.xtrm/skills/default/last30days/scripts/lib/xiaohongshu_api.py +162 -0
  257. package/.xtrm/skills/default/last30days/scripts/lib/youtube_yt.py +538 -0
  258. package/.xtrm/skills/default/last30days/scripts/store.py +654 -0
  259. package/.xtrm/skills/default/last30days/scripts/sync.sh +50 -0
  260. package/.xtrm/skills/default/last30days/scripts/test-v1-vs-v2.sh +219 -0
  261. package/.xtrm/skills/default/last30days/scripts/watchlist.py +329 -0
  262. package/.xtrm/skills/default/planning/SKILL.md +405 -0
  263. package/.xtrm/skills/default/planning/evals/evals.json +19 -0
  264. package/.xtrm/skills/default/prompt-improving/README.md +162 -0
  265. package/.xtrm/skills/default/prompt-improving/SKILL.md +74 -0
  266. package/.xtrm/skills/default/prompt-improving/references/analysis_commands.md +24 -0
  267. package/.xtrm/skills/default/prompt-improving/references/chain_of_thought.md +24 -0
  268. package/.xtrm/skills/default/prompt-improving/references/mcp_definitions.md +20 -0
  269. package/.xtrm/skills/default/prompt-improving/references/multishot.md +23 -0
  270. package/.xtrm/skills/default/prompt-improving/references/xml_core.md +60 -0
  271. package/.xtrm/skills/default/quality-gates/.claude/hooks/hook-config.json +66 -0
  272. package/.xtrm/skills/default/quality-gates/.claude/hooks/quality-check.cjs +1286 -0
  273. package/.xtrm/skills/default/quality-gates/.claude/hooks/quality-check.py +334 -0
  274. package/.xtrm/skills/default/quality-gates/.claude/settings.json +3 -0
  275. package/.xtrm/skills/default/quality-gates/.claude/skills/using-quality-gates/SKILL.md +254 -0
  276. package/.xtrm/skills/default/quality-gates/README.md +109 -0
  277. package/.xtrm/skills/default/quality-gates/evals/evals.json +181 -0
  278. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/FINAL-EVAL-SUMMARY.md +75 -0
  279. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/edge-case-auto-fix-verification/with_skill/outputs/response.md +59 -0
  280. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/edge-case-mixed-language-project/with_skill/outputs/response.md +60 -0
  281. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/eval-summary.md +105 -0
  282. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/partial-install-python-only/with_skill/outputs/response.md +93 -0
  283. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/python-refactor-request/with_skill/outputs/response.md +104 -0
  284. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/quality-gate-error-fix/with_skill/outputs/response.md +74 -0
  285. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-general-chat/with_skill/outputs/response.md +18 -0
  286. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-math-question/with_skill/outputs/response.md +18 -0
  287. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-unrelated-coding/with_skill/outputs/response.md +56 -0
  288. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/tdd-guard-blocking-confusion/with_skill/outputs/response.md +67 -0
  289. package/.xtrm/skills/default/quality-gates/workspace/iteration-1/typescript-feature-with-tests/with_skill/outputs/response.md +97 -0
  290. package/.xtrm/skills/default/scoping-service-skills/SKILL.md +231 -0
  291. package/.xtrm/skills/default/scoping-service-skills/scripts/scope.py +74 -0
  292. package/.xtrm/skills/default/service-skills-set/README.md +93 -0
  293. package/.xtrm/skills/default/service-skills-set/git-hooks/doc_reminder.py +67 -0
  294. package/.xtrm/skills/default/service-skills-set/git-hooks/skill_staleness.py +194 -0
  295. package/.xtrm/skills/default/service-skills-set/install-service-skills.py +193 -0
  296. package/.xtrm/skills/default/service-skills-set/service-registry.json +4 -0
  297. package/.xtrm/skills/default/service-skills-set/service-skills-readme.md +236 -0
  298. package/.xtrm/skills/default/service-skills-set/settings.json +37 -0
  299. package/.xtrm/skills/default/session-close-report/SKILL.md +131 -0
  300. package/.xtrm/skills/default/skill-creator/LICENSE.txt +202 -0
  301. package/.xtrm/skills/default/skill-creator/SKILL.md +479 -0
  302. package/.xtrm/skills/default/skill-creator/agents/analyzer.md +274 -0
  303. package/.xtrm/skills/default/skill-creator/agents/comparator.md +202 -0
  304. package/.xtrm/skills/default/skill-creator/agents/grader.md +223 -0
  305. package/.xtrm/skills/default/skill-creator/assets/eval_review.html +146 -0
  306. package/.xtrm/skills/default/skill-creator/eval-viewer/generate_review.py +471 -0
  307. package/.xtrm/skills/default/skill-creator/eval-viewer/viewer.html +1325 -0
  308. package/.xtrm/skills/default/skill-creator/references/schemas.md +430 -0
  309. package/.xtrm/skills/default/skill-creator/scripts/__init__.py +0 -0
  310. package/.xtrm/skills/default/skill-creator/scripts/aggregate_benchmark.py +401 -0
  311. package/.xtrm/skills/default/skill-creator/scripts/generate_report.py +326 -0
  312. package/.xtrm/skills/default/skill-creator/scripts/improve_description.py +248 -0
  313. package/.xtrm/skills/default/skill-creator/scripts/package_skill.py +136 -0
  314. package/.xtrm/skills/default/skill-creator/scripts/quick_validate.py +103 -0
  315. package/.xtrm/skills/default/skill-creator/scripts/run_eval.py +310 -0
  316. package/.xtrm/skills/default/skill-creator/scripts/run_loop.py +332 -0
  317. package/.xtrm/skills/default/skill-creator/scripts/utils.py +47 -0
  318. package/.xtrm/skills/default/specialists-creator/SKILL.md +705 -0
  319. package/.xtrm/skills/default/specialists-creator/scripts/validate-specialist.ts +41 -0
  320. package/.xtrm/skills/default/sync-docs/SKILL.md +262 -0
  321. package/.xtrm/skills/default/sync-docs/evals/evals.json +89 -0
  322. package/.xtrm/skills/default/sync-docs/references/doc-structure.md +99 -0
  323. package/.xtrm/skills/default/sync-docs/references/schema.md +103 -0
  324. package/.xtrm/skills/default/sync-docs/scripts/changelog/add_entry.py +216 -0
  325. package/.xtrm/skills/default/sync-docs/scripts/context_gatherer.py +405 -0
  326. package/.xtrm/skills/default/sync-docs/scripts/doc_structure_analyzer.py +495 -0
  327. package/.xtrm/skills/default/sync-docs/scripts/drift_detector.py +563 -0
  328. package/.xtrm/skills/default/sync-docs/scripts/validate_doc.py +365 -0
  329. package/.xtrm/skills/default/sync-docs/scripts/validate_metadata.py +185 -0
  330. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/benchmark.json +293 -0
  331. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/benchmark.md +13 -0
  332. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/eval_metadata.json +27 -0
  333. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/outputs/result.md +210 -0
  334. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/grading.json +28 -0
  335. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/timing.json +1 -0
  336. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/outputs/result.md +101 -0
  337. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/grading.json +28 -0
  338. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/timing.json +5 -0
  339. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/timing.json +5 -0
  340. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/eval_metadata.json +27 -0
  341. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/outputs/result.md +198 -0
  342. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/grading.json +28 -0
  343. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/timing.json +1 -0
  344. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/outputs/result.md +94 -0
  345. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/grading.json +28 -0
  346. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/timing.json +1 -0
  347. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/eval_metadata.json +27 -0
  348. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/outputs/result.md +237 -0
  349. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/grading.json +28 -0
  350. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
  351. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/outputs/result.md +134 -0
  352. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/grading.json +28 -0
  353. package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/timing.json +1 -0
  354. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/benchmark.json +297 -0
  355. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/benchmark.md +13 -0
  356. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/eval_metadata.json +27 -0
  357. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/outputs/result.md +137 -0
  358. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/grading.json +92 -0
  359. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/timing.json +1 -0
  360. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/outputs/result.md +134 -0
  361. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/grading.json +86 -0
  362. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/timing.json +1 -0
  363. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/eval_metadata.json +27 -0
  364. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/outputs/result.md +193 -0
  365. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/grading.json +72 -0
  366. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/timing.json +1 -0
  367. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/outputs/result.md +211 -0
  368. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/grading.json +91 -0
  369. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/timing.json +5 -0
  370. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/eval_metadata.json +27 -0
  371. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/outputs/result.md +182 -0
  372. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
  373. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
  374. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/outputs/result.md +222 -0
  375. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/grading.json +88 -0
  376. package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
  377. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/benchmark.json +298 -0
  378. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/benchmark.md +13 -0
  379. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/eval_metadata.json +27 -0
  380. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/outputs/result.md +125 -0
  381. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/grading.json +97 -0
  382. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/timing.json +5 -0
  383. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/outputs/result.md +144 -0
  384. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/grading.json +78 -0
  385. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/timing.json +5 -0
  386. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/eval_metadata.json +27 -0
  387. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/outputs/result.md +104 -0
  388. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/grading.json +91 -0
  389. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/timing.json +5 -0
  390. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/outputs/result.md +79 -0
  391. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/grading.json +82 -0
  392. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/timing.json +5 -0
  393. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/eval_metadata.json +27 -0
  394. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase1_context.json +302 -0
  395. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase2_drift.txt +33 -0
  396. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase3_analysis.json +114 -0
  397. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase4_fix.txt +118 -0
  398. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase5_validate.txt +38 -0
  399. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/result.md +158 -0
  400. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
  401. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/timing.json +5 -0
  402. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/outputs/result.md +71 -0
  403. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/grading.json +90 -0
  404. package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
  405. package/.xtrm/skills/default/test-planning/SKILL.md +465 -0
  406. package/.xtrm/skills/default/test-planning/evals/evals.json +23 -0
  407. package/.xtrm/skills/default/updating-service-skills/SKILL.md +136 -0
  408. package/.xtrm/skills/default/updating-service-skills/scripts/drift_detector.py +222 -0
  409. package/.xtrm/skills/default/using-nodes/SKILL.md +333 -0
  410. package/.xtrm/skills/default/using-quality-gates/SKILL.md +254 -0
  411. package/.xtrm/skills/default/using-service-skills/SKILL.md +108 -0
  412. package/.xtrm/skills/default/using-service-skills/scripts/cataloger.py +74 -0
  413. package/.xtrm/skills/default/using-service-skills/scripts/skill_activator.py +152 -0
  414. package/.xtrm/skills/default/using-specialists/SKILL.md +848 -0
  415. package/.xtrm/skills/default/using-specialists/evals/evals.json +68 -0
  416. package/.xtrm/skills/default/using-tdd/SKILL.md +410 -0
  417. package/.xtrm/skills/default/using-xtrm/SKILL.md +127 -0
  418. package/.xtrm/skills/default/xt-debugging/SKILL.md +149 -0
  419. package/.xtrm/skills/default/xt-end/SKILL.md +297 -0
  420. package/.xtrm/skills/default/xt-merge/SKILL.md +326 -0
  421. package/.xtrm/skills/optional/README.txt +2 -0
  422. package/.xtrm/skills/optional/architecture-design/PACK.json +11 -0
  423. package/.xtrm/skills/optional/architecture-design/architecture-patterns/SKILL.md +494 -0
  424. package/.xtrm/skills/optional/architecture-design/architecture-patterns/references/advanced-patterns.md +391 -0
  425. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/SKILL.md +473 -0
  426. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/assets/few-shot-examples.json +106 -0
  427. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/assets/prompt-template-library.md +264 -0
  428. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/chain-of-thought.md +412 -0
  429. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/few-shot-learning.md +386 -0
  430. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/prompt-optimization.md +428 -0
  431. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/prompt-templates.md +484 -0
  432. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/system-prompts.md +195 -0
  433. package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/scripts/optimize-prompt.py +279 -0
  434. package/.xtrm/skills/optional/architecture-design/subagent-driven-development/SKILL.md +277 -0
  435. package/.xtrm/skills/optional/architecture-design/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
  436. package/.xtrm/skills/optional/architecture-design/subagent-driven-development/implementer-prompt.md +113 -0
  437. package/.xtrm/skills/optional/architecture-design/subagent-driven-development/spec-reviewer-prompt.md +61 -0
  438. package/.xtrm/skills/optional/code-quality/PACK.json +12 -0
  439. package/.xtrm/skills/optional/code-quality/code-review-excellence/SKILL.md +529 -0
  440. package/.xtrm/skills/optional/code-quality/multi-reviewer-patterns/SKILL.md +127 -0
  441. package/.xtrm/skills/optional/code-quality/systematic-debugging/SKILL.md +296 -0
  442. package/.xtrm/skills/optional/code-quality/verification-before-completion/SKILL.md +139 -0
  443. package/.xtrm/skills/optional/data-engineering/PACK.json +9 -0
  444. package/.xtrm/skills/optional/data-engineering/data-analyst/SKILL.md +57 -0
  445. package/.xtrm/skills/optional/research-methods/PACK.json +12 -0
  446. package/.xtrm/skills/optional/research-methods/academic-researcher/SKILL.md +269 -0
  447. package/.xtrm/skills/optional/research-methods/brainstorming/SKILL.md +164 -0
  448. package/.xtrm/skills/optional/research-methods/brainstorming/scripts/frame-template.html +214 -0
  449. package/.xtrm/skills/optional/research-methods/brainstorming/scripts/helper.js +88 -0
  450. package/.xtrm/skills/optional/research-methods/brainstorming/scripts/server.cjs +354 -0
  451. package/.xtrm/skills/optional/research-methods/brainstorming/scripts/start-server.sh +148 -0
  452. package/.xtrm/skills/optional/research-methods/brainstorming/scripts/stop-server.sh +56 -0
  453. package/.xtrm/skills/optional/research-methods/brainstorming/spec-document-reviewer-prompt.md +49 -0
  454. package/.xtrm/skills/optional/research-methods/brainstorming/visual-companion.md +287 -0
  455. package/.xtrm/skills/optional/research-methods/deep-research/SKILL.md +192 -0
  456. package/.xtrm/skills/optional/research-methods/fact-checker/SKILL.md +182 -0
  457. package/.xtrm/skills/optional/security-ops/PACK.json +9 -0
  458. package/.xtrm/skills/optional/security-ops/security-auditor/SKILL.md +165 -0
  459. package/.xtrm/skills/optional/xt-optional/PACK.json +16 -0
  460. package/.xtrm/skills/optional/xt-optional/docker-expert/SKILL.md +409 -0
  461. package/.xtrm/skills/optional/xt-optional/obsidian-cli/SKILL.md +106 -0
  462. package/.xtrm/skills/optional/xt-optional/python-testing/SKILL.md +815 -0
  463. package/.xtrm/skills/optional/xt-optional/senior-backend/SKILL.md +209 -0
  464. package/.xtrm/skills/optional/xt-optional/senior-backend/references/api_design_patterns.md +103 -0
  465. package/.xtrm/skills/optional/xt-optional/senior-backend/references/backend_security_practices.md +103 -0
  466. package/.xtrm/skills/optional/xt-optional/senior-backend/references/database_optimization_guide.md +103 -0
  467. package/.xtrm/skills/optional/xt-optional/senior-backend/scripts/api_load_tester.py +114 -0
  468. package/.xtrm/skills/optional/xt-optional/senior-backend/scripts/api_scaffolder.py +114 -0
  469. package/.xtrm/skills/optional/xt-optional/senior-backend/scripts/database_migration_tool.py +114 -0
  470. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/SKILL.md +226 -0
  471. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/references/experiment_design_frameworks.md +80 -0
  472. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/references/feature_engineering_patterns.md +80 -0
  473. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/references/statistical_methods_advanced.md +80 -0
  474. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/scripts/experiment_designer.py +100 -0
  475. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/scripts/feature_engineering_pipeline.py +100 -0
  476. package/.xtrm/skills/optional/xt-optional/senior-data-scientist/scripts/model_evaluation_suite.py +100 -0
  477. package/.xtrm/skills/optional/xt-optional/senior-devops/SKILL.md +209 -0
  478. package/.xtrm/skills/optional/xt-optional/senior-devops/references/cicd_pipeline_guide.md +103 -0
  479. package/.xtrm/skills/optional/xt-optional/senior-devops/references/deployment_strategies.md +103 -0
  480. package/.xtrm/skills/optional/xt-optional/senior-devops/references/infrastructure_as_code.md +103 -0
  481. package/.xtrm/skills/optional/xt-optional/senior-devops/scripts/deployment_manager.py +114 -0
  482. package/.xtrm/skills/optional/xt-optional/senior-devops/scripts/pipeline_generator.py +114 -0
  483. package/.xtrm/skills/optional/xt-optional/senior-devops/scripts/terraform_scaffolder.py +114 -0
  484. package/.xtrm/skills/optional/xt-optional/senior-security/SKILL.md +209 -0
  485. package/.xtrm/skills/optional/xt-optional/senior-security/references/cryptography_implementation.md +103 -0
  486. package/.xtrm/skills/optional/xt-optional/senior-security/references/penetration_testing_guide.md +103 -0
  487. package/.xtrm/skills/optional/xt-optional/senior-security/references/security_architecture_patterns.md +103 -0
  488. package/.xtrm/skills/optional/xt-optional/senior-security/scripts/pentest_automator.py +114 -0
  489. package/.xtrm/skills/optional/xt-optional/senior-security/scripts/security_auditor.py +114 -0
  490. package/.xtrm/skills/optional/xt-optional/senior-security/scripts/threat_modeler.py +114 -0
  491. package/CHANGELOG.md +16 -0
  492. package/README.md +5 -0
  493. package/cli/dist/index.cjs +862 -614
  494. package/cli/dist/index.cjs.map +1 -1
  495. package/cli/package.json +1 -1
  496. package/package.json +4 -1
  497. package/.xtrm/extensions/xtrm-ui/format.ts +0 -93
  498. package/.xtrm/extensions/xtrm-ui/package.json +0 -10
  499. /package/.xtrm/{extensions → ext-src}/auto-session-name/index.ts +0 -0
  500. /package/.xtrm/{extensions → ext-src}/auto-session-name/package.json +0 -0
  501. /package/.xtrm/{extensions → ext-src}/auto-update/index.ts +0 -0
  502. /package/.xtrm/{extensions → ext-src}/auto-update/package.json +0 -0
  503. /package/.xtrm/{extensions → ext-src}/beads/index.ts +0 -0
  504. /package/.xtrm/{extensions → ext-src}/beads/package.json +0 -0
  505. /package/.xtrm/{extensions → ext-src}/compact-header/index.ts +0 -0
  506. /package/.xtrm/{extensions → ext-src}/compact-header/package.json +0 -0
  507. /package/.xtrm/{extensions → ext-src}/core/adapter.ts +0 -0
  508. /package/.xtrm/{extensions → ext-src}/core/guard-rules.ts +0 -0
  509. /package/.xtrm/{extensions → ext-src}/core/lib.ts +0 -0
  510. /package/.xtrm/{extensions → ext-src}/core/logger.ts +0 -0
  511. /package/.xtrm/{extensions → ext-src}/core/package.json +0 -0
  512. /package/.xtrm/{extensions → ext-src}/core/runner.ts +0 -0
  513. /package/.xtrm/{extensions → ext-src}/core/session-state.ts +0 -0
  514. /package/.xtrm/{extensions → ext-src}/custom-footer/index.ts +0 -0
  515. /package/.xtrm/{extensions → ext-src}/custom-footer/package.json +0 -0
  516. /package/.xtrm/{extensions → ext-src}/custom-provider-qwen-cli/index.ts +0 -0
  517. /package/.xtrm/{extensions → ext-src}/custom-provider-qwen-cli/package.json +0 -0
  518. /package/.xtrm/{extensions → ext-src}/git-checkpoint/index.ts +0 -0
  519. /package/.xtrm/{extensions → ext-src}/git-checkpoint/package.json +0 -0
  520. /package/.xtrm/{extensions → ext-src}/lsp-bootstrap/index.ts +0 -0
  521. /package/.xtrm/{extensions → ext-src}/lsp-bootstrap/package.json +0 -0
  522. /package/.xtrm/{extensions → ext-src}/pi-serena-compact/index.ts +0 -0
  523. /package/.xtrm/{extensions → ext-src}/pi-serena-compact/package.json +0 -0
  524. /package/.xtrm/{extensions → ext-src}/quality-gates/index.ts +0 -0
  525. /package/.xtrm/{extensions → ext-src}/quality-gates/package.json +0 -0
  526. /package/.xtrm/{extensions → ext-src}/service-skills/index.ts +0 -0
  527. /package/.xtrm/{extensions → ext-src}/service-skills/package.json +0 -0
  528. /package/.xtrm/{extensions → ext-src}/session-flow/index.ts +0 -0
  529. /package/.xtrm/{extensions → ext-src}/session-flow/package.json +0 -0
  530. /package/.xtrm/{extensions → ext-src}/xtrm-loader/index.ts +0 -0
  531. /package/.xtrm/{extensions → ext-src}/xtrm-loader/package.json +0 -0
  532. /package/.xtrm/{extensions → ext-src}/xtrm-ui/themes/pidex-dark.json +0 -0
  533. /package/.xtrm/{extensions → ext-src}/xtrm-ui/themes/pidex-light.json +0 -0
@@ -0,0 +1,2137 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ last30days - Research a topic from the last 30 days on Reddit + X + YouTube + Web.
4
+
5
+ Usage:
6
+ python3 last30days.py <topic> [options]
7
+
8
+ Options:
9
+ --mock Use fixtures instead of real API calls
10
+ --emit=MODE Output mode: compact|json|md|context|path (default: compact)
11
+ --sources=MODE Source selection: auto|reddit|x|both (default: auto)
12
+ --quick Faster research with fewer sources (8-12 each)
13
+ --deep Comprehensive research with more sources (50-70 Reddit, 40-60 X)
14
+ --debug Enable verbose debug logging
15
+ --store Persist findings to SQLite database
16
+ --diagnose Show source availability diagnostics and exit
17
+ """
18
+
19
+ import argparse
20
+ import atexit
21
+ import json
22
+ import os
23
+ import signal
24
+ import sys
25
+ import threading
26
+ from concurrent.futures import ThreadPoolExecutor, as_completed
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+
30
+ # Add lib to path
31
+ SCRIPT_DIR = Path(__file__).parent.resolve()
32
+ sys.path.insert(0, str(SCRIPT_DIR))
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Global timeout & child process management
36
+ # ---------------------------------------------------------------------------
37
+ _child_pids: set = set()
38
+ _child_pids_lock = threading.Lock()
39
+
40
+ TIMEOUT_PROFILES = {
41
+ "quick": {"global": 90, "future": 30, "reddit_future": 60, "youtube_future": 60, "tiktok_future": 90, "instagram_future": 90, "hackernews_future": 30, "bluesky_future": 30, "truthsocial_future": 30, "polymarket_future": 15, "http": 15, "enrich_per": 8, "enrich_total": 30, "enrich_max_items": 10},
42
+ "default": {"global": 180, "future": 60, "reddit_future": 90, "youtube_future": 90, "tiktok_future": 120, "instagram_future": 120, "hackernews_future": 60, "bluesky_future": 60, "truthsocial_future": 60, "polymarket_future": 30, "http": 30, "enrich_per": 15, "enrich_total": 45, "enrich_max_items": 15},
43
+ "deep": {"global": 300, "future": 90, "reddit_future": 120, "youtube_future": 120, "tiktok_future": 150, "instagram_future": 150, "hackernews_future": 90, "bluesky_future": 90, "truthsocial_future": 90, "polymarket_future": 45, "http": 30, "enrich_per": 15, "enrich_total": 60, "enrich_max_items": 25},
44
+ }
45
+
46
+ # Valid source names for the --search flag
47
+ VALID_SEARCH_SOURCES = {
48
+ "reddit", "x", "hn", "bluesky", "bsky", "truthsocial", "truth", "youtube", "tiktok", "instagram",
49
+ "polymarket", "web", "xiaohongshu", "xhs",
50
+ }
51
+
52
+
53
+ def parse_search_flag(search_str: str) -> set:
54
+ """Parse and validate the --search flag value.
55
+
56
+ Args:
57
+ search_str: Comma-separated source names (e.g. "reddit,hn")
58
+
59
+ Returns:
60
+ Set of validated source names
61
+
62
+ Raises:
63
+ SystemExit: If invalid sources are specified
64
+ """
65
+ sources = set()
66
+ for s in search_str.split(","):
67
+ s = s.strip().lower()
68
+ if not s:
69
+ continue
70
+ if s == "xhs":
71
+ s = "xiaohongshu"
72
+ if s not in VALID_SEARCH_SOURCES:
73
+ print(
74
+ f"Error: Unknown search source '{s}'. "
75
+ f"Valid: {', '.join(sorted(VALID_SEARCH_SOURCES))}",
76
+ file=sys.stderr,
77
+ )
78
+ sys.exit(1)
79
+ sources.add(s)
80
+ if not sources:
81
+ print("Error: --search requires at least one source.", file=sys.stderr)
82
+ sys.exit(1)
83
+ return sources
84
+
85
+
86
+ def register_child_pid(pid: int):
87
+ """Track a child process for cleanup."""
88
+ with _child_pids_lock:
89
+ _child_pids.add(pid)
90
+
91
+
92
+ def unregister_child_pid(pid: int):
93
+ """Remove a child process from tracking."""
94
+ with _child_pids_lock:
95
+ _child_pids.discard(pid)
96
+
97
+
98
+ def _cleanup_children():
99
+ """Kill all tracked child processes."""
100
+ with _child_pids_lock:
101
+ pids = list(_child_pids)
102
+ for pid in pids:
103
+ try:
104
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
105
+ except (ProcessLookupError, PermissionError, OSError):
106
+ pass
107
+
108
+
109
+ atexit.register(_cleanup_children)
110
+
111
+
112
+ def _install_global_timeout(timeout_seconds: int):
113
+ """Install a global timeout watchdog.
114
+
115
+ Uses SIGALRM on Unix, threading.Timer as fallback.
116
+ """
117
+ if hasattr(signal, 'SIGALRM'):
118
+ def _handler(signum, frame):
119
+ sys.stderr.write(f"\n[TIMEOUT] Global timeout ({timeout_seconds}s) exceeded. Cleaning up.\n")
120
+ sys.stderr.flush()
121
+ _cleanup_children()
122
+ sys.exit(1)
123
+ signal.signal(signal.SIGALRM, _handler)
124
+ signal.alarm(timeout_seconds)
125
+ else:
126
+ # Windows fallback
127
+ def _watchdog():
128
+ sys.stderr.write(f"\n[TIMEOUT] Global timeout ({timeout_seconds}s) exceeded. Cleaning up.\n")
129
+ sys.stderr.flush()
130
+ _cleanup_children()
131
+ os._exit(1)
132
+ timer = threading.Timer(timeout_seconds, _watchdog)
133
+ timer.daemon = True
134
+ timer.start()
135
+
136
+ from lib import (
137
+ bird_x,
138
+ bluesky,
139
+ truthsocial,
140
+ dates,
141
+ dedupe,
142
+ hackernews,
143
+ xiaohongshu_api,
144
+ polymarket,
145
+ entity_extract,
146
+ env,
147
+ http,
148
+ models,
149
+ normalize,
150
+ openai_reddit,
151
+ reddit,
152
+ reddit_public,
153
+ reddit_enrich,
154
+ render,
155
+ schema,
156
+ score,
157
+ scrapecreators_x,
158
+ setup_wizard,
159
+ ui,
160
+ tiktok,
161
+ instagram,
162
+ websearch,
163
+ xai_x,
164
+ youtube_yt,
165
+ quality_nudge,
166
+ query_type as qt,
167
+ )
168
+
169
+
170
+ def load_fixture(name: str) -> dict:
171
+ """Load a fixture file."""
172
+ fixture_path = SCRIPT_DIR.parent / "fixtures" / name
173
+ if fixture_path.exists():
174
+ with open(fixture_path) as f:
175
+ return json.load(f)
176
+ return {}
177
+
178
+
179
+ def _search_reddit(
180
+ topic: str,
181
+ config: dict,
182
+ selected_models: dict,
183
+ from_date: str,
184
+ to_date: str,
185
+ depth: str,
186
+ mock: bool,
187
+ ) -> tuple:
188
+ """Search Reddit (runs in thread).
189
+
190
+ Hierarchy:
191
+ 1. ScrapeCreators (if SCRAPECREATORS_API_KEY exists) — premium, best quality
192
+ 2. Public Reddit JSON (always available) — free, good for thread discovery
193
+ 3. OpenAI Responses API — legacy fallback for backwards compatibility
194
+
195
+ Returns:
196
+ Tuple of (reddit_items, raw_response, error, used_scrapecreators)
197
+ """
198
+ raw_response = None
199
+ reddit_error = None
200
+ used_scrapecreators = False
201
+
202
+ sc_token = config.get("SCRAPECREATORS_API_KEY")
203
+
204
+ if mock:
205
+ raw_response = load_fixture("openai_sample.json")
206
+ elif sc_token:
207
+ # === Tier 1: ScrapeCreators path (preferred) ===
208
+ used_scrapecreators = True
209
+ try:
210
+ sys.stderr.write("[Reddit] Using ScrapeCreators API\n")
211
+ sys.stderr.flush()
212
+ result = reddit.search_and_enrich(
213
+ topic, from_date, to_date,
214
+ depth=depth, token=sc_token,
215
+ )
216
+ reddit_items = result.get("items", [])
217
+ if result.get("error"):
218
+ reddit_error = result["error"]
219
+ return reddit_items, result, reddit_error, used_scrapecreators
220
+ except Exception as e:
221
+ reddit_error = f"ScrapeCreators: {type(e).__name__}: {e}"
222
+ sys.stderr.write(f"[Reddit] ScrapeCreators failed: {e}\n")
223
+ sys.stderr.flush()
224
+ used_scrapecreators = False
225
+ # Fall through to Tier 2 (public JSON)
226
+
227
+ # === Tier 2: Public Reddit JSON (free, always available) ===
228
+ if not mock:
229
+ try:
230
+ sys.stderr.write("[Reddit] Trying public Reddit JSON\n")
231
+ sys.stderr.flush()
232
+ reddit_items = reddit_public.search_reddit_public(
233
+ topic, from_date, to_date, depth=depth,
234
+ )
235
+ if reddit_items:
236
+ raw_response = {"source": "reddit_public", "items": reddit_items}
237
+ sys.stderr.write(f"[Reddit] Public JSON returned {len(reddit_items)} results\n")
238
+ sys.stderr.flush()
239
+ return reddit_items, raw_response, None, False
240
+ # Empty results — fall through to Tier 3
241
+ sys.stderr.write("[Reddit] Public JSON returned 0 results, trying OpenAI\n")
242
+ sys.stderr.flush()
243
+ except Exception as e:
244
+ sys.stderr.write(f"[Reddit] Public JSON failed: {e}\n")
245
+ sys.stderr.flush()
246
+ # Fall through to Tier 3
247
+
248
+ # === Tier 3: OpenAI Responses API (legacy fallback) ===
249
+ if not mock and config.get("OPENAI_API_KEY"):
250
+ try:
251
+ sys.stderr.write("[Reddit] Falling back to OpenAI Responses API\n")
252
+ sys.stderr.flush()
253
+ raw_response = openai_reddit.search_reddit(
254
+ config["OPENAI_API_KEY"],
255
+ selected_models["openai"],
256
+ topic,
257
+ from_date,
258
+ to_date,
259
+ depth=depth,
260
+ auth_source=config.get("OPENAI_AUTH_SOURCE", "api_key"),
261
+ account_id=config.get("OPENAI_CHATGPT_ACCOUNT_ID"),
262
+ )
263
+ except http.HTTPError as e:
264
+ raw_response = {"error": str(e)}
265
+ reddit_error = f"API error: {e}"
266
+ except Exception as e:
267
+ raw_response = {"error": str(e)}
268
+ reddit_error = f"{type(e).__name__}: {e}"
269
+
270
+ # Parse response (OpenAI path)
271
+ reddit_items = openai_reddit.parse_reddit_response(raw_response or {})
272
+
273
+ # Quick retry with simpler query if few results (OpenAI path only)
274
+ if len(reddit_items) < 5 and not mock and not reddit_error and config.get("OPENAI_API_KEY"):
275
+ core = openai_reddit._extract_core_subject(topic)
276
+ if core.lower() != topic.lower():
277
+ try:
278
+ retry_raw = openai_reddit.search_reddit(
279
+ config["OPENAI_API_KEY"],
280
+ selected_models["openai"],
281
+ core,
282
+ from_date, to_date,
283
+ depth=depth,
284
+ auth_source=config.get("OPENAI_AUTH_SOURCE", "api_key"),
285
+ account_id=config.get("OPENAI_CHATGPT_ACCOUNT_ID"),
286
+ )
287
+ retry_items = openai_reddit.parse_reddit_response(retry_raw)
288
+ existing_urls = {item.get("url") for item in reddit_items}
289
+ for item in retry_items:
290
+ if item.get("url") not in existing_urls:
291
+ reddit_items.append(item)
292
+ except Exception:
293
+ pass
294
+
295
+ # Subreddit-targeted fallback if still < 3 results (OpenAI path only)
296
+ if len(reddit_items) < 3 and not mock and not reddit_error and config.get("OPENAI_API_KEY"):
297
+ sub_query = openai_reddit._build_subreddit_query(topic)
298
+ try:
299
+ sub_raw = openai_reddit.search_reddit(
300
+ config["OPENAI_API_KEY"],
301
+ selected_models["openai"],
302
+ sub_query,
303
+ from_date, to_date,
304
+ depth=depth,
305
+ )
306
+ sub_items = openai_reddit.parse_reddit_response(sub_raw)
307
+ existing_urls = {item.get("url") for item in reddit_items}
308
+ for item in sub_items:
309
+ if item.get("url") not in existing_urls:
310
+ reddit_items.append(item)
311
+ except Exception:
312
+ pass
313
+
314
+ return reddit_items, raw_response, reddit_error, used_scrapecreators
315
+
316
+
317
+ def _search_x(
318
+ topic: str,
319
+ config: dict,
320
+ selected_models: dict,
321
+ from_date: str,
322
+ to_date: str,
323
+ depth: str,
324
+ mock: bool,
325
+ x_source: str = "xai",
326
+ ) -> tuple:
327
+ """Search X via Bird CLI or xAI (runs in thread).
328
+
329
+ Args:
330
+ x_source: 'bird' or 'xai' - which backend to use
331
+
332
+ Returns:
333
+ Tuple of (x_items, raw_response, error)
334
+ """
335
+ raw_response = None
336
+ x_error = None
337
+
338
+ if mock:
339
+ raw_response = load_fixture("xai_sample.json")
340
+ x_items = xai_x.parse_x_response(raw_response or {})
341
+ return x_items, raw_response, x_error
342
+
343
+ # Use Bird if specified
344
+ if x_source == "bird":
345
+ try:
346
+ raw_response = bird_x.search_x(
347
+ topic,
348
+ from_date,
349
+ to_date,
350
+ depth=depth,
351
+ )
352
+ except Exception as e:
353
+ raw_response = {"error": str(e)}
354
+ x_error = f"{type(e).__name__}: {e}"
355
+
356
+ x_items = bird_x.parse_bird_response(raw_response or {}, query=topic)
357
+
358
+ # Check for error in response (Bird returns list on success, dict on error)
359
+ if raw_response and isinstance(raw_response, dict) and raw_response.get("error") and not x_error:
360
+ x_error = raw_response["error"]
361
+
362
+ return x_items, raw_response, x_error
363
+
364
+ # Use ScrapeCreators if specified
365
+ if x_source == "scrapecreators":
366
+ try:
367
+ raw_response = scrapecreators_x.search_x(
368
+ topic, from_date, to_date,
369
+ depth=depth,
370
+ token=config.get("SCRAPECREATORS_API_KEY"),
371
+ )
372
+ except Exception as e:
373
+ raw_response = {"error": str(e)}
374
+ x_error = f"{type(e).__name__}: {e}"
375
+
376
+ x_items = scrapecreators_x.parse_x_response(raw_response or {})
377
+
378
+ if raw_response and isinstance(raw_response, dict) and raw_response.get("error") and not x_error:
379
+ x_error = raw_response["error"]
380
+
381
+ return x_items, raw_response, x_error
382
+
383
+ # Use xAI (original behavior)
384
+ try:
385
+ raw_response = xai_x.search_x(
386
+ config["XAI_API_KEY"],
387
+ selected_models["xai"],
388
+ topic,
389
+ from_date,
390
+ to_date,
391
+ depth=depth,
392
+ )
393
+ except http.HTTPError as e:
394
+ raw_response = {"error": str(e)}
395
+ x_error = f"API error: {e}"
396
+ except Exception as e:
397
+ raw_response = {"error": str(e)}
398
+ x_error = f"{type(e).__name__}: {e}"
399
+
400
+ x_items = xai_x.parse_x_response(raw_response or {})
401
+
402
+ return x_items, raw_response, x_error
403
+
404
+
405
+ def _search_youtube(
406
+ topic: str,
407
+ from_date: str,
408
+ to_date: str,
409
+ depth: str,
410
+ ) -> tuple:
411
+ """Search YouTube via yt-dlp (runs in thread).
412
+
413
+ Returns:
414
+ Tuple of (youtube_items, youtube_error)
415
+ """
416
+ youtube_error = None
417
+
418
+ try:
419
+ response = youtube_yt.search_and_transcribe(
420
+ topic, from_date, to_date, depth=depth,
421
+ )
422
+ except Exception as e:
423
+ return [], f"{type(e).__name__}: {e}"
424
+
425
+ youtube_items = youtube_yt.parse_youtube_response(response)
426
+
427
+ if response.get("error"):
428
+ youtube_error = response["error"]
429
+
430
+ return youtube_items, youtube_error
431
+
432
+
433
+ def _search_tiktok(
434
+ topic: str,
435
+ from_date: str,
436
+ to_date: str,
437
+ depth: str,
438
+ token: str,
439
+ ) -> tuple:
440
+ """Search TikTok via ScrapeCreators (runs in thread).
441
+
442
+ Returns:
443
+ Tuple of (tiktok_items, tiktok_error)
444
+ """
445
+ tiktok_error = None
446
+
447
+ try:
448
+ response = tiktok.search_and_enrich(
449
+ topic, from_date, to_date, depth=depth, token=token,
450
+ )
451
+ except Exception as e:
452
+ return [], f"{type(e).__name__}: {e}"
453
+
454
+ tiktok_items = tiktok.parse_tiktok_response(response)
455
+
456
+ if response.get("error"):
457
+ tiktok_error = response["error"]
458
+
459
+ return tiktok_items, tiktok_error
460
+
461
+
462
+ def _search_instagram(
463
+ topic: str,
464
+ from_date: str,
465
+ to_date: str,
466
+ depth: str,
467
+ token: str,
468
+ ) -> tuple:
469
+ """Search Instagram via ScrapeCreators (runs in thread).
470
+
471
+ Returns:
472
+ Tuple of (instagram_items, instagram_error)
473
+ """
474
+ instagram_error = None
475
+
476
+ try:
477
+ response = instagram.search_and_enrich(
478
+ topic, from_date, to_date, depth=depth, token=token,
479
+ )
480
+ except Exception as e:
481
+ return [], f"{type(e).__name__}: {e}"
482
+
483
+ instagram_items = instagram.parse_instagram_response(response)
484
+
485
+ if response.get("error"):
486
+ instagram_error = response["error"]
487
+
488
+ return instagram_items, instagram_error
489
+
490
+
491
+ def _search_hackernews(
492
+ topic: str,
493
+ from_date: str,
494
+ to_date: str,
495
+ depth: str,
496
+ ) -> tuple:
497
+ """Search Hacker News via Algolia (runs in thread).
498
+
499
+ Returns:
500
+ Tuple of (hn_items, hn_error)
501
+ """
502
+ hn_error = None
503
+
504
+ try:
505
+ response = hackernews.search_hackernews(
506
+ topic, from_date, to_date, depth=depth,
507
+ )
508
+ except Exception as e:
509
+ return [], f"{type(e).__name__}: {e}"
510
+
511
+ hn_items = hackernews.parse_hackernews_response(response, query=topic)
512
+
513
+ if response.get("error"):
514
+ hn_error = response["error"]
515
+
516
+ return hn_items, hn_error
517
+
518
+
519
+ def _search_bluesky(
520
+ topic: str,
521
+ from_date: str,
522
+ to_date: str,
523
+ depth: str,
524
+ config: dict = None,
525
+ ) -> tuple:
526
+ """Search Bluesky via AT Protocol (runs in thread).
527
+
528
+ Returns:
529
+ Tuple of (bsky_items, bsky_error)
530
+ """
531
+ bsky_error = None
532
+
533
+ try:
534
+ response = bluesky.search_bluesky(
535
+ topic, from_date, to_date, depth=depth, config=config,
536
+ )
537
+ except Exception as e:
538
+ return [], f"{type(e).__name__}: {e}"
539
+
540
+ bsky_items = bluesky.parse_bluesky_response(response)
541
+
542
+ if response.get("error"):
543
+ bsky_error = response["error"]
544
+
545
+ return bsky_items, bsky_error
546
+
547
+
548
+ def _search_truthsocial(
549
+ topic: str,
550
+ from_date: str,
551
+ to_date: str,
552
+ depth: str,
553
+ config: dict = None,
554
+ ) -> tuple:
555
+ """Search Truth Social via Mastodon API (runs in thread).
556
+
557
+ Returns:
558
+ Tuple of (ts_items, ts_error)
559
+ """
560
+ ts_error = None
561
+
562
+ try:
563
+ response = truthsocial.search_truthsocial(
564
+ topic, from_date, to_date, depth=depth, config=config,
565
+ )
566
+ except Exception as e:
567
+ return [], f"{type(e).__name__}: {e}"
568
+
569
+ ts_items = truthsocial.parse_truthsocial_response(response)
570
+
571
+ if response.get("error"):
572
+ ts_error = response["error"]
573
+
574
+ return ts_items, ts_error
575
+
576
+
577
+ def _search_polymarket(
578
+ topic: str,
579
+ from_date: str,
580
+ to_date: str,
581
+ depth: str,
582
+ ) -> tuple:
583
+ """Search Polymarket via Gamma API (runs in thread).
584
+
585
+ Returns:
586
+ Tuple of (pm_items, pm_error)
587
+ """
588
+ pm_error = None
589
+
590
+ try:
591
+ response = polymarket.search_polymarket(
592
+ topic, from_date, to_date, depth=depth,
593
+ )
594
+ except Exception as e:
595
+ return [], f"{type(e).__name__}: {e}"
596
+
597
+ pm_items = polymarket.parse_polymarket_response(response, topic=topic)
598
+
599
+ if response.get("error"):
600
+ pm_error = response["error"]
601
+
602
+ return pm_items, pm_error
603
+
604
+
605
+ def _search_web(
606
+ topic: str,
607
+ config: dict,
608
+ from_date: str,
609
+ to_date: str,
610
+ depth: str,
611
+ ) -> tuple:
612
+ """Search the web via native API backend (runs in thread).
613
+
614
+ Uses the best available backend: Parallel AI > Brave > OpenRouter.
615
+
616
+ Returns:
617
+ Tuple of (web_items, web_error)
618
+ web_items are raw dicts ready for websearch.normalize_websearch_items()
619
+ """
620
+ from lib import brave_search, parallel_search, openrouter_search, exa_search
621
+
622
+ backend = env.get_web_search_source(config)
623
+ if not backend:
624
+ return [], "No web search API keys configured"
625
+
626
+ web_error = None
627
+ raw_results = []
628
+
629
+ try:
630
+ if backend == "exa":
631
+ raw_results = exa_search.search_web(
632
+ topic, from_date, to_date, config["EXA_API_KEY"], depth=depth,
633
+ )
634
+ elif backend == "parallel":
635
+ raw_results = parallel_search.search_web(
636
+ topic, from_date, to_date, config["PARALLEL_API_KEY"], depth=depth,
637
+ )
638
+ elif backend == "brave":
639
+ use_llm_ctx = os.environ.get("BRAVE_LLM_CONTEXT", "").strip() == "1"
640
+ raw_results = brave_search.search_web(
641
+ topic, from_date, to_date, config["BRAVE_API_KEY"],
642
+ depth=depth, use_llm_context=use_llm_ctx,
643
+ )
644
+ elif backend == "openrouter":
645
+ raw_results = openrouter_search.search_web(
646
+ topic, from_date, to_date, config["OPENROUTER_API_KEY"], depth=depth,
647
+ )
648
+ except Exception as e:
649
+ return [], f"{type(e).__name__}: {e}"
650
+
651
+ # Add IDs and date_confidence for websearch.normalize_websearch_items()
652
+ for i, item in enumerate(raw_results):
653
+ item.setdefault("id", f"W{i+1}")
654
+ if item.get("date") and not item.get("date_confidence"):
655
+ item["date_confidence"] = "med"
656
+ elif not item.get("date"):
657
+ item["date_confidence"] = "low"
658
+ item.setdefault("why_relevant", "")
659
+
660
+ return raw_results, web_error
661
+
662
+
663
+ def _search_xiaohongshu(
664
+ topic: str,
665
+ config: dict,
666
+ from_date: str,
667
+ to_date: str,
668
+ depth: str,
669
+ ) -> tuple:
670
+ """Search Xiaohongshu via xiaohongshu-mcp HTTP API (runs in thread).
671
+
672
+ Returns:
673
+ Tuple of (xiaohongshu_items, xiaohongshu_error)
674
+ Items are in web-item dict shape and can be normalized with websearch module.
675
+ """
676
+ base_url = env.get_xiaohongshu_api_base(config)
677
+ try:
678
+ items = xiaohongshu_api.search_feeds(
679
+ topic=topic,
680
+ from_date=from_date,
681
+ to_date=to_date,
682
+ base_url=base_url,
683
+ depth=depth,
684
+ )
685
+ except Exception as e:
686
+ return [], f"{type(e).__name__}: {e}"
687
+
688
+ # Ensure all required keys exist for normalize_websearch_items()
689
+ for i, item in enumerate(items):
690
+ item.setdefault("id", f"XHS{i+1}")
691
+ item.setdefault("title", "")
692
+ item.setdefault("url", "")
693
+ item.setdefault("source_domain", "xiaohongshu.com")
694
+ item.setdefault("snippet", "")
695
+ if item.get("date") and not item.get("date_confidence"):
696
+ item["date_confidence"] = "med"
697
+ elif not item.get("date"):
698
+ item["date_confidence"] = "low"
699
+ item.setdefault("relevance", 0.5)
700
+ item.setdefault("why_relevant", "")
701
+
702
+ return items, None
703
+
704
+
705
+ def _run_supplemental(
706
+ topic: str,
707
+ reddit_items: list,
708
+ x_items: list,
709
+ from_date: str,
710
+ to_date: str,
711
+ depth: str,
712
+ x_source: str,
713
+ progress: ui.ProgressDisplay = None,
714
+ skip_reddit: bool = False,
715
+ resolved_handle: str = None,
716
+ ) -> tuple:
717
+ """Run Phase 2 supplemental searches based on entities from Phase 1.
718
+
719
+ Extracts handles/subreddits from initial results, then runs targeted
720
+ searches to find additional content the broad search missed.
721
+
722
+ Args:
723
+ topic: Original search topic
724
+ reddit_items: Phase 1 Reddit items (raw dicts)
725
+ x_items: Phase 1 X items (raw dicts)
726
+ from_date: Start date
727
+ to_date: End date
728
+ depth: Research depth
729
+ x_source: 'bird' or 'xai'
730
+ progress: Optional progress display
731
+ skip_reddit: If True, skip Reddit supplemental (e.g. rate-limited)
732
+ resolved_handle: X handle resolved by the agent (without @), searched unfiltered
733
+
734
+ Returns:
735
+ Tuple of (supplemental_reddit, supplemental_x)
736
+ """
737
+ # Depth-dependent caps
738
+ if depth == "default":
739
+ max_handles = 3
740
+ max_subs = 3
741
+ count_per = 3
742
+ else: # deep
743
+ max_handles = 5
744
+ max_subs = 5
745
+ count_per = 5
746
+
747
+ # Extract entities from Phase 1 results
748
+ entities = entity_extract.extract_entities(
749
+ reddit_items, x_items,
750
+ max_handles=max_handles,
751
+ max_subreddits=max_subs,
752
+ )
753
+
754
+ has_handles = entities["x_handles"] and x_source == "bird"
755
+ has_subs = entities["reddit_subreddits"] and not skip_reddit
756
+
757
+ # Always run unfiltered search for resolved handle (even if entity-extracted).
758
+ # Entity-extracted handles get topic-filtered queries (from:handle topic),
759
+ # but resolved handles need UNFILTERED search (from:handle) to find posts
760
+ # that don't mention the topic string (e.g. Dor Brothers' viral tweet about
761
+ # Logan Paul doesn't contain "dor brothers" in the text).
762
+ has_resolved = bool(resolved_handle) and x_source == "bird"
763
+
764
+ if not has_handles and not has_subs and not has_resolved:
765
+ return [], []
766
+
767
+ parts = []
768
+ if has_resolved:
769
+ parts.append(f"@{resolved_handle} (resolved)")
770
+ if has_handles:
771
+ parts.append(f"@{', @'.join(entities['x_handles'][:3])}")
772
+ if has_subs:
773
+ parts.append(f"r/{', r/'.join(entities['reddit_subreddits'][:3])}")
774
+ sys.stderr.write(f"[Phase 2] Drilling into {' + '.join(parts)}\n")
775
+ sys.stderr.flush()
776
+
777
+ supplemental_reddit = []
778
+ supplemental_x = []
779
+
780
+ # Collect existing URLs to avoid adding duplicates before dedupe
781
+ existing_urls = set()
782
+ for item in reddit_items:
783
+ existing_urls.add(item.get("url", ""))
784
+ for item in x_items:
785
+ existing_urls.add(item.get("url", ""))
786
+
787
+ # Run supplemental searches in parallel
788
+ reddit_future = None
789
+ x_future = None
790
+ resolved_future = None
791
+
792
+ max_workers = sum([bool(has_subs), bool(has_handles), bool(has_resolved)])
793
+ with ThreadPoolExecutor(max_workers=max(max_workers, 1)) as executor:
794
+ if has_subs:
795
+ reddit_future = executor.submit(
796
+ openai_reddit.search_subreddits,
797
+ entities["reddit_subreddits"],
798
+ topic,
799
+ from_date,
800
+ to_date,
801
+ count_per,
802
+ )
803
+
804
+ if has_handles:
805
+ x_future = executor.submit(
806
+ bird_x.search_handles,
807
+ entities["x_handles"],
808
+ topic,
809
+ from_date,
810
+ count_per,
811
+ )
812
+
813
+ if has_resolved:
814
+ # Resolved handle: search unfiltered (topic=None) to get all recent posts
815
+ resolved_future = executor.submit(
816
+ bird_x.search_handles,
817
+ [resolved_handle],
818
+ None, # No topic filter - get all recent activity
819
+ from_date,
820
+ 10, # More results for the topic entity
821
+ )
822
+
823
+ if reddit_future:
824
+ try:
825
+ raw_reddit = reddit_future.result(timeout=30)
826
+ # Filter out URLs already found in Phase 1
827
+ supplemental_reddit = [
828
+ item for item in raw_reddit
829
+ if item.get("url", "") not in existing_urls
830
+ ]
831
+ except TimeoutError:
832
+ sys.stderr.write("[Phase 2] Supplemental Reddit timed out (30s)\n")
833
+ except Exception as e:
834
+ sys.stderr.write(f"[Phase 2] Supplemental Reddit error: {e}\n")
835
+
836
+ if x_future:
837
+ try:
838
+ raw_x = x_future.result(timeout=30)
839
+ supplemental_x = [
840
+ item for item in raw_x
841
+ if item.get("url", "") not in existing_urls
842
+ ]
843
+ except TimeoutError:
844
+ sys.stderr.write("[Phase 2] Supplemental X timed out (30s)\n")
845
+ except Exception as e:
846
+ sys.stderr.write(f"[Phase 2] Supplemental X error: {e}\n")
847
+
848
+ if resolved_future:
849
+ try:
850
+ raw_resolved = resolved_future.result(timeout=30)
851
+ # Lower relevance for unfiltered handle posts (no topic keyword signal)
852
+ for item in raw_resolved:
853
+ item["relevance"] = 0.5
854
+ resolved_new = [
855
+ item for item in raw_resolved
856
+ if item.get("url", "") not in existing_urls
857
+ ]
858
+ supplemental_x.extend(resolved_new)
859
+ if resolved_new:
860
+ sys.stderr.write(f"[Phase 2] +{len(resolved_new)} from @{resolved_handle}\n")
861
+ except TimeoutError:
862
+ sys.stderr.write(f"[Phase 2] Resolved handle @{resolved_handle} timed out (30s)\n")
863
+ except Exception as e:
864
+ sys.stderr.write(f"[Phase 2] Resolved handle error: {e}\n")
865
+
866
+ if supplemental_reddit or supplemental_x:
867
+ sys.stderr.write(
868
+ f"[Phase 2] +{len(supplemental_reddit)} Reddit, +{len(supplemental_x)} X\n"
869
+ )
870
+ sys.stderr.flush()
871
+
872
+ return supplemental_reddit, supplemental_x
873
+
874
+
875
+ def run_research(
876
+ topic: str,
877
+ sources: str,
878
+ config: dict,
879
+ selected_models: dict,
880
+ from_date: str,
881
+ to_date: str,
882
+ depth: str = "default",
883
+ mock: bool = False,
884
+ progress: ui.ProgressDisplay = None,
885
+ x_source: str = "xai",
886
+ run_youtube: bool = False,
887
+ run_tiktok: bool = False,
888
+ run_instagram: bool = False,
889
+ run_xiaohongshu: bool = False,
890
+ timeouts: dict = None,
891
+ resolved_handle: str = None,
892
+ do_hackernews: bool = True,
893
+ do_bluesky: bool = True,
894
+ do_truthsocial: bool = True,
895
+ do_polymarket: bool = True,
896
+ no_native_web: bool = False,
897
+ ) -> tuple:
898
+ """Run the research pipeline.
899
+
900
+ Returns:
901
+ Tuple of (reddit_items, x_items, youtube_items, tiktok_items, instagram_items,
902
+ hackernews_items, bluesky_items, truthsocial_items, polymarket_items, web_items, web_needed,
903
+ raw_openai, raw_xai, raw_reddit_enriched,
904
+ reddit_error, x_error, youtube_error, tiktok_error, instagram_error,
905
+ hackernews_error, bluesky_error, truthsocial_error, polymarket_error, web_error)
906
+
907
+ Note: web_needed is True when web search should be performed by the assistant
908
+ (i.e., no native web search API keys are configured). When native web search
909
+ runs, web_items will be populated and web_needed will be False.
910
+ """
911
+ if timeouts is None:
912
+ timeouts = TIMEOUT_PROFILES[depth]
913
+ future_timeout = timeouts["future"]
914
+
915
+ reddit_items = []
916
+ x_items = []
917
+ youtube_items = []
918
+ tiktok_items = []
919
+ instagram_items = []
920
+ hackernews_items = []
921
+ bluesky_items = []
922
+ truthsocial_items = []
923
+ polymarket_items = []
924
+ web_items = []
925
+ raw_openai = None
926
+ raw_xai = None
927
+ raw_reddit_enriched = []
928
+ reddit_error = None
929
+ x_error = None
930
+ youtube_error = None
931
+ tiktok_error = None
932
+ instagram_error = None
933
+ hackernews_error = None
934
+ bluesky_error = None
935
+ truthsocial_error = None
936
+ polymarket_error = None
937
+ web_error = None
938
+ xiaohongshu_error = None
939
+
940
+ # Determine web search mode
941
+ do_web = sources in ("all", "web", "reddit-web", "x-web")
942
+ web_backend = env.get_web_search_source(config) if (do_web and not no_native_web) else None
943
+ web_needed = do_web and not web_backend
944
+
945
+ # Web-only mode
946
+ if sources == "web":
947
+ if web_backend:
948
+ # Native web search available — run it
949
+ sys.stderr.write(f"[web] Searching via {web_backend}\n")
950
+ sys.stderr.flush()
951
+ try:
952
+ web_items, web_error = _search_web(topic, config, from_date, to_date, depth)
953
+ if web_error and progress:
954
+ progress.show_error(f"Web error: {web_error}")
955
+ except Exception as e:
956
+ web_error = f"{type(e).__name__}: {e}"
957
+ if progress:
958
+ progress.show_error(f"Web error: {e}")
959
+ sys.stderr.write(f"[web] {len(web_items)} results\n")
960
+ sys.stderr.flush()
961
+ else:
962
+ # No native backend — assistant handles WebSearch
963
+ if progress:
964
+ progress.start_web_only()
965
+ progress.end_web_only()
966
+ # Optional Xiaohongshu search in web-only mode.
967
+ if run_xiaohongshu:
968
+ try:
969
+ xhs_items, xiaohongshu_error = _search_xiaohongshu(
970
+ topic, config, from_date, to_date, depth,
971
+ )
972
+ web_items.extend(xhs_items)
973
+ if xiaohongshu_error and progress:
974
+ progress.show_error(f"Xiaohongshu error: {xiaohongshu_error}")
975
+ except Exception as e:
976
+ xiaohongshu_error = f"{type(e).__name__}: {e}"
977
+ if progress:
978
+ progress.show_error(f"Xiaohongshu error: {e}")
979
+ # Still run YouTube/TikTok/Instagram in web-only mode if available
980
+ if run_youtube:
981
+ if progress:
982
+ progress.start_youtube()
983
+ try:
984
+ youtube_items, youtube_error = _search_youtube(topic, from_date, to_date, depth)
985
+ if youtube_error and progress:
986
+ progress.show_error(f"YouTube error: {youtube_error}")
987
+ except Exception as e:
988
+ youtube_error = f"{type(e).__name__}: {e}"
989
+ if progress:
990
+ progress.show_error(f"YouTube error: {e}")
991
+ if progress:
992
+ progress.end_youtube(len(youtube_items))
993
+ if run_tiktok:
994
+ if progress:
995
+ progress.start_tiktok()
996
+ try:
997
+ tiktok_items, tiktok_error = _search_tiktok(topic, from_date, to_date, depth, env.get_tiktok_token(config))
998
+ if tiktok_error and progress:
999
+ progress.show_error(f"TikTok error: {tiktok_error}")
1000
+ except Exception as e:
1001
+ tiktok_error = f"{type(e).__name__}: {e}"
1002
+ if progress:
1003
+ progress.show_error(f"TikTok error: {e}")
1004
+ if progress:
1005
+ progress.end_tiktok(len(tiktok_items))
1006
+ if run_instagram:
1007
+ if progress:
1008
+ progress.start_instagram()
1009
+ try:
1010
+ ig_timeout = timeouts.get("instagram_future", future_timeout)
1011
+ instagram_items, instagram_error = _search_instagram(topic, from_date, to_date, depth, env.get_instagram_token(config))
1012
+ if instagram_error and progress:
1013
+ progress.show_error(f"Instagram error: {instagram_error}")
1014
+ except Exception as e:
1015
+ instagram_error = f"{type(e).__name__}: {e}"
1016
+ if progress:
1017
+ progress.show_error(f"Instagram error: {e}")
1018
+ if progress:
1019
+ progress.end_instagram(len(instagram_items))
1020
+ return reddit_items, x_items, youtube_items, tiktok_items, instagram_items, hackernews_items, bluesky_items, truthsocial_items, polymarket_items, web_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error, youtube_error, tiktok_error, instagram_error, hackernews_error, bluesky_error, truthsocial_error, polymarket_error, web_error
1021
+
1022
+ # Determine which searches to run
1023
+ do_reddit = sources in ("both", "reddit", "all", "reddit-web")
1024
+ do_x = sources in ("both", "x", "all", "x-web")
1025
+ # do_hackernews / do_polymarket are always True by default, but can be
1026
+ # restricted via the --search flag to run a focused source subset.
1027
+
1028
+ # Run Reddit, X, YouTube, HN, Polymarket, and Web searches in parallel
1029
+ reddit_future = None
1030
+ x_future = None
1031
+ youtube_future = None
1032
+ tiktok_future = None
1033
+ instagram_future = None
1034
+ xiaohongshu_future = None
1035
+ hackernews_future = None
1036
+ bluesky_future = None
1037
+ truthsocial_future = None
1038
+ polymarket_future = None
1039
+ web_future = None
1040
+ max_workers = (
1041
+ 2
1042
+ + (1 if run_youtube else 0)
1043
+ + (1 if run_tiktok else 0)
1044
+ + (1 if run_instagram else 0)
1045
+ + (1 if run_xiaohongshu else 0)
1046
+ + (1 if do_hackernews else 0)
1047
+ + (1 if do_bluesky else 0)
1048
+ + (1 if do_truthsocial else 0)
1049
+ + (1 if do_polymarket else 0)
1050
+ + (1 if web_backend else 0)
1051
+ )
1052
+
1053
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
1054
+ # Submit searches
1055
+ if do_reddit:
1056
+ if progress:
1057
+ progress.start_reddit()
1058
+ reddit_future = executor.submit(
1059
+ _search_reddit, topic, config, selected_models,
1060
+ from_date, to_date, depth, mock
1061
+ )
1062
+
1063
+ if do_x:
1064
+ if progress:
1065
+ progress.start_x()
1066
+ x_future = executor.submit(
1067
+ _search_x, topic, config, selected_models,
1068
+ from_date, to_date, depth, mock, x_source
1069
+ )
1070
+
1071
+ if run_youtube:
1072
+ if progress:
1073
+ progress.start_youtube()
1074
+ youtube_future = executor.submit(
1075
+ _search_youtube, topic, from_date, to_date, depth
1076
+ )
1077
+
1078
+ if run_tiktok:
1079
+ if progress:
1080
+ progress.start_tiktok()
1081
+ tiktok_future = executor.submit(
1082
+ _search_tiktok, topic, from_date, to_date, depth,
1083
+ env.get_tiktok_token(config),
1084
+ )
1085
+
1086
+ if run_instagram:
1087
+ if progress:
1088
+ progress.start_instagram()
1089
+ instagram_future = executor.submit(
1090
+ _search_instagram, topic, from_date, to_date, depth,
1091
+ env.get_instagram_token(config),
1092
+ )
1093
+
1094
+ if run_xiaohongshu:
1095
+ xiaohongshu_future = executor.submit(
1096
+ _search_xiaohongshu, topic, config, from_date, to_date, depth,
1097
+ )
1098
+
1099
+ if do_hackernews:
1100
+ if progress:
1101
+ progress.start_hackernews()
1102
+ hackernews_future = executor.submit(
1103
+ _search_hackernews, topic, from_date, to_date, depth
1104
+ )
1105
+
1106
+ if do_bluesky:
1107
+ bluesky_future = executor.submit(
1108
+ _search_bluesky, topic, from_date, to_date, depth, config
1109
+ )
1110
+
1111
+ if do_truthsocial:
1112
+ truthsocial_future = executor.submit(
1113
+ _search_truthsocial, topic, from_date, to_date, depth, config
1114
+ )
1115
+
1116
+ if do_polymarket:
1117
+ if progress:
1118
+ progress.start_polymarket()
1119
+ polymarket_future = executor.submit(
1120
+ _search_polymarket, topic, from_date, to_date, depth
1121
+ )
1122
+
1123
+ if web_backend:
1124
+ sys.stderr.write(f"[web] Searching via {web_backend}\n")
1125
+ sys.stderr.flush()
1126
+ web_future = executor.submit(
1127
+ _search_web, topic, config, from_date, to_date, depth
1128
+ )
1129
+
1130
+ # Collect results (with timeouts to prevent indefinite blocking)
1131
+ reddit_used_sc = False # Track if ScrapeCreators was used for Reddit
1132
+ if reddit_future:
1133
+ reddit_timeout = timeouts.get("reddit_future", future_timeout)
1134
+ try:
1135
+ reddit_items, raw_openai, reddit_error, reddit_used_sc = reddit_future.result(timeout=reddit_timeout)
1136
+ if reddit_error and progress:
1137
+ progress.show_error(f"Reddit error: {reddit_error}")
1138
+ except TimeoutError:
1139
+ reddit_error = f"Reddit search timed out after {reddit_timeout}s"
1140
+ if progress:
1141
+ progress.show_error(reddit_error)
1142
+ except Exception as e:
1143
+ reddit_error = f"{type(e).__name__}: {e}"
1144
+ if progress:
1145
+ progress.show_error(f"Reddit error: {e}")
1146
+ if progress:
1147
+ progress.end_reddit(len(reddit_items))
1148
+
1149
+ if x_future:
1150
+ try:
1151
+ x_items, raw_xai, x_error = x_future.result(timeout=future_timeout)
1152
+ if x_error and progress:
1153
+ progress.show_error(f"X error: {x_error}")
1154
+ except TimeoutError:
1155
+ x_error = f"X search timed out after {future_timeout}s"
1156
+ if progress:
1157
+ progress.show_error(x_error)
1158
+ except Exception as e:
1159
+ x_error = f"{type(e).__name__}: {e}"
1160
+ if progress:
1161
+ progress.show_error(f"X error: {e}")
1162
+ if progress:
1163
+ progress.end_x(len(x_items))
1164
+
1165
+ if youtube_future:
1166
+ yt_timeout = timeouts.get("youtube_future", future_timeout)
1167
+ try:
1168
+ youtube_items, youtube_error = youtube_future.result(timeout=yt_timeout)
1169
+ if youtube_error and progress:
1170
+ progress.show_error(f"YouTube error: {youtube_error}")
1171
+ except TimeoutError:
1172
+ youtube_error = f"YouTube search timed out after {yt_timeout}s"
1173
+ if progress:
1174
+ progress.show_error(youtube_error)
1175
+ except Exception as e:
1176
+ youtube_error = f"{type(e).__name__}: {e}"
1177
+ if progress:
1178
+ progress.show_error(f"YouTube error: {e}")
1179
+ if progress:
1180
+ progress.end_youtube(len(youtube_items))
1181
+
1182
+ if tiktok_future:
1183
+ tk_timeout = timeouts.get("tiktok_future", future_timeout)
1184
+ try:
1185
+ tiktok_items, tiktok_error = tiktok_future.result(timeout=tk_timeout)
1186
+ if tiktok_error and progress:
1187
+ progress.show_error(f"TikTok error: {tiktok_error}")
1188
+ except TimeoutError:
1189
+ tiktok_error = f"TikTok search timed out after {tk_timeout}s"
1190
+ if progress:
1191
+ progress.show_error(tiktok_error)
1192
+ except Exception as e:
1193
+ tiktok_error = f"{type(e).__name__}: {e}"
1194
+ if progress:
1195
+ progress.show_error(f"TikTok error: {e}")
1196
+ if progress:
1197
+ progress.end_tiktok(len(tiktok_items))
1198
+
1199
+ if instagram_future:
1200
+ ig_timeout = timeouts.get("instagram_future", future_timeout)
1201
+ try:
1202
+ instagram_items, instagram_error = instagram_future.result(timeout=ig_timeout)
1203
+ if instagram_error and progress:
1204
+ progress.show_error(f"Instagram error: {instagram_error}")
1205
+ except TimeoutError:
1206
+ instagram_error = f"Instagram search timed out after {ig_timeout}s"
1207
+ if progress:
1208
+ progress.show_error(instagram_error)
1209
+ except Exception as e:
1210
+ instagram_error = f"{type(e).__name__}: {e}"
1211
+ if progress:
1212
+ progress.show_error(f"Instagram error: {e}")
1213
+ if progress:
1214
+ progress.end_instagram(len(instagram_items))
1215
+
1216
+ if xiaohongshu_future:
1217
+ try:
1218
+ xhs_items, xiaohongshu_error = xiaohongshu_future.result(timeout=future_timeout)
1219
+ web_items.extend(xhs_items)
1220
+ if xiaohongshu_error and progress:
1221
+ progress.show_error(f"Xiaohongshu error: {xiaohongshu_error}")
1222
+ except TimeoutError:
1223
+ xiaohongshu_error = f"Xiaohongshu search timed out after {future_timeout}s"
1224
+ if progress:
1225
+ progress.show_error(xiaohongshu_error)
1226
+ except Exception as e:
1227
+ xiaohongshu_error = f"{type(e).__name__}: {e}"
1228
+ if progress:
1229
+ progress.show_error(f"Xiaohongshu error: {e}")
1230
+
1231
+ if hackernews_future:
1232
+ hn_timeout = timeouts.get("hackernews_future", future_timeout)
1233
+ try:
1234
+ hackernews_items, hackernews_error = hackernews_future.result(timeout=hn_timeout)
1235
+ if hackernews_error and progress:
1236
+ progress.show_error(f"HN error: {hackernews_error}")
1237
+ except TimeoutError:
1238
+ hackernews_error = f"HN search timed out after {hn_timeout}s"
1239
+ if progress:
1240
+ progress.show_error(hackernews_error)
1241
+ except Exception as e:
1242
+ hackernews_error = f"{type(e).__name__}: {e}"
1243
+ if progress:
1244
+ progress.show_error(f"HN error: {e}")
1245
+ if progress:
1246
+ progress.end_hackernews(len(hackernews_items))
1247
+
1248
+ if bluesky_future:
1249
+ bsky_timeout = timeouts.get("bluesky_future", future_timeout)
1250
+ try:
1251
+ bluesky_items, bluesky_error = bluesky_future.result(timeout=bsky_timeout)
1252
+ if bluesky_error and progress:
1253
+ progress.show_error(f"Bluesky error: {bluesky_error}")
1254
+ except TimeoutError:
1255
+ bluesky_error = f"Bluesky search timed out after {bsky_timeout}s"
1256
+ if progress:
1257
+ progress.show_error(bluesky_error)
1258
+ except Exception as e:
1259
+ bluesky_error = f"{type(e).__name__}: {e}"
1260
+ if progress:
1261
+ progress.show_error(f"Bluesky error: {e}")
1262
+
1263
+ if truthsocial_future:
1264
+ ts_timeout = timeouts.get("truthsocial_future", future_timeout)
1265
+ try:
1266
+ truthsocial_items, truthsocial_error = truthsocial_future.result(timeout=ts_timeout)
1267
+ if truthsocial_error and progress:
1268
+ progress.show_error(f"Truth Social error: {truthsocial_error}")
1269
+ except TimeoutError:
1270
+ truthsocial_error = f"Truth Social search timed out after {ts_timeout}s"
1271
+ if progress:
1272
+ progress.show_error(truthsocial_error)
1273
+ except Exception as e:
1274
+ truthsocial_error = f"{type(e).__name__}: {e}"
1275
+ if progress:
1276
+ progress.show_error(f"Truth Social error: {e}")
1277
+
1278
+ if polymarket_future:
1279
+ pm_timeout = timeouts.get("polymarket_future", future_timeout)
1280
+ try:
1281
+ polymarket_items, polymarket_error = polymarket_future.result(timeout=pm_timeout)
1282
+ if polymarket_error and progress:
1283
+ progress.show_error(f"Polymarket error: {polymarket_error}")
1284
+ except TimeoutError:
1285
+ polymarket_error = f"Polymarket search timed out after {pm_timeout}s"
1286
+ if progress:
1287
+ progress.show_error(polymarket_error)
1288
+ except Exception as e:
1289
+ polymarket_error = f"{type(e).__name__}: {e}"
1290
+ if progress:
1291
+ progress.show_error(f"Polymarket error: {e}")
1292
+ if progress:
1293
+ progress.end_polymarket(len(polymarket_items))
1294
+
1295
+ if web_future:
1296
+ try:
1297
+ web_items, web_error = web_future.result(timeout=future_timeout)
1298
+ if web_error and progress:
1299
+ progress.show_error(f"Web error: {web_error}")
1300
+ except TimeoutError:
1301
+ web_error = f"Web search timed out after {future_timeout}s"
1302
+ if progress:
1303
+ progress.show_error(web_error)
1304
+ except Exception as e:
1305
+ web_error = f"{type(e).__name__}: {e}"
1306
+ if progress:
1307
+ progress.show_error(f"Web error: {e}")
1308
+ sys.stderr.write(f"[web] {len(web_items)} results\n")
1309
+ sys.stderr.flush()
1310
+
1311
+ # Enrich Reddit items with real data (parallel, capped)
1312
+ # Skip enrichment if ScrapeCreators already provided comments + engagement
1313
+ enrich_max = timeouts["enrich_max_items"]
1314
+ enrich_total_timeout = timeouts["enrich_total"]
1315
+ items_to_enrich = reddit_items[:enrich_max]
1316
+ rate_limited = False # Set True if Reddit returns 429 during enrichment
1317
+
1318
+ if reddit_used_sc and items_to_enrich:
1319
+ # ScrapeCreators already enriched items with comments — just copy to raw list
1320
+ sys.stderr.write(f"[Reddit] Skipping old enrichment — ScrapeCreators already provided comments\n")
1321
+ sys.stderr.flush()
1322
+ raw_reddit_enriched = list(reddit_items[:enrich_max])
1323
+ items_to_enrich = [] # Skip the enrichment block below
1324
+
1325
+ if items_to_enrich:
1326
+ if progress:
1327
+ progress.start_reddit_enrich(1, len(items_to_enrich))
1328
+
1329
+ if mock:
1330
+ # Sequential mock enrichment (fast, no need for parallelism)
1331
+ for i, item in enumerate(items_to_enrich):
1332
+ if progress and i > 0:
1333
+ progress.update_reddit_enrich(i + 1, len(items_to_enrich))
1334
+ try:
1335
+ mock_thread = load_fixture("reddit_thread_sample.json")
1336
+ reddit_items[i] = reddit_enrich.enrich_reddit_item(item, mock_thread)
1337
+ except Exception as e:
1338
+ if progress:
1339
+ progress.show_error(f"Enrich failed for {item.get('url', 'unknown')}: {e}")
1340
+ raw_reddit_enriched.append(reddit_items[i])
1341
+ else:
1342
+ # Parallel enrichment with bounded concurrency and total timeout
1343
+ # Uses short HTTP timeout (10s) and 1 retry to fail fast on 429
1344
+ completed_count = 0
1345
+ rate_limited = False
1346
+ with ThreadPoolExecutor(max_workers=5) as enrich_pool:
1347
+ futures = {
1348
+ enrich_pool.submit(reddit_enrich.enrich_reddit_item, item): i
1349
+ for i, item in enumerate(items_to_enrich)
1350
+ }
1351
+ try:
1352
+ for future in as_completed(futures, timeout=enrich_total_timeout):
1353
+ idx = futures[future]
1354
+ completed_count += 1
1355
+ if progress:
1356
+ progress.update_reddit_enrich(completed_count, len(items_to_enrich))
1357
+ try:
1358
+ reddit_items[idx] = future.result(timeout=timeouts["enrich_per"])
1359
+ except reddit_enrich.RedditRateLimitError:
1360
+ rate_limited = True
1361
+ if progress:
1362
+ progress.show_error(
1363
+ "Reddit rate-limited (429) — skipping remaining enrichment"
1364
+ )
1365
+ # Cancel remaining futures and bail
1366
+ for f in futures:
1367
+ f.cancel()
1368
+ break
1369
+ except Exception as e:
1370
+ if progress:
1371
+ progress.show_error(
1372
+ f"Enrich failed for {items_to_enrich[idx].get('url', 'unknown')}: {e}"
1373
+ )
1374
+ raw_reddit_enriched.append(reddit_items[idx])
1375
+ except TimeoutError:
1376
+ if progress:
1377
+ progress.show_error(
1378
+ f"Enrichment timed out after {enrich_total_timeout}s "
1379
+ f"({completed_count}/{len(items_to_enrich)} done)"
1380
+ )
1381
+ # Keep unenriched items as-is
1382
+ for idx in futures.values():
1383
+ if reddit_items[idx] not in raw_reddit_enriched:
1384
+ raw_reddit_enriched.append(reddit_items[idx])
1385
+
1386
+ if progress:
1387
+ progress.end_reddit_enrich()
1388
+
1389
+ # Enrich HN stories with comments
1390
+ if hackernews_items:
1391
+ try:
1392
+ hackernews_items = hackernews.enrich_top_stories(hackernews_items, depth=depth)
1393
+ except Exception as e:
1394
+ sys.stderr.write(f"[HN] Enrichment error: {e}\n")
1395
+ sys.stderr.flush()
1396
+
1397
+ # Phase 2: Supplemental search based on entities from Phase 1
1398
+ # Skip on --quick (speed matters), mock mode, or if Reddit is rate-limiting
1399
+ # Also skip Reddit supplemental when ScrapeCreators was used (subreddit drilling already done)
1400
+ if depth != "quick" and not mock and (reddit_items or x_items):
1401
+ sup_reddit, sup_x = _run_supplemental(
1402
+ topic, reddit_items, x_items,
1403
+ from_date, to_date, depth, x_source, progress,
1404
+ skip_reddit=(rate_limited or reddit_used_sc),
1405
+ resolved_handle=resolved_handle,
1406
+ )
1407
+ if sup_reddit:
1408
+ reddit_items.extend(sup_reddit)
1409
+ if sup_x:
1410
+ x_items.extend(sup_x)
1411
+
1412
+ return reddit_items, x_items, youtube_items, tiktok_items, instagram_items, hackernews_items, bluesky_items, truthsocial_items, polymarket_items, web_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error, youtube_error, tiktok_error, instagram_error, hackernews_error, bluesky_error, truthsocial_error, polymarket_error, web_error
1413
+
1414
+
1415
+ def main():
1416
+ # Fix Unicode output on Windows (cp1252 can't encode emoji)
1417
+ if sys.platform == "win32":
1418
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
1419
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
1420
+
1421
+ parser = argparse.ArgumentParser(
1422
+ description="Research a topic from the last N days on Reddit + X"
1423
+ )
1424
+ parser.add_argument("topic", nargs="*", help="Topic to research")
1425
+ parser.add_argument("--mock", action="store_true", help="Use fixtures")
1426
+ parser.add_argument(
1427
+ "--emit",
1428
+ choices=["compact", "json", "md", "context", "path"],
1429
+ default="compact",
1430
+ help="Output mode",
1431
+ )
1432
+ parser.add_argument(
1433
+ "--sources",
1434
+ choices=["auto", "reddit", "x", "both"],
1435
+ default="auto",
1436
+ help="Source selection",
1437
+ )
1438
+ parser.add_argument(
1439
+ "--quick",
1440
+ action="store_true",
1441
+ help="Faster research with fewer sources (8-12 each)",
1442
+ )
1443
+ parser.add_argument(
1444
+ "--deep",
1445
+ action="store_true",
1446
+ help="Comprehensive research with more sources (50-70 Reddit, 40-60 X)",
1447
+ )
1448
+ parser.add_argument(
1449
+ "--debug",
1450
+ action="store_true",
1451
+ help="Enable verbose debug logging",
1452
+ )
1453
+ parser.add_argument(
1454
+ "--include-web",
1455
+ action="store_true",
1456
+ help="Include general web search alongside Reddit/X (lower weighted)",
1457
+ )
1458
+ parser.add_argument(
1459
+ "--days",
1460
+ type=int,
1461
+ default=30,
1462
+ choices=range(1, 31),
1463
+ metavar="N",
1464
+ help="Number of days to look back (1-30, default: 30)",
1465
+ )
1466
+ parser.add_argument(
1467
+ "--store",
1468
+ action="store_true",
1469
+ help="Persist findings to SQLite database (~/.local/share/last30days/research.db)",
1470
+ )
1471
+ parser.add_argument(
1472
+ "--diagnose",
1473
+ action="store_true",
1474
+ help="Show source availability diagnostics and exit",
1475
+ )
1476
+ parser.add_argument(
1477
+ "--timeout",
1478
+ type=int,
1479
+ default=None,
1480
+ metavar="SECS",
1481
+ help="Global timeout in seconds (default: 180, quick: 90, deep: 300)",
1482
+ )
1483
+ parser.add_argument(
1484
+ "--x-handle",
1485
+ type=str,
1486
+ default=None,
1487
+ metavar="HANDLE",
1488
+ help="Resolved X handle for topic entity (without @). Searched unfiltered in Phase 2.",
1489
+ )
1490
+ parser.add_argument(
1491
+ "--search",
1492
+ type=str,
1493
+ default=None,
1494
+ metavar="SOURCES",
1495
+ help=(
1496
+ "Comma-separated list of sources to run. "
1497
+ f"Valid: {', '.join(sorted(VALID_SEARCH_SOURCES))}. "
1498
+ "Example: --search reddit,hn (default: all configured sources)"
1499
+ ),
1500
+ )
1501
+ parser.add_argument(
1502
+ "--no-native-web",
1503
+ action="store_true",
1504
+ default=False,
1505
+ help="Skip native web search backends (Parallel/Brave/OpenRouter). Use when the assistant has its own WebSearch tool.",
1506
+ )
1507
+ parser.add_argument(
1508
+ "--save-dir",
1509
+ type=str,
1510
+ default=None,
1511
+ metavar="DIR",
1512
+ help="Auto-save raw research output to DIR/{topic-slug}.md",
1513
+ )
1514
+
1515
+ args = parser.parse_args()
1516
+ args.topic = " ".join(args.topic) if args.topic else None
1517
+
1518
+ # Enable debug logging if requested
1519
+ if args.debug:
1520
+ os.environ["LAST30DAYS_DEBUG"] = "1"
1521
+ # Re-import http to pick up debug flag
1522
+ from lib import http as http_module
1523
+ http_module.DEBUG = True
1524
+
1525
+ # Determine depth
1526
+ if args.quick and args.deep:
1527
+ print("Error: Cannot use both --quick and --deep", file=sys.stderr)
1528
+ sys.exit(1)
1529
+ elif args.quick:
1530
+ depth = "quick"
1531
+ elif args.deep:
1532
+ depth = "deep"
1533
+ else:
1534
+ depth = "default"
1535
+
1536
+ # Install global timeout watchdog
1537
+ timeouts = TIMEOUT_PROFILES[depth]
1538
+ global_timeout = args.timeout or timeouts["global"]
1539
+ _install_global_timeout(global_timeout)
1540
+
1541
+ # Load config
1542
+ config = env.get_config()
1543
+
1544
+ # Detect first run (no SETUP_COMPLETE in config)
1545
+ first_run = setup_wizard.is_first_run(config)
1546
+
1547
+ # On first run, block Bird's Node.js sweet-cookie scanner from probing
1548
+ # browser cookies before the user has given consent via the setup wizard.
1549
+ # Explicit AUTH_TOKEN (from env var) is fine — only block browser scanning.
1550
+ if first_run and config.get('_AUTH_TOKEN_SOURCE') != 'env':
1551
+ os.environ['BIRD_DISABLE_BROWSER_COOKIES'] = '1'
1552
+
1553
+ # Inject .env credentials into Bird module before auth check.
1554
+ # On first run (no SETUP_COMPLETE), only inject explicit env var credentials —
1555
+ # skip if AUTH_TOKEN came from browser cookies (no consent yet).
1556
+ if first_run:
1557
+ auth_source = config.get('_AUTH_TOKEN_SOURCE')
1558
+ if auth_source == 'env':
1559
+ bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0'))
1560
+ else:
1561
+ bird_x.set_credentials(config.get('AUTH_TOKEN'), config.get('CT0'))
1562
+
1563
+ # Auto-detect Bird (no prompts - just use it if available)
1564
+ x_source_status = env.get_x_source_status(config)
1565
+ x_source = x_source_status["source"] # 'bird', 'xai', or None
1566
+ x_method = x_source_status.get("method") # 'env', 'browser-firefox', 'api', etc.
1567
+
1568
+ # Auto-detect yt-dlp for YouTube search
1569
+ has_ytdlp = env.is_ytdlp_available()
1570
+
1571
+ # Auto-detect ScrapeCreators/Apify for TikTok
1572
+ has_tiktok = env.is_tiktok_available(config)
1573
+
1574
+ # Auto-detect ScrapeCreators for Instagram
1575
+ has_instagram = env.is_instagram_available(config)
1576
+
1577
+ # Auto-detect Xiaohongshu HTTP API (requires service + login)
1578
+ has_xiaohongshu = env.is_xiaohongshu_available(config)
1579
+
1580
+ # Auto-detect Bluesky (requires BSKY_HANDLE + BSKY_APP_PASSWORD)
1581
+ has_bluesky = env.is_bluesky_available(config)
1582
+
1583
+ # Auto-detect Truth Social (requires TRUTHSOCIAL_TOKEN)
1584
+ has_truthsocial = env.is_truthsocial_available(config)
1585
+
1586
+ # --diagnose: show source availability and exit
1587
+ if args.diagnose:
1588
+ web_source = env.get_web_search_source(config)
1589
+ diag = {
1590
+ "openai": bool(config.get("OPENAI_API_KEY")),
1591
+ "reddit_public": True,
1592
+ "xai": bool(config.get("XAI_API_KEY")),
1593
+ "x_source": x_source_status["source"],
1594
+ "x_method": x_source_status.get("method"),
1595
+ "bird_installed": x_source_status["bird_installed"],
1596
+ "bird_authenticated": x_source_status["bird_authenticated"],
1597
+ "bird_username": x_source_status.get("bird_username"),
1598
+ "youtube": has_ytdlp,
1599
+ "tiktok": has_tiktok,
1600
+ "instagram": has_instagram,
1601
+ "xiaohongshu": has_xiaohongshu,
1602
+ "xiaohongshu_api_base": env.get_xiaohongshu_api_base(config),
1603
+ "hackernews": True,
1604
+ "bluesky": has_bluesky,
1605
+ "truthsocial": has_truthsocial,
1606
+ "polymarket": True,
1607
+ "web_search_backend": web_source,
1608
+ "exa": bool(config.get("EXA_API_KEY")),
1609
+ "parallel_ai": bool(config.get("PARALLEL_API_KEY")),
1610
+ "brave": bool(config.get("BRAVE_API_KEY")),
1611
+ "openrouter": bool(config.get("OPENROUTER_API_KEY")),
1612
+ }
1613
+ print(json.dumps(diag, indent=2))
1614
+ sys.exit(0)
1615
+
1616
+ # Handle 'setup' subcommand
1617
+ if args.topic and args.topic.strip().lower() == "setup":
1618
+ results = setup_wizard.run_auto_setup(config)
1619
+ # Write config
1620
+ env_path = env.CONFIG_FILE
1621
+ if env_path:
1622
+ written = setup_wizard.write_setup_config(env_path)
1623
+ results["env_written"] = written
1624
+ else:
1625
+ results["env_written"] = False
1626
+ print(setup_wizard.get_setup_status_text(results))
1627
+ sys.exit(0)
1628
+
1629
+ # Validate topic (--diagnose doesn't need one)
1630
+ if not args.topic:
1631
+ print("Error: Please provide a topic to research.", file=sys.stderr)
1632
+ print("Usage: python3 last30days.py <topic> [options]", file=sys.stderr)
1633
+ sys.exit(1)
1634
+
1635
+ # Initialize progress display with topic
1636
+ progress = ui.ProgressDisplay(args.topic, show_banner=True)
1637
+
1638
+ # Show status banner (free-first design — lead with what works)
1639
+ web_source = env.get_web_search_source(config)
1640
+ reddit_source = env.get_reddit_source(config)
1641
+ diag = {
1642
+ "setup_complete": bool(config.get("SETUP_COMPLETE")),
1643
+ "reddit_source": reddit_source, # 'scrapecreators', 'openai', or None
1644
+ "x_source": x_source_status["source"],
1645
+ "x_method": x_source_status.get("method"),
1646
+ "youtube": has_ytdlp,
1647
+ "tiktok": has_tiktok,
1648
+ "instagram": has_instagram,
1649
+ "hackernews": True,
1650
+ "polymarket": True,
1651
+ "bluesky": has_bluesky,
1652
+ "truthsocial": has_truthsocial,
1653
+ "xiaohongshu": has_xiaohongshu,
1654
+ "scrapecreators": bool(config.get("SCRAPECREATORS_API_KEY")),
1655
+ "web_search_backend": "deferred to assistant" if args.no_native_web else web_source,
1656
+ }
1657
+ ui.show_diagnostic_banner(diag)
1658
+
1659
+ # Check available sources (now accounts for Bird/cookie auth automatically)
1660
+ available = env.get_available_sources(config)
1661
+
1662
+ # Mock mode can work without keys
1663
+ if args.mock:
1664
+ if args.sources == "auto":
1665
+ sources = "both"
1666
+ else:
1667
+ sources = args.sources
1668
+ else:
1669
+ # Validate requested sources against available
1670
+ sources, error = env.validate_sources(args.sources, available, args.include_web)
1671
+ if error:
1672
+ # If it's a warning about WebSearch fallback, print but continue
1673
+ if "WebSearch fallback" in error:
1674
+ print(f"Note: {error}", file=sys.stderr)
1675
+ else:
1676
+ print(f"Error: {error}", file=sys.stderr)
1677
+ sys.exit(1)
1678
+
1679
+ # Get date range
1680
+ from_date, to_date = dates.get_date_range(args.days)
1681
+
1682
+ # Check what keys are missing for promo messaging
1683
+ missing_keys = env.get_missing_keys(config)
1684
+
1685
+ # Show NUX / promo for missing keys BEFORE research
1686
+ if missing_keys != 'none':
1687
+ progress.show_promo(missing_keys, diag=diag)
1688
+
1689
+ # Select models
1690
+ if args.mock:
1691
+ # Use mock models
1692
+ mock_openai_models = load_fixture("models_openai_sample.json").get("data", [])
1693
+ mock_xai_models = load_fixture("models_xai_sample.json").get("data", [])
1694
+ selected_models = models.get_models(
1695
+ {
1696
+ "OPENAI_API_KEY": "mock",
1697
+ "XAI_API_KEY": "mock",
1698
+ **config,
1699
+ },
1700
+ mock_openai_models,
1701
+ mock_xai_models,
1702
+ )
1703
+ else:
1704
+ selected_models = models.get_models(config)
1705
+
1706
+ # Determine mode string
1707
+ if sources == "all":
1708
+ mode = "all" # reddit + x + web
1709
+ elif sources == "both":
1710
+ mode = "both" # reddit + x
1711
+ elif sources == "reddit":
1712
+ mode = "reddit-only"
1713
+ elif sources == "reddit-web":
1714
+ mode = "reddit-web"
1715
+ elif sources == "x":
1716
+ mode = "x-only"
1717
+ elif sources == "x-web":
1718
+ mode = "x-web"
1719
+ elif sources == "web":
1720
+ mode = "web-only"
1721
+ else:
1722
+ mode = sources
1723
+
1724
+ # Detect query type for source tiering and scoring adjustments
1725
+ query_type = qt.detect_query_type(args.topic)
1726
+
1727
+ # Apply --search flag: restrict sources to the specified subset
1728
+ # Source defaults are query-type-aware (Truth Social always opt-in,
1729
+ # Bluesky only for query types where it adds signal)
1730
+ search_do_hackernews = qt.is_source_enabled("hn", query_type) if not args.search else True
1731
+ search_do_bluesky = has_bluesky and qt.is_source_enabled("bluesky", query_type)
1732
+ search_do_truthsocial = False # Always opt-in (requires --search truthsocial)
1733
+ search_do_polymarket = qt.is_source_enabled("polymarket", query_type)
1734
+ search_run_youtube = has_ytdlp and qt.is_source_enabled("youtube", query_type)
1735
+ search_run_tiktok = has_tiktok and qt.is_source_enabled("tiktok", query_type)
1736
+ search_run_instagram = has_instagram and qt.is_source_enabled("instagram", query_type)
1737
+ search_run_xiaohongshu = has_xiaohongshu
1738
+
1739
+ # INCLUDE_SOURCES override: force specific sources on regardless of tier
1740
+ _include_sources = {s.strip().lower() for s in (config.get('INCLUDE_SOURCES') or '').split(',') if s.strip()}
1741
+ if _include_sources:
1742
+ if 'tiktok' in _include_sources and has_tiktok:
1743
+ if not search_run_tiktok:
1744
+ sys.stderr.write("[Config] INCLUDE_SOURCES override: forcing tiktok\n")
1745
+ search_run_tiktok = True
1746
+ if 'instagram' in _include_sources and has_instagram:
1747
+ if not search_run_instagram:
1748
+ sys.stderr.write("[Config] INCLUDE_SOURCES override: forcing instagram\n")
1749
+ search_run_instagram = True
1750
+ if args.search:
1751
+ search_sources = parse_search_flag(args.search)
1752
+ has_reddit = "reddit" in search_sources
1753
+ has_x = "x" in search_sources
1754
+ search_do_hackernews = "hn" in search_sources
1755
+ search_do_bluesky = ("bluesky" in search_sources or "bsky" in search_sources) and has_bluesky
1756
+ search_do_truthsocial = ("truthsocial" in search_sources or "truth" in search_sources) and has_truthsocial
1757
+ search_do_polymarket = "polymarket" in search_sources
1758
+ search_run_youtube = "youtube" in search_sources and has_ytdlp
1759
+ search_run_tiktok = "tiktok" in search_sources and has_tiktok
1760
+ search_run_instagram = "instagram" in search_sources and has_instagram
1761
+ # If explicitly requested, attempt Xiaohongshu even when preflight says unavailable.
1762
+ search_run_xiaohongshu = "xiaohongshu" in search_sources
1763
+ include_search_web = "web" in search_sources
1764
+ # Map to existing sources string
1765
+ if has_reddit and has_x:
1766
+ sources = "both" + ("-web" if include_search_web else "")
1767
+ sources = "all" if include_search_web else "both"
1768
+ elif has_reddit:
1769
+ sources = "reddit-web" if include_search_web else "reddit"
1770
+ elif has_x:
1771
+ sources = "x-web" if include_search_web else "x"
1772
+ else:
1773
+ sources = "web" # hn/polymarket only; no Reddit/X
1774
+
1775
+ # Run research
1776
+ reddit_items, x_items, youtube_items, tiktok_items, instagram_items, hackernews_items, bluesky_items, truthsocial_items, polymarket_items, web_items, web_needed, raw_openai, raw_xai, raw_reddit_enriched, reddit_error, x_error, youtube_error, tiktok_error, instagram_error, hackernews_error, bluesky_error, truthsocial_error, polymarket_error, web_error = run_research(
1777
+ args.topic,
1778
+ sources,
1779
+ config,
1780
+ selected_models,
1781
+ from_date,
1782
+ to_date,
1783
+ depth,
1784
+ args.mock,
1785
+ progress,
1786
+ x_source=x_source or "xai",
1787
+ run_youtube=search_run_youtube,
1788
+ run_tiktok=search_run_tiktok,
1789
+ run_instagram=search_run_instagram,
1790
+ run_xiaohongshu=search_run_xiaohongshu,
1791
+ timeouts=timeouts,
1792
+ resolved_handle=args.x_handle,
1793
+ do_hackernews=search_do_hackernews,
1794
+ do_bluesky=search_do_bluesky,
1795
+ do_truthsocial=search_do_truthsocial,
1796
+ do_polymarket=search_do_polymarket,
1797
+ no_native_web=args.no_native_web,
1798
+ )
1799
+
1800
+ # Processing phase
1801
+ progress.start_processing()
1802
+
1803
+ # Normalize items
1804
+ normalized_reddit = normalize.normalize_reddit_items(reddit_items, from_date, to_date)
1805
+ normalized_x = normalize.normalize_x_items(x_items, from_date, to_date)
1806
+ normalized_youtube = normalize.normalize_youtube_items(youtube_items, from_date, to_date) if youtube_items else []
1807
+ normalized_tiktok = normalize.normalize_tiktok_items(tiktok_items, from_date, to_date) if tiktok_items else []
1808
+ normalized_ig = normalize.normalize_instagram_items(instagram_items, from_date, to_date) if instagram_items else []
1809
+ normalized_hn = normalize.normalize_hackernews_items(hackernews_items, from_date, to_date) if hackernews_items else []
1810
+ normalized_bsky = normalize.normalize_bluesky_items(bluesky_items, from_date, to_date) if bluesky_items else []
1811
+ normalized_ts = normalize.normalize_truthsocial_items(truthsocial_items, from_date, to_date) if truthsocial_items else []
1812
+ normalized_pm = normalize.normalize_polymarket_items(polymarket_items, from_date, to_date) if polymarket_items else []
1813
+ normalized_web = websearch.normalize_websearch_items(web_items, from_date, to_date) if web_items else []
1814
+
1815
+ # Hard date filter: exclude items with verified dates outside the range
1816
+ # This is the safety net - even if prompts let old content through, this filters it
1817
+ filtered_reddit = normalize.filter_by_date_range(normalized_reddit, from_date, to_date)
1818
+ filtered_x = normalize.filter_by_date_range(normalized_x, from_date, to_date)
1819
+ # YouTube: skip hard date filter — youtube_yt.py already applies a soft filter
1820
+ # that prefers recent videos but keeps older ones for evergreen topics.
1821
+ # YouTube content has a longer shelf life than tweets/posts.
1822
+ filtered_youtube = normalized_youtube
1823
+ # TikTok: hard date filter (tiktok.py already pre-filters, but safety net)
1824
+ filtered_tiktok = normalize.filter_by_date_range(normalized_tiktok, from_date, to_date) if normalized_tiktok else []
1825
+ # Instagram: hard date filter (instagram.py already pre-filters, but safety net)
1826
+ filtered_ig = normalize.filter_by_date_range(normalized_ig, from_date, to_date) if normalized_ig else []
1827
+ filtered_hn = normalize.filter_by_date_range(normalized_hn, from_date, to_date) if normalized_hn else []
1828
+ filtered_bsky = normalize.filter_by_date_range(normalized_bsky, from_date, to_date) if normalized_bsky else []
1829
+ filtered_ts = normalize.filter_by_date_range(normalized_ts, from_date, to_date) if normalized_ts else []
1830
+ # Polymarket: skip hard date filter - markets are active/traded, updatedAt is fine
1831
+ filtered_pm = normalized_pm
1832
+ filtered_web = normalize.filter_by_date_range(normalized_web, from_date, to_date) if normalized_web else []
1833
+
1834
+ # Score items
1835
+ scored_reddit = score.score_reddit_items(filtered_reddit)
1836
+ scored_x = score.score_x_items(filtered_x)
1837
+ scored_youtube = score.score_youtube_items(filtered_youtube) if filtered_youtube else []
1838
+ scored_tiktok = score.score_tiktok_items(filtered_tiktok) if filtered_tiktok else []
1839
+ scored_ig = score.score_instagram_items(filtered_ig) if filtered_ig else []
1840
+ scored_hn = score.score_hackernews_items(filtered_hn) if filtered_hn else []
1841
+ scored_bsky = score.score_bluesky_items(filtered_bsky) if filtered_bsky else []
1842
+ scored_ts = score.score_truthsocial_items(filtered_ts) if filtered_ts else []
1843
+ scored_pm = score.score_polymarket_items(filtered_pm) if filtered_pm else []
1844
+ scored_web = score.score_websearch_items(filtered_web, query_type=query_type) if filtered_web else []
1845
+
1846
+ # Sort items (query-type-aware tiebreaker ordering)
1847
+ sorted_reddit = score.sort_items(scored_reddit, query_type=query_type)
1848
+ sorted_x = score.sort_items(scored_x, query_type=query_type)
1849
+ sorted_youtube = score.sort_items(scored_youtube, query_type=query_type) if scored_youtube else []
1850
+ sorted_tiktok = score.sort_items(scored_tiktok, query_type=query_type) if scored_tiktok else []
1851
+ sorted_ig = score.sort_items(scored_ig, query_type=query_type) if scored_ig else []
1852
+ sorted_hn = score.sort_items(scored_hn, query_type=query_type) if scored_hn else []
1853
+ sorted_bsky = score.sort_items(scored_bsky, query_type=query_type) if scored_bsky else []
1854
+ sorted_ts = score.sort_items(scored_ts, query_type=query_type) if scored_ts else []
1855
+ sorted_pm = score.sort_items(scored_pm, query_type=query_type) if scored_pm else []
1856
+ sorted_web = score.sort_items(scored_web, query_type=query_type) if scored_web else []
1857
+
1858
+ # Dedupe items
1859
+ deduped_reddit = dedupe.dedupe_reddit(sorted_reddit)
1860
+ deduped_x = dedupe.dedupe_x(sorted_x)
1861
+ deduped_youtube = dedupe.dedupe_youtube(sorted_youtube) if sorted_youtube else []
1862
+ deduped_tiktok = dedupe.dedupe_tiktok(sorted_tiktok) if sorted_tiktok else []
1863
+ deduped_ig = dedupe.dedupe_instagram(sorted_ig) if sorted_ig else []
1864
+ deduped_hn = dedupe.dedupe_hackernews(sorted_hn) if sorted_hn else []
1865
+ deduped_bsky = dedupe.dedupe_bluesky(sorted_bsky) if sorted_bsky else []
1866
+ deduped_ts = dedupe.dedupe_truthsocial(sorted_ts) if sorted_ts else []
1867
+ deduped_pm = dedupe.dedupe_polymarket(sorted_pm) if sorted_pm else []
1868
+ deduped_web = websearch.dedupe_websearch(sorted_web) if sorted_web else []
1869
+
1870
+ # Post-retrieval relevance filter: drop low-relevance items per source
1871
+ deduped_reddit = score.relevance_filter(deduped_reddit, "REDDIT")
1872
+ deduped_x = score.relevance_filter(deduped_x, "X")
1873
+ deduped_youtube = score.relevance_filter(deduped_youtube, "YOUTUBE")
1874
+ deduped_tiktok = score.relevance_filter(deduped_tiktok, "TIKTOK")
1875
+ deduped_ig = score.relevance_filter(deduped_ig, "INSTAGRAM")
1876
+ deduped_hn = score.relevance_filter(deduped_hn, "HN")
1877
+ deduped_bsky = score.relevance_filter(deduped_bsky, "BLUESKY")
1878
+ deduped_ts = score.relevance_filter(deduped_ts, "TRUTHSOCIAL")
1879
+ deduped_pm = score.relevance_filter(deduped_pm, "POLYMARKET") if deduped_pm else []
1880
+
1881
+ # Cross-source linking: annotate items that discuss the same story
1882
+ dedupe.cross_source_link(
1883
+ deduped_reddit, deduped_x, deduped_youtube, deduped_tiktok, deduped_ig, deduped_hn, deduped_bsky, deduped_ts, deduped_pm, deduped_web,
1884
+ )
1885
+
1886
+ progress.end_processing()
1887
+
1888
+ # Create report
1889
+ report = schema.create_report(
1890
+ args.topic,
1891
+ from_date,
1892
+ to_date,
1893
+ mode,
1894
+ selected_models.get("openai"),
1895
+ selected_models.get("xai"),
1896
+ )
1897
+ report.reddit = deduped_reddit
1898
+ report.x = deduped_x
1899
+ report.youtube = deduped_youtube
1900
+ report.tiktok = deduped_tiktok
1901
+ report.instagram = deduped_ig
1902
+ report.hackernews = deduped_hn
1903
+ report.bluesky = deduped_bsky
1904
+ report.truthsocial = deduped_ts
1905
+ report.polymarket = deduped_pm
1906
+ report.web = deduped_web
1907
+ report.reddit_error = reddit_error
1908
+ report.x_error = x_error
1909
+ report.youtube_error = youtube_error
1910
+ report.tiktok_error = tiktok_error
1911
+ report.instagram_error = instagram_error
1912
+ report.hackernews_error = hackernews_error
1913
+ report.bluesky_error = bluesky_error
1914
+ report.truthsocial_error = truthsocial_error
1915
+ report.polymarket_error = polymarket_error
1916
+ report.web_error = web_error
1917
+ report.resolved_x_handle = args.x_handle
1918
+
1919
+ # Generate context snippet
1920
+ report.context_snippet_md = render.render_context_snippet(report)
1921
+
1922
+ # Write outputs
1923
+ render.write_outputs(report, raw_openai, raw_xai, raw_reddit_enriched)
1924
+
1925
+ # Show completion
1926
+ if sources == "web":
1927
+ progress.show_web_only_complete()
1928
+ else:
1929
+ progress.show_complete(len(deduped_reddit), len(deduped_x), len(deduped_youtube), len(deduped_hn), len(deduped_pm), len(deduped_tiktok), len(deduped_ig))
1930
+
1931
+ # Build source info for status footer
1932
+ source_info = {}
1933
+ if not x_source:
1934
+ if x_source_status["bird_installed"]:
1935
+ source_info["x_skip_reason"] = "Bird installed but not authenticated — log into x.com in browser"
1936
+ else:
1937
+ source_info["x_skip_reason"] = "No Bird CLI, XAI_API_KEY, or SCRAPECREATORS_API_KEY"
1938
+ if not has_ytdlp:
1939
+ source_info["youtube_skip_reason"] = "yt-dlp not installed — fix: brew install yt-dlp"
1940
+ elif has_ytdlp and not report.youtube:
1941
+ source_info["youtube_skip_reason"] = "0 results (query may be too specific)"
1942
+ if not has_tiktok:
1943
+ source_info["tiktok_skip_reason"] = "No SCRAPECREATORS_API_KEY - sign up at scrapecreators.com (100 free API calls, no credit card)"
1944
+ if not has_instagram:
1945
+ source_info["instagram_skip_reason"] = "No SCRAPECREATORS_API_KEY - sign up at scrapecreators.com (100 free API calls, no credit card)"
1946
+ if not has_xiaohongshu:
1947
+ source_info["xiaohongshu_skip_reason"] = (
1948
+ f"Xiaohongshu API unavailable or not logged in - start xiaohongshu-mcp and login "
1949
+ f"(base: {env.get_xiaohongshu_api_base(config)})"
1950
+ )
1951
+ if not web_source:
1952
+ source_info["web_skip_reason"] = "assistant will use WebSearch (add BRAVE_API_KEY for native search)"
1953
+
1954
+ # Compute quality score and upgrade nudge
1955
+ research_results = {
1956
+ "x_error": x_error,
1957
+ "youtube_error": youtube_error,
1958
+ "reddit_error": reddit_error,
1959
+ }
1960
+ quality = quality_nudge.compute_quality_score(config, research_results)
1961
+
1962
+ # Output result
1963
+ output_result(report, args.emit, web_needed, args.topic, from_date, to_date, missing_keys, args.days, source_info, first_run=first_run, quality=quality)
1964
+
1965
+ # Auto-save raw research to file if --save-dir is set
1966
+ if args.save_dir:
1967
+ import re
1968
+ save_dir = Path(args.save_dir).expanduser()
1969
+ save_dir.mkdir(parents=True, exist_ok=True)
1970
+ slug = re.sub(r'[^a-z0-9]+', '-', args.topic.lower()).strip('-')[:60]
1971
+ save_path = save_dir / f"{slug}-raw.md"
1972
+ if save_path.exists():
1973
+ save_path = save_dir / f"{slug}-raw-{datetime.now().strftime('%Y-%m-%d')}.md"
1974
+ content = render.render_compact(report, missing_keys=missing_keys)
1975
+ if quality and quality.get("nudge_text"):
1976
+ content += "\n" + render.render_quality_nudge(quality)
1977
+ content += "\n" + render.render_source_status(report, source_info)
1978
+ save_path.write_text(content, encoding="utf-8")
1979
+ print(f"📎 {save_path}", file=sys.stderr)
1980
+
1981
+ # Persist findings to SQLite if requested
1982
+ if args.store:
1983
+ import store as store_mod
1984
+ store_mod.init_db()
1985
+ topic_row = store_mod.add_topic(args.topic)
1986
+ topic_id = topic_row["id"]
1987
+ run_id = store_mod.record_run(topic_id, source_mode=mode, status="completed")
1988
+
1989
+ findings = []
1990
+ for item in deduped_reddit:
1991
+ findings.append({
1992
+ "source": "reddit",
1993
+ "url": item.url,
1994
+ "title": item.title,
1995
+ "author": item.subreddit,
1996
+ "content": item.title,
1997
+ "engagement_score": item.engagement.score if item.engagement else 0,
1998
+ "relevance_score": item.relevance,
1999
+ })
2000
+ for item in deduped_x:
2001
+ findings.append({
2002
+ "source": "x",
2003
+ "url": item.url,
2004
+ "title": item.text[:100],
2005
+ "author": item.author_handle,
2006
+ "content": item.text,
2007
+ "engagement_score": item.engagement.likes if item.engagement else 0,
2008
+ "relevance_score": item.relevance,
2009
+ })
2010
+ for item in deduped_youtube:
2011
+ findings.append({
2012
+ "source": "youtube",
2013
+ "url": item.url,
2014
+ "title": item.title,
2015
+ "author": item.channel_name,
2016
+ "content": item.transcript_snippet[:500] if item.transcript_snippet else item.title,
2017
+ "engagement_score": item.engagement.views if item.engagement and item.engagement.views else 0,
2018
+ "relevance_score": item.relevance,
2019
+ })
2020
+ for item in deduped_hn:
2021
+ findings.append({
2022
+ "source": "hackernews",
2023
+ "url": item.hn_url,
2024
+ "title": item.title,
2025
+ "author": item.author,
2026
+ "content": item.title,
2027
+ "engagement_score": item.engagement.score if item.engagement else 0,
2028
+ "relevance_score": item.relevance,
2029
+ })
2030
+ for item in deduped_bsky:
2031
+ findings.append({
2032
+ "source": "bluesky",
2033
+ "url": item.url,
2034
+ "title": item.text[:100],
2035
+ "author": item.author_handle,
2036
+ "content": item.text,
2037
+ "engagement_score": item.engagement.likes if item.engagement else 0,
2038
+ "relevance_score": item.relevance,
2039
+ })
2040
+ for item in deduped_pm:
2041
+ findings.append({
2042
+ "source": "polymarket",
2043
+ "url": item.url,
2044
+ "title": item.question,
2045
+ "author": "polymarket",
2046
+ "content": item.title,
2047
+ "engagement_score": item.engagement.volume if item.engagement and item.engagement.volume else 0,
2048
+ "relevance_score": item.relevance,
2049
+ })
2050
+ for item in deduped_ig:
2051
+ findings.append({
2052
+ "source": "instagram",
2053
+ "url": item.url,
2054
+ "title": item.text[:100],
2055
+ "author": item.author_name,
2056
+ "content": item.caption_snippet[:500] if item.caption_snippet else item.text,
2057
+ "engagement_score": item.engagement.views if item.engagement and item.engagement.views else 0,
2058
+ "relevance_score": item.relevance,
2059
+ })
2060
+ for item in deduped_web:
2061
+ findings.append({
2062
+ "source": "web",
2063
+ "url": item.url,
2064
+ "title": item.title,
2065
+ "author": item.source_domain,
2066
+ "content": item.snippet,
2067
+ "engagement_score": 0,
2068
+ "relevance_score": item.relevance,
2069
+ })
2070
+
2071
+ counts = store_mod.store_findings(run_id, topic_id, findings)
2072
+ store_mod.update_run(
2073
+ run_id,
2074
+ status="completed",
2075
+ findings_new=counts["new"],
2076
+ findings_updated=counts["updated"],
2077
+ )
2078
+ sys.stderr.write(
2079
+ f"[store] Saved {counts['new']} new, {counts['updated']} updated findings\n"
2080
+ )
2081
+ sys.stderr.flush()
2082
+
2083
+
2084
+ def output_result(
2085
+ report: schema.Report,
2086
+ emit_mode: str,
2087
+ web_needed: bool = False,
2088
+ topic: str = "",
2089
+ from_date: str = "",
2090
+ to_date: str = "",
2091
+ missing_keys: str = "none",
2092
+ days: int = 30,
2093
+ source_info: dict = None,
2094
+ first_run: bool = False,
2095
+ quality: dict = None,
2096
+ ):
2097
+ """Output the result based on emit mode."""
2098
+ if emit_mode == "compact":
2099
+ # Emit first-run flag before research output so SKILL.md can detect it
2100
+ if first_run:
2101
+ print("FIRST_RUN: true")
2102
+ print("")
2103
+ print(render.render_compact(report, missing_keys=missing_keys))
2104
+ # Quality nudge (right before source status/stats block)
2105
+ if quality and quality.get("nudge_text"):
2106
+ print(render.render_quality_nudge(quality))
2107
+ # Append source status footer
2108
+ print(render.render_source_status(report, source_info))
2109
+ elif emit_mode == "json":
2110
+ print(json.dumps(report.to_dict(), indent=2))
2111
+ elif emit_mode == "md":
2112
+ print(render.render_full_report(report))
2113
+ elif emit_mode == "context":
2114
+ print(report.context_snippet_md)
2115
+ elif emit_mode == "path":
2116
+ print(render.get_context_path())
2117
+
2118
+ # Output WebSearch instructions if needed
2119
+ if web_needed:
2120
+ print("\n" + "="*60)
2121
+ print("### WEBSEARCH REQUIRED ###")
2122
+ print("="*60)
2123
+ print(f"Topic: {topic}")
2124
+ print(f"Date range: {from_date} to {to_date}")
2125
+ print("")
2126
+ print("Assistant: Use your web search tool to find 8-15 relevant web pages.")
2127
+ print("EXCLUDE: reddit.com, x.com, twitter.com (already covered above)")
2128
+ print(f"INCLUDE: blogs, docs, news, tutorials from the last {days} days")
2129
+ print("")
2130
+ print("After searching, synthesize WebSearch results WITH the Reddit/X")
2131
+ print("results above. WebSearch items should rank LOWER than comparable")
2132
+ print("Reddit/X items (they lack engagement metrics).")
2133
+ print("="*60)
2134
+
2135
+
2136
+ if __name__ == "__main__":
2137
+ main()