argo-search 1.0.1 → 2.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (472) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +735 -0
  3. package/README.es.md +777 -0
  4. package/README.ja.md +777 -0
  5. package/README.ko.md +777 -0
  6. package/README.md +778 -320
  7. package/SKILL.md +135 -0
  8. package/assets/readme/hero.svg +121 -0
  9. package/assets/readme/made-with-beautify.svg +10 -0
  10. package/assets/readme/proof-routes.svg +73 -0
  11. package/assets/readme/why-better.svg +104 -0
  12. package/assets/readme/workflow.svg +83 -0
  13. package/backends/domain_profiles.json +2356 -96
  14. package/backends/engine_registry.yaml +3428 -503
  15. package/backends/query_synonyms_cn.json +35 -0
  16. package/backends/quota_profiles.json +2488 -160
  17. package/backends/source_types_cn.json +109 -0
  18. package/backends/text_lexicon.json +44 -0
  19. package/bin/argo +772 -0
  20. package/bin/argo.js +102 -39
  21. package/config.yaml +5139 -367
  22. package/cordis.patch.yml +21 -0
  23. package/data/countries.json +40 -0
  24. package/data/lang_matrix/lang_capability.json +842 -0
  25. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  26. package/data/regions_cn.json +1 -0
  27. package/docs/ADDING_NEW_ENGINE.md +184 -0
  28. package/docs/ARGO_INTRO.md +124 -0
  29. package/docs/DATA_SOURCE_GAPS.md +81 -0
  30. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  31. package/docs/ENGINE_CATALOG.md +575 -0
  32. package/docs/MCP_SETUP.md +108 -0
  33. package/docs/PLATFORM_PATHS.md +111 -0
  34. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  35. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  36. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  37. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  38. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  39. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  40. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  41. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  42. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  43. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  44. package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
  45. package/docs/SEARCH_ARCHIVE.md +115 -0
  46. package/docs/assets/architecture.svg +104 -0
  47. package/docs/assets/hero.svg +98 -0
  48. package/docs/engines/bangumi.md +27 -0
  49. package/docs/engines/biorxiv.md +27 -0
  50. package/docs/engines/clawhub.md +27 -0
  51. package/docs/engines/crt_sh.md +27 -0
  52. package/docs/engines/datacite.md +27 -0
  53. package/docs/engines/deps_dev.md +27 -0
  54. package/docs/engines/douban_movie.md +27 -0
  55. package/docs/engines/endoflife.md +27 -0
  56. package/docs/engines/flk_law.md +27 -0
  57. package/docs/engines/fxtwitter.md +27 -0
  58. package/docs/engines/google_news.md +27 -0
  59. package/docs/engines/gov_regulations.md +27 -0
  60. package/docs/engines/guqian.md +27 -0
  61. package/docs/engines/hackernews.md +27 -0
  62. package/docs/engines/iplant.md +27 -0
  63. package/docs/engines/keenable.md +27 -0
  64. package/docs/engines/met_museum.md +27 -0
  65. package/docs/engines/nasa_images.md +27 -0
  66. package/docs/engines/netease_music.md +27 -0
  67. package/docs/engines/npm.md +27 -0
  68. package/docs/engines/nvd.md +27 -0
  69. package/docs/engines/openreview.md +27 -0
  70. package/docs/engines/openstd.md +27 -0
  71. package/docs/engines/parallel_free.md +27 -0
  72. package/docs/engines/people_daily.md +27 -0
  73. package/docs/engines/realtime_index.md +27 -0
  74. package/docs/engines/sec_edgar.md +27 -0
  75. package/docs/engines/seltz.md +27 -0
  76. package/docs/engines/skillsmp.md +27 -0
  77. package/docs/engines/sspai.md +27 -0
  78. package/docs/engines/std_samr.md +27 -0
  79. package/docs/engines/train.md +27 -0
  80. package/docs/engines/un_comtrade.md +27 -0
  81. package/docs/engines/wikisource.md +27 -0
  82. package/docs/engines/zdic.md +27 -0
  83. package/docs/engines/zenodo.md +27 -0
  84. package/docs/engines/zhihu_hot_app.md +27 -0
  85. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  86. package/engines/README.md +10 -0
  87. package/engines/_template_cli.yaml +39 -0
  88. package/engines/_template_http.yaml +37 -0
  89. package/engines/plugins/_template_plugin.py +50 -0
  90. package/engines/specs/_cisa_kev.yaml +54 -0
  91. package/engines/specs/_osv.yaml +63 -0
  92. package/engines/specs/aljazeera.yaml +43 -0
  93. package/engines/specs/antara.yaml +43 -0
  94. package/engines/specs/artic.yaml +20 -0
  95. package/engines/specs/aviation_weather.yaml +39 -0
  96. package/engines/specs/bangumi.yaml +21 -0
  97. package/engines/specs/biorxiv.yaml +24 -0
  98. package/engines/specs/carbon_intensity.yaml +32 -0
  99. package/engines/specs/cfets.yaml +41 -0
  100. package/engines/specs/cinii.yaml +44 -0
  101. package/engines/specs/clawhub.yaml +49 -0
  102. package/engines/specs/cleveland.yaml +19 -0
  103. package/engines/specs/cn_ai_news.yaml +43 -0
  104. package/engines/specs/core.yaml +35 -0
  105. package/engines/specs/crt_sh.yaml +38 -0
  106. package/engines/specs/ct24.yaml +43 -0
  107. package/engines/specs/datacite.yaml +27 -0
  108. package/engines/specs/datagov_uk.yaml +41 -0
  109. package/engines/specs/ddgs_images.yaml +45 -0
  110. package/engines/specs/deepseek_search.yaml +74 -0
  111. package/engines/specs/deezer.yaml +20 -0
  112. package/engines/specs/defillama.yaml +38 -0
  113. package/engines/specs/deps_dev.yaml +21 -0
  114. package/engines/specs/digup.yaml +36 -0
  115. package/engines/specs/doi.yaml +39 -0
  116. package/engines/specs/douban_movie.yaml +22 -0
  117. package/engines/specs/dw_news.yaml +47 -0
  118. package/engines/specs/egov_law.yaml +21 -0
  119. package/engines/specs/elpais.yaml +43 -0
  120. package/engines/specs/endoflife.yaml +21 -0
  121. package/engines/specs/energy_charts.yaml +21 -0
  122. package/engines/specs/factcheck_org.yaml +52 -0
  123. package/engines/specs/faz.yaml +43 -0
  124. package/engines/specs/federal_register.yaml +57 -0
  125. package/engines/specs/firecrawl.yaml +53 -0
  126. package/engines/specs/flk_law.yaml +24 -0
  127. package/engines/specs/folha.yaml +43 -0
  128. package/engines/specs/france24.yaml +44 -0
  129. package/engines/specs/full_fact.yaml +47 -0
  130. package/engines/specs/fxtwitter.yaml +36 -0
  131. package/engines/specs/gbfs_nyc.yaml +32 -0
  132. package/engines/specs/gdacs.yaml +21 -0
  133. package/engines/specs/gdelt.yaml +64 -0
  134. package/engines/specs/gleif.yaml +55 -0
  135. package/engines/specs/google_news.yaml +22 -0
  136. package/engines/specs/gov_regulations.yaml +38 -0
  137. package/engines/specs/guardian_rss.yaml +59 -0
  138. package/engines/specs/hkex.yaml +40 -0
  139. package/engines/specs/hurriyet.yaml +43 -0
  140. package/engines/specs/iplant.yaml +25 -0
  141. package/engines/specs/jikan.yaml +24 -0
  142. package/engines/specs/jolpica.yaml +20 -0
  143. package/engines/specs/k10plus.yaml +20 -0
  144. package/engines/specs/keenable.yaml +44 -0
  145. package/engines/specs/keenable_free.yaml +36 -0
  146. package/engines/specs/lefigaro.yaml +43 -0
  147. package/engines/specs/listenbrainz.yaml +19 -0
  148. package/engines/specs/matichon.yaml +43 -0
  149. package/engines/specs/met_museum.yaml +21 -0
  150. package/engines/specs/nasa_images.yaml +45 -0
  151. package/engines/specs/netease_music.yaml +38 -0
  152. package/engines/specs/nhk.yaml +43 -0
  153. package/engines/specs/nhtsa_vpic.yaml +20 -0
  154. package/engines/specs/noaa_swpc.yaml +20 -0
  155. package/engines/specs/nvd.yaml +38 -0
  156. package/engines/specs/obis.yaml +20 -0
  157. package/engines/specs/opencitations.yaml +52 -0
  158. package/engines/specs/openf1.yaml +21 -0
  159. package/engines/specs/openfoodfacts.yaml +45 -0
  160. package/engines/specs/openligadb.yaml +20 -0
  161. package/engines/specs/openreview.yaml +37 -0
  162. package/engines/specs/openstd.yaml +21 -0
  163. package/engines/specs/people_daily.yaml +22 -0
  164. package/engines/specs/realtime_index.yaml +34 -0
  165. package/engines/specs/ror.yaml +20 -0
  166. package/engines/specs/satnogs.yaml +20 -0
  167. package/engines/specs/sec_edgar.yaml +46 -0
  168. package/engines/specs/shfe.yaml +39 -0
  169. package/engines/specs/skillsmp.yaml +50 -0
  170. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  171. package/engines/specs/soilgrids.yaml +25 -0
  172. package/engines/specs/sspai.yaml +36 -0
  173. package/engines/specs/stackexchange.yaml +46 -0
  174. package/engines/specs/std_samr.yaml +21 -0
  175. package/engines/specs/tass.yaml +43 -0
  176. package/engines/specs/tavily_free.yaml +56 -0
  177. package/engines/specs/tle_mirror.yaml +21 -0
  178. package/engines/specs/train.yaml +37 -0
  179. package/engines/specs/tvmaze.yaml +20 -0
  180. package/engines/specs/twitter_syndication.yaml +31 -0
  181. package/engines/specs/un_comtrade.yaml +23 -0
  182. package/engines/specs/unpaywall.yaml +54 -0
  183. package/engines/specs/vnpress.yaml +43 -0
  184. package/engines/specs/weather.yaml +32 -0
  185. package/engines/specs/who_don.yaml +21 -0
  186. package/engines/specs/who_gho.yaml +20 -0
  187. package/engines/specs/wikimedia_commons.yaml +34 -0
  188. package/engines/specs/wikisource.yaml +22 -0
  189. package/engines/specs/worms.yaml +19 -0
  190. package/engines/specs/x_trending.yaml +34 -0
  191. package/engines/specs/yna.yaml +43 -0
  192. package/engines/specs/ynet.yaml +43 -0
  193. package/engines/specs/yuanbao.yaml +93 -0
  194. package/engines/specs/zbmath.yaml +40 -0
  195. package/engines/specs/zdic.yaml +22 -0
  196. package/engines/specs/zenodo.yaml +27 -0
  197. package/engines/specs/zhihu_hot_app.yaml +35 -0
  198. package/mcp/clients.yaml +106 -0
  199. package/package.json +55 -12
  200. package/packages/dsh-plugin/README.md +152 -0
  201. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  202. package/packages/dsh-plugin/dsh/client.js +1533 -0
  203. package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
  204. package/packages/dsh-plugin/dsh/index.js +527 -0
  205. package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
  206. package/packages/dsh-plugin/dsh/runner.js +666 -0
  207. package/packages/dsh-plugin/dsh/wide-research.js +878 -0
  208. package/packages/dsh-plugin/package.json +38 -0
  209. package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
  210. package/packages/dsh-plugin/test/client.test.js +406 -0
  211. package/packages/dsh-plugin/test/config.test.js +30 -0
  212. package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
  213. package/packages/dsh-plugin/test/index.test.js +1382 -0
  214. package/references/query-templates.md +96 -0
  215. package/references/query-templates.yaml +118 -0
  216. package/scripts/ab_eval.py +219 -0
  217. package/scripts/ab_eval_p0p1.py +266 -0
  218. package/scripts/adaptive.py +294 -70
  219. package/scripts/answer.py +154 -0
  220. package/scripts/archive_run.py +591 -0
  221. package/scripts/argo_engine_registry.py +172 -0
  222. package/scripts/argo_paths.py +728 -0
  223. package/scripts/article.py +136 -0
  224. package/scripts/article_meta.py +131 -0
  225. package/scripts/arxiv_source.py +201 -0
  226. package/scripts/badcase_ledger.py +252 -0
  227. package/scripts/batch_probe.py +241 -0
  228. package/scripts/benchmark.py +19 -0
  229. package/scripts/bounded_run.py +144 -0
  230. package/scripts/browser_auth.py +323 -0
  231. package/scripts/budget.py +247 -0
  232. package/scripts/budget_cli.py +93 -0
  233. package/scripts/build_release_assets.sh +65 -0
  234. package/scripts/burstiness_detector.py +147 -0
  235. package/scripts/cache.py +1066 -67
  236. package/scripts/cache_guard.py +271 -0
  237. package/scripts/cache_key_vdom.py +56 -0
  238. package/scripts/cache_similarity.py +170 -0
  239. package/scripts/caliber_isolation.py +195 -0
  240. package/scripts/calibrate_lowquality.py +210 -0
  241. package/scripts/candidate_envelope.py +366 -0
  242. package/scripts/chrome_cdp.py +871 -0
  243. package/scripts/ci_drift_gate.py +90 -0
  244. package/scripts/circuit_breaker.py +389 -0
  245. package/scripts/citations.py +334 -0
  246. package/scripts/cite_cli.py +59 -0
  247. package/scripts/cjk_tokens.py +95 -0
  248. package/scripts/clarify.py +47 -24
  249. package/scripts/cli_io.py +240 -0
  250. package/scripts/config.py +683 -45
  251. package/scripts/config_cache.py +402 -0
  252. package/scripts/conn_pool.py +220 -0
  253. package/scripts/content_boundary.py +46 -0
  254. package/scripts/content_security.py +685 -0
  255. package/scripts/content_signals.py +751 -0
  256. package/scripts/crawl.py +74 -35
  257. package/scripts/cross_lingual_trace.py +293 -0
  258. package/scripts/daemon.py +866 -0
  259. package/scripts/digup_bridge.py +214 -0
  260. package/scripts/dispatch_result.py +98 -0
  261. package/scripts/engine_admission.py +366 -0
  262. package/scripts/engine_dispatch.py +982 -0
  263. package/scripts/engine_env.py +539 -0
  264. package/scripts/engine_failure.py +411 -0
  265. package/scripts/engine_families.py +738 -0
  266. package/scripts/engine_policy.py +218 -0
  267. package/scripts/engine_requires.py +190 -0
  268. package/scripts/engine_status.py +322 -0
  269. package/scripts/engine_validate.py +638 -0
  270. package/scripts/engines.py +799 -294
  271. package/scripts/engines_base.py +1521 -0
  272. package/scripts/engines_builders.py +310 -0
  273. package/scripts/engines_builders_batch10.py +204 -0
  274. package/scripts/engines_builders_batch11.py +174 -0
  275. package/scripts/engines_builders_batch12.py +103 -0
  276. package/scripts/engines_builders_batch13.py +340 -0
  277. package/scripts/engines_builders_batch14.py +656 -0
  278. package/scripts/engines_builders_batch15.py +249 -0
  279. package/scripts/engines_builders_batch9.py +1468 -0
  280. package/scripts/engines_builders_cn.py +2283 -0
  281. package/scripts/engines_builders_data.py +2923 -0
  282. package/scripts/engines_builders_data_macro.py +775 -0
  283. package/scripts/engines_builders_feeds.py +255 -0
  284. package/scripts/engines_builders_intl.py +1239 -0
  285. package/scripts/engines_builders_keyless.py +111 -0
  286. package/scripts/engines_builders_search.py +440 -0
  287. package/scripts/engines_builders_tech.py +1359 -0
  288. package/scripts/evidence.py +620 -166
  289. package/scripts/evidence_loop.py +624 -0
  290. package/scripts/evidence_tier.py +298 -0
  291. package/scripts/except_sets.py +25 -0
  292. package/scripts/extract.py +55 -16
  293. package/scripts/fact_align.py +207 -0
  294. package/scripts/fact_crosscheck.py +230 -0
  295. package/scripts/fetch.py +25 -12
  296. package/scripts/fetch_browser.py +87 -0
  297. package/scripts/fetch_jsrun.py +122 -0
  298. package/scripts/fetch_quality.py +225 -0
  299. package/scripts/fetch_render_tinyfish.py +117 -0
  300. package/scripts/fetch_v3.py +1804 -0
  301. package/scripts/focus_extract.py +350 -0
  302. package/scripts/fulltext_store.py +169 -0
  303. package/scripts/fxtwitter_api.py +325 -0
  304. package/scripts/gen_engine_catalog.py +364 -0
  305. package/scripts/gen_native_tools.py +91 -0
  306. package/scripts/gen_routing_snapshot.py +195 -0
  307. package/scripts/health_check.py +140 -402
  308. package/scripts/health_probe.py +23 -8
  309. package/scripts/hot_state.py +134 -0
  310. package/scripts/http_client.py +1053 -0
  311. package/scripts/image_ops.py +430 -0
  312. package/scripts/image_vision/vision_probe.swift +126 -0
  313. package/scripts/install.ps1 +166 -0
  314. package/scripts/install.sh +221 -0
  315. package/scripts/job.py +1186 -0
  316. package/scripts/jump_resolver.py +235 -0
  317. package/scripts/lang_capability.py +150 -0
  318. package/scripts/lang_detect.py +289 -0
  319. package/scripts/lang_matrix_probe.py +329 -0
  320. package/scripts/lang_pref.py +311 -0
  321. package/scripts/link_source.py +340 -0
  322. package/scripts/local_image.py +970 -0
  323. package/scripts/local_seek.py +402 -0
  324. package/scripts/macro_countries.py +97 -0
  325. package/scripts/matrix_search_eval.py +1187 -0
  326. package/scripts/mcp_compact.py +234 -0
  327. package/scripts/mcp_diag.py +8 -3
  328. package/scripts/mcp_handlers.py +492 -0
  329. package/scripts/mcp_handlers_surface.py +157 -0
  330. package/scripts/mcp_launch.sh +124 -0
  331. package/scripts/mcp_local.py +241 -0
  332. package/scripts/mcp_server.py +125 -419
  333. package/scripts/mcp_setup.py +431 -0
  334. package/scripts/mcp_tools.py +331 -0
  335. package/scripts/mcp_transport.py +191 -0
  336. package/scripts/mcp_warm.py +127 -0
  337. package/scripts/min_version_scan.py +335 -0
  338. package/scripts/net_proxy.py +258 -0
  339. package/scripts/network_aware.py +152 -0
  340. package/scripts/output_transparency.py +200 -0
  341. package/scripts/paid_policy.py +98 -0
  342. package/scripts/paper.py +274 -0
  343. package/scripts/pdf_extract.py +518 -0
  344. package/scripts/phrase_filter.py +66 -0
  345. package/scripts/plan.py +422 -0
  346. package/scripts/postprocess_signals.py +150 -0
  347. package/scripts/pre-commit +48 -0
  348. package/scripts/query_enhance.py +157 -0
  349. package/scripts/query_operators.py +196 -0
  350. package/scripts/query_rewriter.py +116 -7
  351. package/scripts/query_signals.py +300 -0
  352. package/scripts/query_templates.py +167 -0
  353. package/scripts/query_understanding.py +411 -0
  354. package/scripts/query_variants.py +298 -0
  355. package/scripts/quota.py +633 -76
  356. package/scripts/rank_signals.py +775 -0
  357. package/scripts/ranking_eval.py +319 -0
  358. package/scripts/readability_extract.py +433 -0
  359. package/scripts/recompute.py +415 -0
  360. package/scripts/recovery.py +616 -0
  361. package/scripts/recovery_l0.py +160 -0
  362. package/scripts/recovery_runner.py +150 -0
  363. package/scripts/redskill/redskill_engine.py +332 -0
  364. package/scripts/regression_p0p1.py +318 -0
  365. package/scripts/replay_eval.py +658 -0
  366. package/scripts/rerank_tfidf.py +225 -0
  367. package/scripts/research.py +607 -403
  368. package/scripts/research_cli.py +361 -0
  369. package/scripts/research_dossier.py +492 -0
  370. package/scripts/research_expand.py +169 -0
  371. package/scripts/research_gates.py +272 -0
  372. package/scripts/research_report.py +209 -0
  373. package/scripts/research_strategy.py +37 -0
  374. package/scripts/research_work_packages.py +195 -0
  375. package/scripts/result_lang.py +311 -0
  376. package/scripts/retry_policy.py +76 -0
  377. package/scripts/robots_guard.py +259 -0
  378. package/scripts/route.py +921 -254
  379. package/scripts/route_bangs.py +40 -0
  380. package/scripts/route_cache.py +243 -0
  381. package/scripts/route_combo.py +917 -0
  382. package/scripts/route_domains.py +341 -0
  383. package/scripts/route_lang.py +605 -0
  384. package/scripts/route_log.py +63 -0
  385. package/scripts/route_policy.py +250 -0
  386. package/scripts/rrf_classic.py +66 -0
  387. package/scripts/screenshot.py +73 -0
  388. package/scripts/search.py +886 -406
  389. package/scripts/search_attribution.py +185 -0
  390. package/scripts/search_benchmark.py +407 -0
  391. package/scripts/search_cli.py +640 -0
  392. package/scripts/search_entry.py +395 -0
  393. package/scripts/search_output.py +690 -0
  394. package/scripts/search_pipeline.py +926 -0
  395. package/scripts/search_rank.py +991 -0
  396. package/scripts/search_types.py +129 -16
  397. package/scripts/seek_locator.py +68 -0
  398. package/scripts/semantic_evidence.py +391 -0
  399. package/scripts/serp_guard.py +336 -0
  400. package/scripts/serp_kill_log.py +74 -0
  401. package/scripts/single_flight.py +120 -0
  402. package/scripts/social_engines/__init__.py +10 -0
  403. package/scripts/social_engines/bilibili_engine.py +131 -0
  404. package/scripts/social_engines/hackernews_engine.py +113 -0
  405. package/scripts/social_engines/reddit_engine.py +172 -0
  406. package/scripts/social_engines/twitter_engine.py +397 -0
  407. package/scripts/social_engines/v2ex_engine.py +101 -0
  408. package/scripts/social_engines/weibo_engine.py +128 -0
  409. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  410. package/scripts/social_engines/zhihu_engine.py +139 -0
  411. package/scripts/social_research.py +201 -0
  412. package/scripts/sogou_weixin_browser.py +318 -0
  413. package/scripts/source_ledger.py +249 -0
  414. package/scripts/stage_timing.py +58 -0
  415. package/scripts/stats_cli.py +107 -0
  416. package/scripts/straggler_policy.py +60 -0
  417. package/scripts/stylometry_detector.py +231 -0
  418. package/scripts/sync_backends.py +353 -0
  419. package/scripts/sync_installs.py +29 -0
  420. package/scripts/template_cli.py +127 -0
  421. package/scripts/text_lexicon.py +72 -0
  422. package/scripts/tfidf_router.py +109 -37
  423. package/scripts/time_utils.py +233 -0
  424. package/scripts/topic_research_profiles.py +643 -0
  425. package/scripts/train.py +737 -0
  426. package/scripts/tweet.py +596 -0
  427. package/scripts/url_canon.py +217 -0
  428. package/scripts/url_optimize.py +56 -0
  429. package/scripts/url_safety.py +236 -0
  430. package/scripts/usage_log.py +116 -0
  431. package/scripts/usage_log_clean.py +113 -0
  432. package/scripts/v2ex_nodes.py +311 -0
  433. package/scripts/variant_recall.py +188 -0
  434. package/scripts/watch.py +205 -0
  435. package/scripts/wechat_article.py +258 -0
  436. package/scripts/wide_research.py +447 -0
  437. package/scripts/wx.py +349 -0
  438. package/scripts/x_article_fetch.py +141 -0
  439. package/scripts/x_search.py +234 -0
  440. package/scripts/x_trending.py +189 -0
  441. package/scripts/yaml_load.py +67 -0
  442. package/scripts/zhihu_quota.py +139 -0
  443. package/sub-skills/ego-search/SKILL.md +253 -0
  444. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  445. package/sub-skills/ego-search/scripts/merge.py +226 -0
  446. package/sub-skills/ego-search/scripts/quality.py +64 -0
  447. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  448. package/sub-skills/ego-search/scripts/safety.py +109 -0
  449. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  450. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  451. package/sub-skills/js-run/SKILL.md +67 -0
  452. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  453. package/sub-skills/js-run/scripts/shim.js +188 -0
  454. package/sub-skills/local-search/SKILL.md +50 -53
  455. package/sub-skills/local-search/bm25.py +186 -0
  456. package/sub-skills/local-search/cli_runner.py +331 -0
  457. package/sub-skills/local-search/config.yaml +152 -55
  458. package/sub-skills/local-search/engine_registry.py +91 -26
  459. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  460. package/sub-skills/local-search/local_search_adapter.py +13 -2
  461. package/sub-skills/local-search/parse_maps.yaml +40 -42
  462. package/sub-skills/local-search/parsers.py +328 -0
  463. package/sub-skills/local-search/search_v3.py +716 -242
  464. package/sub-skills/local-search/smart_router.py +47 -7
  465. package/sub-skills/local-seek/SKILL.md +110 -0
  466. package/sub-skills/local-seek/config/domains.yaml +20 -0
  467. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  468. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  469. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  470. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  471. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  472. package/sub-skills/local-seek/scripts/seek_rank.py +197 -0
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """V2EX 社区搜索引擎(公开网页,零密钥)
3
+
4
+ 复用 argo route 库 v2ex builder 的 HTML 解析逻辑,保持一致 social_engines schema。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ import urllib.parse
12
+ import urllib.request
13
+ from typing import Any
14
+
15
+ # 出口调度唯一入口(issue #13 同类修复):urlopen 不认 config.yaml 的
16
+ # network.proxy,裸用会在「必须经代理才能出网」的环境里整源连不上。
17
+ import os
18
+ import sys
19
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20
+ from net_proxy import open_url # noqa: E402
21
+
22
+
23
+ def _http_get(url: str, timeout: int = 10) -> str:
24
+ req = urllib.request.Request(
25
+ url,
26
+ headers={"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"},
27
+ )
28
+ with open_url(req, timeout=timeout) as resp:
29
+ return resp.read().decode("utf-8", "replace")
30
+
31
+
32
+ def search(query: str, n: int = 5) -> list[dict[str, Any]]:
33
+ """V2EX 站内搜索(q 参数 + 结果页解析)。"""
34
+ if not query or not query.strip():
35
+ return []
36
+ url = f"https://www.v2ex.com/search?q={urllib.parse.quote(query.strip())}"
37
+ try:
38
+ html = _http_get(url)
39
+ except Exception as e:
40
+ # 吞异常返空会让 MCP 社交搜索把网络故障静默报成「平台成功、0 结果」
41
+ # (mcp_handlers 的 err=None 设计依赖引擎抛异常或返 error 占位,
42
+ # 见 tests/test_regression_p0p1.py「零结果应以 errors 提示」契约)。
43
+ # 对齐 zhihu_engine 范式:失败必须带着 error 字段浮上来。
44
+ return [{"error": f"v2ex {type(e).__name__}: {e}", "source": "v2ex"}]
45
+
46
+ results: list[dict[str, Any]] = []
47
+ # 主题列表:<a href="/t/xxxx"> 标题 </a>
48
+ topic_re = re.compile(r'<a[^>]*href="(/t/\d+)[^"]*"[^>]*>(.*?)</a>', re.DOTALL)
49
+ seen = set()
50
+ for m in topic_re.finditer(html):
51
+ path, raw_title = m.group(1), m.group(2)
52
+ if path in seen:
53
+ continue
54
+ seen.add(path)
55
+ title = re.sub(r"<[^>]+>", "", raw_title).strip()
56
+ if not title or len(title) < 2:
57
+ continue
58
+ results.append({
59
+ "title": title[:100] + ("..." if len(title) > 100 else ""),
60
+ "url": f"https://www.v2ex.com{path}",
61
+ "snippet": title[:300],
62
+ "source": "v2ex",
63
+ "score": max(1.0 - len(results) * 0.1, 0.1),
64
+ "social_meta": {
65
+ "platform": "v2ex",
66
+ "content_type": "topic",
67
+ # 剥的是**前缀**,不是字符集:lstrip("/t/") 会连 topic id 里
68
+ # 出现的 't' 一起吃掉(当前 href 正则限定 \d+ 才没暴露)。
69
+ "id": path.removeprefix("/t/"),
70
+ "provider": "v2ex_html",
71
+ },
72
+ })
73
+ if len(results) >= n:
74
+ break
75
+ return results
76
+
77
+
78
+ def main():
79
+ import argparse
80
+ parser = argparse.ArgumentParser(description="V2EX search engine")
81
+ parser.add_argument("action", nargs="?", default="search")
82
+ parser.add_argument("query", nargs="?")
83
+ parser.add_argument("-n", type=int, default=5)
84
+ parser.add_argument("--json", action="store_true")
85
+ args = parser.parse_args()
86
+
87
+ if not args.query:
88
+ print("[]")
89
+ return
90
+ results = search(args.query, args.n)
91
+ if args.json:
92
+ print(json.dumps(results, ensure_ascii=False))
93
+ else:
94
+ for i, r in enumerate(results, 1):
95
+ print(f"### {i}. {r['title']}")
96
+ print(f"- **URL**: {r['url']}")
97
+ print()
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """微博搜索引擎
3
+
4
+ 使用微博公开搜索 API(无需登录可搜索热门内容)。
5
+ """
6
+
7
+ import json
8
+ import time
9
+ import urllib.error
10
+ import urllib.parse
11
+ import urllib.request
12
+
13
+ # 出口调度唯一入口(issue #13 同类修复):urlopen 原生只认标准 HTTP(S)_PROXY
14
+ # 环境变量,**不认** config.yaml 的 network.proxy —— 裸用会在「必须经代理才能
15
+ # 出网」的环境里整源连不上。本文件在 scripts/ 子目录,父目录不在 sys.path 上。
16
+ import os
17
+ import sys
18
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
+ from net_proxy import open_url # noqa: E402
20
+
21
+
22
+ def _http_get_with_retry(url: str, headers: dict, timeout: int = 10, max_retries: int = 2):
23
+ """带重试的 HTTP GET,尊重 429 + Retry-After。
24
+
25
+ 仅使用 stdlib,不引入第三方依赖。
26
+ 返回 (body_bytes, status_code)。失败时抛出最后一次异常。
27
+ """
28
+ for attempt in range(max_retries + 1):
29
+ try:
30
+ req = urllib.request.Request(url, headers=headers)
31
+ with open_url(req, timeout=timeout) as resp:
32
+ return resp.read(), resp.status
33
+ except urllib.error.HTTPError as e:
34
+ if e.code == 429 and attempt < max_retries:
35
+ retry_after = int(e.headers.get("Retry-After", "5"))
36
+ time.sleep(min(retry_after, 30))
37
+ continue
38
+ raise
39
+ except (urllib.error.URLError, OSError):
40
+ if attempt < max_retries:
41
+ time.sleep(2 ** attempt + 0.5)
42
+ continue
43
+ raise
44
+ return b"", 0
45
+
46
+
47
+ def search(query: str, n: int = 5) -> list[dict]:
48
+ """通过微博搜索 API 搜索"""
49
+ encoded = urllib.parse.quote(query)
50
+ url = f"https://m.weibo.cn/api/container/getIndex?containerid=100103type%3D1%26q%3D{encoded}&page_type=searchall"
51
+ headers = {
52
+ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15",
53
+ "Referer": "https://m.weibo.cn"
54
+ }
55
+ try:
56
+ body, _ = _http_get_with_retry(url, headers, timeout=10, max_retries=2)
57
+ data = json.loads(body.decode("utf-8"))
58
+ return _parse_weibo_response(data, n)
59
+ except Exception as e:
60
+ # 吞异常返空会让 MCP 社交搜索把网络故障静默报成「平台成功、0 结果」
61
+ # (mcp_handlers 的 err=None 设计依赖引擎抛异常或返 error 占位,
62
+ # 见 tests/test_regression_p0p1.py「零结果应以 errors 提示」契约)。
63
+ # 对齐 zhihu_engine 范式:失败必须带着 error 字段浮上来。
64
+ return [{"error": f"weibo {type(e).__name__}: {e}", "source": "weibo"}]
65
+
66
+
67
+ def _parse_weibo_response(data: dict, n: int) -> list[dict]:
68
+ """解析微博 API 响应"""
69
+ results = []
70
+ for card in data.get("data", {}).get("cards", []):
71
+ for card_group in card.get("card_group", []):
72
+ mblog = card_group.get("mblog", {})
73
+ if not mblog:
74
+ continue
75
+ text = mblog.get("text", "")
76
+ import re
77
+ text = re.sub(r'<[^>]+>', '', text).strip()
78
+ if len(text) < 10:
79
+ continue
80
+ results.append({
81
+ "title": text[:100] + ("..." if len(text) > 100 else ""),
82
+ "url": f"https://weibo.com/{mblog.get('user', {}).get('id', '')}/{mblog.get('bid', '')}",
83
+ "snippet": text[:300],
84
+ "source": "weibo",
85
+ "score": max(1.0 - len(results) * 0.1, 0.1),
86
+ "social_meta": {
87
+ "platform": "weibo",
88
+ "content_type": "post",
89
+ "author": mblog.get("user", {}).get("screen_name", ""),
90
+ "likes": mblog.get("attitudes_count", 0),
91
+ "reposts": mblog.get("reposts_count", 0),
92
+ "comments": mblog.get("comments_count", 0),
93
+ "verified": mblog.get("user", {}).get("verified", False),
94
+ }
95
+ })
96
+ if len(results) >= n:
97
+ break
98
+ if len(results) >= n:
99
+ break
100
+ return results
101
+
102
+
103
+ def main():
104
+ import argparse
105
+ parser = argparse.ArgumentParser(description="Weibo search engine")
106
+ parser.add_argument("action", nargs="?", default="search")
107
+ parser.add_argument("query", nargs="?")
108
+ parser.add_argument("-n", type=int, default=5)
109
+ parser.add_argument("--json", action="store_true")
110
+ args = parser.parse_args()
111
+
112
+ if not args.query:
113
+ print("[]")
114
+ return
115
+
116
+ results = search(args.query, args.n)
117
+ if args.json:
118
+ print(json.dumps(results, ensure_ascii=False))
119
+ else:
120
+ for i, r in enumerate(results, 1):
121
+ print(f"### {i}. {r['title']}")
122
+ print(f"- **URL**: {r['url']}")
123
+ print(f"- {r['snippet'][:200]}")
124
+ print()
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """小红书搜索引擎
3
+
4
+ 使用 xhs-cli (xiaohongshu-cli) 工具。
5
+ 需要先通过 `xhs login` 登录获取 cookies。
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import subprocess
11
+
12
+
13
+ # 内部子进程超时(秒)。此前硬编码 15s,比调度层的单引擎墙钟预算(10s)还长,
14
+ # 属「内层白跑」:外层 subprocess 会在预算处 kill,但内层已消耗的时间追不回。
15
+ # 收到 8s —— 与 execution.default_timeout 一致,且短于预算,
16
+ # 使内层能在被 kill 前主动超时返回(走 except 分支),行为更干净。
17
+ # 可用 ARGO_XHS_TIMEOUT 覆盖,便于慢网调试。
18
+ _DEFAULT_TIMEOUT = 8
19
+
20
+
21
+ def _subprocess_timeout() -> int:
22
+ try:
23
+ v = int(os.environ.get("ARGO_XHS_TIMEOUT", "").strip())
24
+ return v if v > 0 else _DEFAULT_TIMEOUT
25
+ except (TypeError, ValueError):
26
+ return _DEFAULT_TIMEOUT
27
+
28
+
29
+ def search(query: str, n: int = 5) -> list[dict]:
30
+ """主搜索入口"""
31
+ try:
32
+ result = subprocess.run(
33
+ ["xhs", "search", query],
34
+ capture_output=True, text=True, timeout=_subprocess_timeout(),
35
+ encoding="utf-8", errors="replace", # Windows GBK locale 下 UTF-8 输出会解码崩
36
+ )
37
+ if result.returncode == 0 and result.stdout.strip():
38
+ return _parse_xhs_output(result.stdout, n)
39
+ except (FileNotFoundError, subprocess.TimeoutExpired) as e:
40
+ # CLI 未安装/超时不是「平台 0 结果」:带 error 浮上来,与社交引擎
41
+ # 统一范式一致(zhihu_engine 范式,mcp_handlers 的 err=None 设计
42
+ # 依赖引擎抛异常或返 error 占位)。
43
+ return [{"error": f"xhs CLI {type(e).__name__}", "source": "xiaohongshu"}]
44
+ return []
45
+
46
+
47
+ def _parse_xhs_output(raw: str, n: int) -> list[dict]:
48
+ """解析 xhs-cli 输出"""
49
+ results = []
50
+ try:
51
+ data = json.loads(raw)
52
+ items = data.get("items", data.get("data", []))
53
+ if isinstance(items, dict):
54
+ items = items.get("items", [])
55
+ for item in items[:n]:
56
+ if not isinstance(item, dict):
57
+ continue
58
+ note_card = item.get("note_card", item)
59
+ title = note_card.get("display_title", note_card.get("title", ""))
60
+ desc = note_card.get("desc", "")
61
+ interact = note_card.get("interact_info", {})
62
+
63
+ # URL 可核验性(关键):
64
+ # 小红书强制 xsec_token —— 裸 note_id 拼出的 /explore/<id> 会被
65
+ # 302 拦截,无法打开(实测确认)。因此**优先使用上游返回的原始
66
+ # URL/带 token 的链接**,只有在拿不到时才回退到裸 ID 拼接,
67
+ # 并显式标注 url_verifiable=False,让调用方知道这条链接不可复核
68
+ # (与 V2EX 引擎同一契约:宁可不给链接,也不给打不开的链接)。
69
+ raw_url = (item.get("url") or item.get("note_url")
70
+ or note_card.get("url") or "")
71
+ xsec = (item.get("xsec_token") or note_card.get("xsec_token") or "")
72
+ note_id = item.get("id") or note_card.get("note_id") or ""
73
+ if raw_url:
74
+ url = raw_url
75
+ verifiable = True
76
+ elif note_id and xsec:
77
+ url = (f"https://www.xiaohongshu.com/explore/{note_id}"
78
+ f"?xsec_token={xsec}")
79
+ verifiable = True
80
+ elif note_id:
81
+ url = f"https://www.xiaohongshu.com/explore/{note_id}"
82
+ verifiable = False # 裸 ID 必被 302 拦截
83
+ else:
84
+ continue # 无 ID 无 URL:不可核验,不产出
85
+
86
+ results.append({
87
+ "title": title[:100] + ("..." if len(title) > 100 else ""),
88
+ "url": url,
89
+ "snippet": (desc or title)[:300],
90
+ "source": "xiaohongshu",
91
+ "score": max(1.0 - len(results) * 0.1, 0.1),
92
+ "social_meta": {
93
+ "platform": "xiaohongshu",
94
+ "content_type": "note",
95
+ "author": note_card.get("user", {}).get("nickname", ""),
96
+ "likes": interact.get("liked_count", 0),
97
+ "comments": interact.get("comment_count", 0),
98
+ "collects": interact.get("collected_count", 0),
99
+ "type": note_card.get("type", "normal"), # normal / video
100
+ "note_id": note_id,
101
+ "has_xsec_token": bool(xsec),
102
+ # False = 该链接为裸 ID 拼接,打开会被平台拦截(不可复核)
103
+ "url_verifiable": verifiable,
104
+ }
105
+ })
106
+ except (json.JSONDecodeError, ValueError):
107
+ # 尝试文本解析
108
+ pass
109
+ return results
110
+
111
+
112
+ def main():
113
+ import argparse
114
+ parser = argparse.ArgumentParser(description="Xiaohongshu search engine")
115
+ parser.add_argument("action", nargs="?", default="search")
116
+ parser.add_argument("query", nargs="?")
117
+ parser.add_argument("-n", type=int, default=5)
118
+ parser.add_argument("--json", action="store_true")
119
+ args = parser.parse_args()
120
+
121
+ if not args.query:
122
+ print("[]")
123
+ return
124
+
125
+ results = search(args.query, args.n)
126
+ if args.json:
127
+ print(json.dumps(results, ensure_ascii=False))
128
+ else:
129
+ for i, r in enumerate(results, 1):
130
+ print(f"### {i}. {r['title']}")
131
+ print(f"- **URL**: {r['url']}")
132
+ print(f"- {r['snippet'][:200]}")
133
+ print()
134
+
135
+
136
+ if __name__ == "__main__":
137
+ main()
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """知乎搜索引擎(知乎开放平台 zhihu_search API)
3
+
4
+ 密钥:环境变量 ZHIHU_ACCESS_SECRET(或 ARGO_ZHIHU_ACCESS_SECRET),
5
+ 未配置时返回空列表(不抛错、不静默假成功——error 由调用侧显示)。
6
+ 保持一致 social_engines 统一 schema,sentiment 聚合按 social_meta 直接可用。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import time
14
+ import urllib.parse
15
+ import urllib.request
16
+ from typing import Any
17
+
18
+ # 出口调度唯一入口(issue #13 同类修复):urlopen 不认 config.yaml 的
19
+ # network.proxy,裸用会在「必须经代理才能出网」的环境里整源连不上。
20
+ import sys as _sys
21
+ _sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
22
+ from net_proxy import open_url # noqa: E402
23
+
24
+ ZHIHU_API = "https://developer.zhihu.com/api/v1/content/zhihu_search"
25
+
26
+
27
+ def _secret() -> str:
28
+ # 走 engine_env 规范访问器:os.environ 优先,~/.config/argo/env 热读保底
29
+ # (密钥轮换改文件即生效,无需重启)
30
+ try:
31
+ from engine_env import get_env
32
+ return get_env(["ARGO_ZHIHU_ACCESS_SECRET", "ZHIHU_ACCESS_SECRET"])
33
+ except ImportError:
34
+ return os.environ.get("ZHIHU_ACCESS_SECRET") or os.environ.get("ARGO_ZHIHU_ACCESS_SECRET", "")
35
+
36
+
37
+ def search(query: str, n: int = 5) -> list[dict[str, Any]]:
38
+ """通过知乎开放平台 zhihu_search 搜索站内内容。"""
39
+ if not query or not query.strip():
40
+ return []
41
+ secret = _secret()
42
+ if not secret:
43
+ return []
44
+
45
+ params = urllib.parse.urlencode({
46
+ "Query": query.strip(),
47
+ "Count": str(max(1, min(int(n), 20))),
48
+ "SearchDB": "zhihu",
49
+ })
50
+ url = f"{ZHIHU_API}?{params}"
51
+ headers = {
52
+ "Authorization": f"Bearer {secret}",
53
+ "Content-Type": "application/json",
54
+ "X-Request-Timestamp": str(int(time.time())),
55
+ "User-Agent": "argo-search/2.6 (unified-search@local)",
56
+ }
57
+ try:
58
+ req = urllib.request.Request(url, headers=headers)
59
+ with open_url(req, timeout=10) as resp:
60
+ data = json.loads(resp.read().decode("utf-8", "replace"))
61
+ except urllib.error.HTTPError as e:
62
+ # 密钥无效/过期(401/403)等必须暴露为 auth-failed,而不是静默
63
+ # no-results——调用侧把「没配置」「没结果」区分开才可行动
64
+ return [{"error": f"zhihu API HTTP {e.code}", "source": "zhihu"}]
65
+ except Exception as e:
66
+ return [{"error": f"zhihu API {type(e).__name__}: {e}", "source": "zhihu"}]
67
+
68
+ if data.get("Code") not in (0, None):
69
+ return [{"error": f"zhihu API Code={data.get('Code')} "
70
+ f"{str(data.get('Message') or '')[:100]}",
71
+ "source": "zhihu"}]
72
+
73
+ results: list[dict[str, Any]] = []
74
+ items = (data.get("Data") or {}).get("Items") or []
75
+ for item in items[:n]:
76
+ if not isinstance(item, dict):
77
+ continue
78
+ title = (item.get("Title") or "").strip()
79
+ # API 标题统一带「 - 知乎」尾巴:截断前剥掉(先剥再切,尾巴不占正文)
80
+ if title.endswith(" - 知乎"):
81
+ title = title[: -len(" - 知乎")].rstrip()
82
+ url_ = item.get("Url") or ""
83
+ snippet = (item.get("ContentText") or title or "")[:300]
84
+ if not title and not url_:
85
+ continue
86
+ content_id = str(item.get("ContentID") or "")
87
+ author = item.get("AuthorName") or ""
88
+ likes = item.get("VoteUpCount") or 0
89
+ comments = item.get("CommentCount") or 0
90
+ edit_ts = item.get("EditTime")
91
+ published = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(edit_ts)) if edit_ts else None
92
+
93
+ results.append({
94
+ "title": title[:100] + ("..." if len(title) > 100 else ""),
95
+ "url": url_,
96
+ "snippet": snippet,
97
+ "source": "zhihu",
98
+ "score": max(1.0 - len(results) * 0.1, 0.1),
99
+ "published_at": published,
100
+ "social_meta": {
101
+ "platform": "zhihu",
102
+ "content_type": (item.get("ContentType") or "answer").lower(),
103
+ "id": content_id,
104
+ "author": author,
105
+ "likes": likes,
106
+ "comments": comments,
107
+ "provider": "zhihu_openapi",
108
+ },
109
+ })
110
+ return results
111
+
112
+
113
+ def main():
114
+ import argparse
115
+ parser = argparse.ArgumentParser(description="Zhihu search engine (zhihu_openapi)")
116
+ parser.add_argument("action", nargs="?", default="search")
117
+ parser.add_argument("query", nargs="?")
118
+ parser.add_argument("-n", type=int, default=5)
119
+ parser.add_argument("--json", action="store_true")
120
+ args = parser.parse_args()
121
+
122
+ if not args.query:
123
+ print("[]")
124
+ return
125
+ results = search(args.query, args.n)
126
+ if args.json:
127
+ print(json.dumps(results, ensure_ascii=False))
128
+ else:
129
+ for i, r in enumerate(results, 1):
130
+ meta = r.get("social_meta") or {}
131
+ print(f"### {i}. {r['title']}")
132
+ print(f"- **URL**: {r['url']}")
133
+ print(f"- {r['snippet'][:200]}")
134
+ print(f"- likes={meta.get('likes')} comments={meta.get('comments')} @{meta.get('author', '')}")
135
+ print()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()