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.
- package/.xtrm/config/hooks.json +3 -0
- package/.xtrm/config/pi/extensions/xtrm-ui/format.ts +189 -0
- package/.xtrm/config/pi/extensions/xtrm-ui/index.ts +76 -17
- package/.xtrm/config/pi/extensions/xtrm-ui/package.json +16 -5
- package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.combined.log +7 -0
- package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.stderr.log +0 -0
- package/.xtrm/ext-src/custom-footer/.pi/structured-returns/83051fe4-97da-4e2c-bdaa-343b32f4e714.stdout.log +7 -0
- package/.xtrm/ext-src/xtrm-ui/format.ts +282 -0
- package/.xtrm/{extensions → ext-src}/xtrm-ui/index.ts +76 -17
- package/.xtrm/ext-src/xtrm-ui/package.json +21 -0
- package/.xtrm/hooks/specialists/specialists-complete.mjs +70 -0
- package/.xtrm/hooks/specialists/specialists-session-start.mjs +105 -0
- package/.xtrm/registry.json +397 -409
- package/.xtrm/skills/default/README.txt +31 -0
- package/.xtrm/skills/default/clean-code/SKILL.md +201 -0
- package/.xtrm/skills/default/creating-service-skills/SKILL.md +433 -0
- package/.xtrm/skills/default/creating-service-skills/references/script_quality_standards.md +425 -0
- package/.xtrm/skills/default/creating-service-skills/references/service_skill_system_guide.md +278 -0
- package/.xtrm/skills/default/creating-service-skills/scripts/bootstrap.py +326 -0
- package/.xtrm/skills/default/creating-service-skills/scripts/deep_dive.py +304 -0
- package/.xtrm/skills/default/creating-service-skills/scripts/scaffolder.py +482 -0
- package/.xtrm/skills/default/deepwiki/SKILL.md +50 -0
- package/.xtrm/skills/default/delegating/SKILL.md +196 -0
- package/.xtrm/skills/default/delegating/config.yaml +210 -0
- package/.xtrm/skills/default/delegating/references/orchestration-protocols.md +41 -0
- package/.xtrm/skills/default/documenting/CHANGELOG.md +23 -0
- package/.xtrm/skills/default/documenting/README.md +148 -0
- package/.xtrm/skills/default/documenting/SKILL.md +113 -0
- package/.xtrm/skills/default/documenting/examples/example_pattern.md +70 -0
- package/.xtrm/skills/default/documenting/examples/example_reference.md +70 -0
- package/.xtrm/skills/default/documenting/examples/example_ssot_analytics.md +64 -0
- package/.xtrm/skills/default/documenting/examples/example_workflow.md +141 -0
- package/.xtrm/skills/default/documenting/references/changelog-format.md +97 -0
- package/.xtrm/skills/default/documenting/references/metadata-schema.md +136 -0
- package/.xtrm/skills/default/documenting/references/taxonomy.md +81 -0
- package/.xtrm/skills/default/documenting/references/versioning-rules.md +78 -0
- package/.xtrm/skills/default/documenting/scripts/bump_version.sh +60 -0
- package/.xtrm/skills/default/documenting/scripts/changelog/__init__.py +0 -0
- package/.xtrm/skills/default/documenting/scripts/changelog/add_entry.py +216 -0
- package/.xtrm/skills/default/documenting/scripts/changelog/bump_release.py +117 -0
- package/.xtrm/skills/default/documenting/scripts/changelog/init_changelog.py +54 -0
- package/.xtrm/skills/default/documenting/scripts/changelog/validate_changelog.py +128 -0
- package/.xtrm/skills/default/documenting/scripts/drift_detector.py +266 -0
- package/.xtrm/skills/default/documenting/scripts/generate_template.py +311 -0
- package/.xtrm/skills/default/documenting/scripts/list_by_category.sh +84 -0
- package/.xtrm/skills/default/documenting/scripts/orchestrator.py +255 -0
- package/.xtrm/skills/default/documenting/scripts/validate_metadata.py +242 -0
- package/.xtrm/skills/default/documenting/templates/CHANGELOG.md.template +13 -0
- package/.xtrm/skills/default/find-docs/SKILL.md +175 -0
- package/.xtrm/skills/default/find-skills/SKILL.md +133 -0
- package/.xtrm/skills/default/github-search/SKILL.md +49 -0
- package/.xtrm/skills/default/gitnexus-debugging/SKILL.md +89 -0
- package/.xtrm/skills/default/gitnexus-impact-analysis/SKILL.md +97 -0
- package/.xtrm/skills/default/gitnexus-pr-review/SKILL.md +163 -0
- package/.xtrm/skills/default/gitnexus-refactoring/SKILL.md +121 -0
- package/.xtrm/skills/default/hook-development/SKILL.md +797 -0
- package/.xtrm/skills/default/hook-development/examples/load-context.sh +55 -0
- package/.xtrm/skills/default/hook-development/examples/quality-check.js +1168 -0
- package/.xtrm/skills/default/hook-development/examples/validate-bash.sh +43 -0
- package/.xtrm/skills/default/hook-development/examples/validate-write.sh +38 -0
- package/.xtrm/skills/default/hook-development/references/advanced.md +527 -0
- package/.xtrm/skills/default/hook-development/references/migration.md +369 -0
- package/.xtrm/skills/default/hook-development/references/patterns.md +412 -0
- package/.xtrm/skills/default/hook-development/scripts/README.md +164 -0
- package/.xtrm/skills/default/hook-development/scripts/hook-linter.sh +153 -0
- package/.xtrm/skills/default/hook-development/scripts/test-hook.sh +252 -0
- package/.xtrm/skills/default/hook-development/scripts/validate-hook-schema.sh +159 -0
- package/.xtrm/skills/default/init-session/SKILL.md +69 -0
- package/.xtrm/skills/default/last30days/SKILL.md +881 -0
- package/.xtrm/skills/default/last30days/scripts/briefing.py +260 -0
- package/.xtrm/skills/default/last30days/scripts/evaluate-synthesis.py +120 -0
- package/.xtrm/skills/default/last30days/scripts/evaluate_search_quality.py +641 -0
- package/.xtrm/skills/default/last30days/scripts/generate-synthesis-inputs.py +53 -0
- package/.xtrm/skills/default/last30days/scripts/last30days.py +2137 -0
- package/.xtrm/skills/default/last30days/scripts/lib/__init__.py +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/bird_x.py +458 -0
- package/.xtrm/skills/default/last30days/scripts/lib/bluesky.py +225 -0
- package/.xtrm/skills/default/last30days/scripts/lib/brave_search.py +329 -0
- package/.xtrm/skills/default/last30days/scripts/lib/cache.py +165 -0
- package/.xtrm/skills/default/last30days/scripts/lib/chrome_cookies.py +265 -0
- package/.xtrm/skills/default/last30days/scripts/lib/cookie_extract.py +295 -0
- package/.xtrm/skills/default/last30days/scripts/lib/dates.py +124 -0
- package/.xtrm/skills/default/last30days/scripts/lib/dedupe.py +290 -0
- package/.xtrm/skills/default/last30days/scripts/lib/entity_extract.py +127 -0
- package/.xtrm/skills/default/last30days/scripts/lib/env.py +807 -0
- package/.xtrm/skills/default/last30days/scripts/lib/exa_search.py +176 -0
- package/.xtrm/skills/default/last30days/scripts/lib/hackernews.py +266 -0
- package/.xtrm/skills/default/last30days/scripts/lib/http.py +174 -0
- package/.xtrm/skills/default/last30days/scripts/lib/instagram.py +365 -0
- package/.xtrm/skills/default/last30days/scripts/lib/models.py +221 -0
- package/.xtrm/skills/default/last30days/scripts/lib/normalize.py +489 -0
- package/.xtrm/skills/default/last30days/scripts/lib/openai_reddit.py +631 -0
- package/.xtrm/skills/default/last30days/scripts/lib/openrouter_search.py +216 -0
- package/.xtrm/skills/default/last30days/scripts/lib/parallel_search.py +139 -0
- package/.xtrm/skills/default/last30days/scripts/lib/polymarket.py +580 -0
- package/.xtrm/skills/default/last30days/scripts/lib/quality_nudge.py +201 -0
- package/.xtrm/skills/default/last30days/scripts/lib/query.py +117 -0
- package/.xtrm/skills/default/last30days/scripts/lib/query_type.py +111 -0
- package/.xtrm/skills/default/last30days/scripts/lib/reddit.py +617 -0
- package/.xtrm/skills/default/last30days/scripts/lib/reddit_enrich.py +325 -0
- package/.xtrm/skills/default/last30days/scripts/lib/reddit_public.py +259 -0
- package/.xtrm/skills/default/last30days/scripts/lib/relevance.py +148 -0
- package/.xtrm/skills/default/last30days/scripts/lib/render.py +1018 -0
- package/.xtrm/skills/default/last30days/scripts/lib/safari_cookies.py +182 -0
- package/.xtrm/skills/default/last30days/scripts/lib/schema.py +843 -0
- package/.xtrm/skills/default/last30days/scripts/lib/score.py +775 -0
- package/.xtrm/skills/default/last30days/scripts/lib/scrapecreators_x.py +182 -0
- package/.xtrm/skills/default/last30days/scripts/lib/setup_wizard.py +186 -0
- package/.xtrm/skills/default/last30days/scripts/lib/tiktok.py +349 -0
- package/.xtrm/skills/default/last30days/scripts/lib/truthsocial.py +183 -0
- package/.xtrm/skills/default/last30days/scripts/lib/ui.py +620 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/LICENSE +21 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/bird-search.mjs +134 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/cookies.js +191 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/features.json +17 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/paginate-cursor.js +37 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/query-ids.json +20 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/runtime-features.js +151 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/runtime-query-ids.js +264 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-base.js +129 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-constants.js +50 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-features.js +347 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-search.js +157 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-types.js +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/lib/twitter-client-utils.js +511 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/LICENSE +22 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/README.md +29 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.d.ts +3 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.js +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/index.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.js +27 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chrome.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts +11 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.js +100 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/crypto.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts +25 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js +104 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/linuxKeyring.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts +10 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.js +293 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/shared.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts +10 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js +26 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqlite/windowsDpapi.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts +7 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.js +51 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteLinux.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.d.ts +7 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.js +60 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteMac.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts +7 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.js +38 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromeSqliteWindows.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts +5 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.js +33 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/linuxPaths.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts +24 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.js +30 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/macosKeychain.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.d.ts +11 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.js +43 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/paths.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.js +41 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsMasterKey.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.js +53 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/chromium/windowsPaths.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.js +27 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edge.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts +7 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.js +53 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteLinux.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.js +60 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteMac.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts +7 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.js +38 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/edgeSqliteWindows.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.d.ts +6 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.js +257 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/firefoxSqlite.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.js +71 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/inline.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.d.ts +6 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.js +173 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/providers/safariBinaryCookies.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.d.ts +26 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.js +195 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/public.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.d.ts +121 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.js +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/types.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.d.ts +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.js +18 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/base64.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.d.ts +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.js +110 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/exec.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.d.ts +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.js +32 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/expire.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.d.ts +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.js +13 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/fs.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.d.ts +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.js +7 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/hostMatch.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.d.ts +5 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.js +58 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/nodeSqlite.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.d.ts +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.js +27 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/origins.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.d.ts +2 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.d.ts.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.js +8 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/dist/util/runtime.js.map +1 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/node_modules/@steipete/sweet-cookie/package.json +40 -0
- package/.xtrm/skills/default/last30days/scripts/lib/vendor/bird-search/package.json +13 -0
- package/.xtrm/skills/default/last30days/scripts/lib/websearch.py +401 -0
- package/.xtrm/skills/default/last30days/scripts/lib/xai_x.py +217 -0
- package/.xtrm/skills/default/last30days/scripts/lib/xiaohongshu_api.py +162 -0
- package/.xtrm/skills/default/last30days/scripts/lib/youtube_yt.py +538 -0
- package/.xtrm/skills/default/last30days/scripts/store.py +654 -0
- package/.xtrm/skills/default/last30days/scripts/sync.sh +50 -0
- package/.xtrm/skills/default/last30days/scripts/test-v1-vs-v2.sh +219 -0
- package/.xtrm/skills/default/last30days/scripts/watchlist.py +329 -0
- package/.xtrm/skills/default/planning/SKILL.md +405 -0
- package/.xtrm/skills/default/planning/evals/evals.json +19 -0
- package/.xtrm/skills/default/prompt-improving/README.md +162 -0
- package/.xtrm/skills/default/prompt-improving/SKILL.md +74 -0
- package/.xtrm/skills/default/prompt-improving/references/analysis_commands.md +24 -0
- package/.xtrm/skills/default/prompt-improving/references/chain_of_thought.md +24 -0
- package/.xtrm/skills/default/prompt-improving/references/mcp_definitions.md +20 -0
- package/.xtrm/skills/default/prompt-improving/references/multishot.md +23 -0
- package/.xtrm/skills/default/prompt-improving/references/xml_core.md +60 -0
- package/.xtrm/skills/default/quality-gates/.claude/hooks/hook-config.json +66 -0
- package/.xtrm/skills/default/quality-gates/.claude/hooks/quality-check.cjs +1286 -0
- package/.xtrm/skills/default/quality-gates/.claude/hooks/quality-check.py +334 -0
- package/.xtrm/skills/default/quality-gates/.claude/settings.json +3 -0
- package/.xtrm/skills/default/quality-gates/.claude/skills/using-quality-gates/SKILL.md +254 -0
- package/.xtrm/skills/default/quality-gates/README.md +109 -0
- package/.xtrm/skills/default/quality-gates/evals/evals.json +181 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/FINAL-EVAL-SUMMARY.md +75 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/edge-case-auto-fix-verification/with_skill/outputs/response.md +59 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/edge-case-mixed-language-project/with_skill/outputs/response.md +60 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/eval-summary.md +105 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/partial-install-python-only/with_skill/outputs/response.md +93 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/python-refactor-request/with_skill/outputs/response.md +104 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/quality-gate-error-fix/with_skill/outputs/response.md +74 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-general-chat/with_skill/outputs/response.md +18 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-math-question/with_skill/outputs/response.md +18 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/should-not-trigger-unrelated-coding/with_skill/outputs/response.md +56 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/tdd-guard-blocking-confusion/with_skill/outputs/response.md +67 -0
- package/.xtrm/skills/default/quality-gates/workspace/iteration-1/typescript-feature-with-tests/with_skill/outputs/response.md +97 -0
- package/.xtrm/skills/default/scoping-service-skills/SKILL.md +231 -0
- package/.xtrm/skills/default/scoping-service-skills/scripts/scope.py +74 -0
- package/.xtrm/skills/default/service-skills-set/README.md +93 -0
- package/.xtrm/skills/default/service-skills-set/git-hooks/doc_reminder.py +67 -0
- package/.xtrm/skills/default/service-skills-set/git-hooks/skill_staleness.py +194 -0
- package/.xtrm/skills/default/service-skills-set/install-service-skills.py +193 -0
- package/.xtrm/skills/default/service-skills-set/service-registry.json +4 -0
- package/.xtrm/skills/default/service-skills-set/service-skills-readme.md +236 -0
- package/.xtrm/skills/default/service-skills-set/settings.json +37 -0
- package/.xtrm/skills/default/session-close-report/SKILL.md +131 -0
- package/.xtrm/skills/default/skill-creator/LICENSE.txt +202 -0
- package/.xtrm/skills/default/skill-creator/SKILL.md +479 -0
- package/.xtrm/skills/default/skill-creator/agents/analyzer.md +274 -0
- package/.xtrm/skills/default/skill-creator/agents/comparator.md +202 -0
- package/.xtrm/skills/default/skill-creator/agents/grader.md +223 -0
- package/.xtrm/skills/default/skill-creator/assets/eval_review.html +146 -0
- package/.xtrm/skills/default/skill-creator/eval-viewer/generate_review.py +471 -0
- package/.xtrm/skills/default/skill-creator/eval-viewer/viewer.html +1325 -0
- package/.xtrm/skills/default/skill-creator/references/schemas.md +430 -0
- package/.xtrm/skills/default/skill-creator/scripts/__init__.py +0 -0
- package/.xtrm/skills/default/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/.xtrm/skills/default/skill-creator/scripts/generate_report.py +326 -0
- package/.xtrm/skills/default/skill-creator/scripts/improve_description.py +248 -0
- package/.xtrm/skills/default/skill-creator/scripts/package_skill.py +136 -0
- package/.xtrm/skills/default/skill-creator/scripts/quick_validate.py +103 -0
- package/.xtrm/skills/default/skill-creator/scripts/run_eval.py +310 -0
- package/.xtrm/skills/default/skill-creator/scripts/run_loop.py +332 -0
- package/.xtrm/skills/default/skill-creator/scripts/utils.py +47 -0
- package/.xtrm/skills/default/specialists-creator/SKILL.md +705 -0
- package/.xtrm/skills/default/specialists-creator/scripts/validate-specialist.ts +41 -0
- package/.xtrm/skills/default/sync-docs/SKILL.md +262 -0
- package/.xtrm/skills/default/sync-docs/evals/evals.json +89 -0
- package/.xtrm/skills/default/sync-docs/references/doc-structure.md +99 -0
- package/.xtrm/skills/default/sync-docs/references/schema.md +103 -0
- package/.xtrm/skills/default/sync-docs/scripts/changelog/add_entry.py +216 -0
- package/.xtrm/skills/default/sync-docs/scripts/context_gatherer.py +405 -0
- package/.xtrm/skills/default/sync-docs/scripts/doc_structure_analyzer.py +495 -0
- package/.xtrm/skills/default/sync-docs/scripts/drift_detector.py +563 -0
- package/.xtrm/skills/default/sync-docs/scripts/validate_doc.py +365 -0
- package/.xtrm/skills/default/sync-docs/scripts/validate_metadata.py +185 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/benchmark.json +293 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/benchmark.md +13 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/outputs/result.md +210 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/grading.json +28 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/with_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/outputs/result.md +101 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/grading.json +28 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-doc-audit/without_skill/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/outputs/result.md +198 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/grading.json +28 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/with_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/outputs/result.md +94 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/grading.json +28 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-fix-mode/without_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/outputs/result.md +237 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/grading.json +28 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/outputs/result.md +134 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/grading.json +28 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-1/eval-sprint-closeout/without_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/benchmark.json +297 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/benchmark.md +13 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/outputs/result.md +137 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/grading.json +92 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/with_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/outputs/result.md +134 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/grading.json +86 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-doc-audit/without_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/outputs/result.md +193 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/grading.json +72 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/with_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/outputs/result.md +211 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/grading.json +91 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-fix-mode/without_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/outputs/result.md +182 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/with_skill/run-1/timing.json +1 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/outputs/result.md +222 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/grading.json +88 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-2/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/benchmark.json +298 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/benchmark.md +13 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/outputs/result.md +125 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/grading.json +97 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/with_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/outputs/result.md +144 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/grading.json +78 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-doc-audit/without_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/outputs/result.md +104 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/grading.json +91 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/with_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/outputs/result.md +79 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/grading.json +82 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-fix-mode/without_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/eval_metadata.json +27 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase1_context.json +302 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase2_drift.txt +33 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase3_analysis.json +114 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase4_fix.txt +118 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/phase5_validate.txt +38 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/outputs/result.md +158 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/grading.json +95 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/with_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/outputs/result.md +71 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/grading.json +90 -0
- package/.xtrm/skills/default/sync-docs-workspace/iteration-3/eval-sprint-closeout/without_skill/run-1/timing.json +5 -0
- package/.xtrm/skills/default/test-planning/SKILL.md +465 -0
- package/.xtrm/skills/default/test-planning/evals/evals.json +23 -0
- package/.xtrm/skills/default/updating-service-skills/SKILL.md +136 -0
- package/.xtrm/skills/default/updating-service-skills/scripts/drift_detector.py +222 -0
- package/.xtrm/skills/default/using-nodes/SKILL.md +333 -0
- package/.xtrm/skills/default/using-quality-gates/SKILL.md +254 -0
- package/.xtrm/skills/default/using-service-skills/SKILL.md +108 -0
- package/.xtrm/skills/default/using-service-skills/scripts/cataloger.py +74 -0
- package/.xtrm/skills/default/using-service-skills/scripts/skill_activator.py +152 -0
- package/.xtrm/skills/default/using-specialists/SKILL.md +848 -0
- package/.xtrm/skills/default/using-specialists/evals/evals.json +68 -0
- package/.xtrm/skills/default/using-tdd/SKILL.md +410 -0
- package/.xtrm/skills/default/using-xtrm/SKILL.md +127 -0
- package/.xtrm/skills/default/xt-debugging/SKILL.md +149 -0
- package/.xtrm/skills/default/xt-end/SKILL.md +297 -0
- package/.xtrm/skills/default/xt-merge/SKILL.md +326 -0
- package/.xtrm/skills/optional/README.txt +2 -0
- package/.xtrm/skills/optional/architecture-design/PACK.json +11 -0
- package/.xtrm/skills/optional/architecture-design/architecture-patterns/SKILL.md +494 -0
- package/.xtrm/skills/optional/architecture-design/architecture-patterns/references/advanced-patterns.md +391 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/SKILL.md +473 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/assets/few-shot-examples.json +106 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/assets/prompt-template-library.md +264 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/chain-of-thought.md +412 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/few-shot-learning.md +386 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/prompt-optimization.md +428 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/prompt-templates.md +484 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/references/system-prompts.md +195 -0
- package/.xtrm/skills/optional/architecture-design/prompt-engineering-patterns/scripts/optimize-prompt.py +279 -0
- package/.xtrm/skills/optional/architecture-design/subagent-driven-development/SKILL.md +277 -0
- package/.xtrm/skills/optional/architecture-design/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
- package/.xtrm/skills/optional/architecture-design/subagent-driven-development/implementer-prompt.md +113 -0
- package/.xtrm/skills/optional/architecture-design/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/.xtrm/skills/optional/code-quality/PACK.json +12 -0
- package/.xtrm/skills/optional/code-quality/code-review-excellence/SKILL.md +529 -0
- package/.xtrm/skills/optional/code-quality/multi-reviewer-patterns/SKILL.md +127 -0
- package/.xtrm/skills/optional/code-quality/systematic-debugging/SKILL.md +296 -0
- package/.xtrm/skills/optional/code-quality/verification-before-completion/SKILL.md +139 -0
- package/.xtrm/skills/optional/data-engineering/PACK.json +9 -0
- package/.xtrm/skills/optional/data-engineering/data-analyst/SKILL.md +57 -0
- package/.xtrm/skills/optional/research-methods/PACK.json +12 -0
- package/.xtrm/skills/optional/research-methods/academic-researcher/SKILL.md +269 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/SKILL.md +164 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/scripts/frame-template.html +214 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/scripts/helper.js +88 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/scripts/server.cjs +354 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/scripts/start-server.sh +148 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/scripts/stop-server.sh +56 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/.xtrm/skills/optional/research-methods/brainstorming/visual-companion.md +287 -0
- package/.xtrm/skills/optional/research-methods/deep-research/SKILL.md +192 -0
- package/.xtrm/skills/optional/research-methods/fact-checker/SKILL.md +182 -0
- package/.xtrm/skills/optional/security-ops/PACK.json +9 -0
- package/.xtrm/skills/optional/security-ops/security-auditor/SKILL.md +165 -0
- package/.xtrm/skills/optional/xt-optional/PACK.json +16 -0
- package/.xtrm/skills/optional/xt-optional/docker-expert/SKILL.md +409 -0
- package/.xtrm/skills/optional/xt-optional/obsidian-cli/SKILL.md +106 -0
- package/.xtrm/skills/optional/xt-optional/python-testing/SKILL.md +815 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/SKILL.md +209 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/references/api_design_patterns.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/references/backend_security_practices.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/references/database_optimization_guide.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/scripts/api_load_tester.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/scripts/api_scaffolder.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-backend/scripts/database_migration_tool.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/SKILL.md +226 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/references/experiment_design_frameworks.md +80 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/references/feature_engineering_patterns.md +80 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/references/statistical_methods_advanced.md +80 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/scripts/experiment_designer.py +100 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/scripts/feature_engineering_pipeline.py +100 -0
- package/.xtrm/skills/optional/xt-optional/senior-data-scientist/scripts/model_evaluation_suite.py +100 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/SKILL.md +209 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/references/cicd_pipeline_guide.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/references/deployment_strategies.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/references/infrastructure_as_code.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/scripts/deployment_manager.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/scripts/pipeline_generator.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-devops/scripts/terraform_scaffolder.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/SKILL.md +209 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/references/cryptography_implementation.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/references/penetration_testing_guide.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/references/security_architecture_patterns.md +103 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/scripts/pentest_automator.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/scripts/security_auditor.py +114 -0
- package/.xtrm/skills/optional/xt-optional/senior-security/scripts/threat_modeler.py +114 -0
- package/CHANGELOG.md +16 -0
- package/README.md +5 -0
- package/cli/dist/index.cjs +862 -614
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/package.json +4 -1
- package/.xtrm/extensions/xtrm-ui/format.ts +0 -93
- package/.xtrm/extensions/xtrm-ui/package.json +0 -10
- /package/.xtrm/{extensions → ext-src}/auto-session-name/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/auto-session-name/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/auto-update/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/auto-update/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/beads/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/beads/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/compact-header/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/compact-header/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/core/adapter.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/core/guard-rules.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/core/lib.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/core/logger.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/core/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/core/runner.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/core/session-state.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/custom-footer/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/custom-footer/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/custom-provider-qwen-cli/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/custom-provider-qwen-cli/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/git-checkpoint/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/git-checkpoint/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/lsp-bootstrap/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/lsp-bootstrap/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/pi-serena-compact/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/pi-serena-compact/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/quality-gates/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/quality-gates/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/service-skills/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/service-skills/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/session-flow/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/session-flow/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/xtrm-loader/index.ts +0 -0
- /package/.xtrm/{extensions → ext-src}/xtrm-loader/package.json +0 -0
- /package/.xtrm/{extensions → ext-src}/xtrm-ui/themes/pidex-dark.json +0 -0
- /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()
|