argo-search 1.0.1 → 2.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +822 -0
  3. package/README.es.md +864 -0
  4. package/README.ja.md +864 -0
  5. package/README.ko.md +864 -0
  6. package/README.md +867 -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/settings-keys.png +0 -0
  12. package/assets/readme/settings-tools.png +0 -0
  13. package/assets/readme/why-better.svg +104 -0
  14. package/assets/readme/workflow.svg +83 -0
  15. package/backends/domain_profiles.json +2308 -96
  16. package/backends/engine_registry.yaml +3314 -503
  17. package/backends/query_synonyms_cn.json +35 -0
  18. package/backends/quota_profiles.json +2409 -161
  19. package/backends/source_types_cn.json +83 -0
  20. package/backends/text_lexicon.json +44 -0
  21. package/bin/argo +703 -0
  22. package/bin/argo.js +71 -39
  23. package/config.yaml +4976 -363
  24. package/cordis.patch.yml +21 -0
  25. package/data/countries.json +40 -0
  26. package/data/lang_matrix/lang_capability.json +842 -0
  27. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  28. package/data/regions_cn.json +1 -0
  29. package/docs/ADDING_NEW_ENGINE.md +184 -0
  30. package/docs/ARGO_INTRO.md +124 -0
  31. package/docs/DATA_SOURCE_GAPS.md +81 -0
  32. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  33. package/docs/ENGINE_CATALOG.md +564 -0
  34. package/docs/MCP_SETUP.md +106 -0
  35. package/docs/PLATFORM_PATHS.md +111 -0
  36. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  37. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  38. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  39. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  40. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  41. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  42. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  43. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  44. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  45. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  46. package/docs/SEARCH_ARCHIVE.md +115 -0
  47. package/docs/assets/architecture.svg +104 -0
  48. package/docs/assets/hero.svg +98 -0
  49. package/docs/engines/bangumi.md +27 -0
  50. package/docs/engines/biorxiv.md +27 -0
  51. package/docs/engines/clawhub.md +27 -0
  52. package/docs/engines/crt_sh.md +27 -0
  53. package/docs/engines/datacite.md +27 -0
  54. package/docs/engines/deps_dev.md +27 -0
  55. package/docs/engines/douban_movie.md +27 -0
  56. package/docs/engines/endoflife.md +27 -0
  57. package/docs/engines/flk_law.md +27 -0
  58. package/docs/engines/fxtwitter.md +27 -0
  59. package/docs/engines/google_news.md +27 -0
  60. package/docs/engines/gov_regulations.md +27 -0
  61. package/docs/engines/guqian.md +27 -0
  62. package/docs/engines/hackernews.md +27 -0
  63. package/docs/engines/iplant.md +27 -0
  64. package/docs/engines/keenable.md +27 -0
  65. package/docs/engines/met_museum.md +27 -0
  66. package/docs/engines/nasa_images.md +27 -0
  67. package/docs/engines/netease_music.md +27 -0
  68. package/docs/engines/npm.md +27 -0
  69. package/docs/engines/nvd.md +27 -0
  70. package/docs/engines/openreview.md +27 -0
  71. package/docs/engines/openstd.md +27 -0
  72. package/docs/engines/parallel_free.md +27 -0
  73. package/docs/engines/people_daily.md +27 -0
  74. package/docs/engines/realtime_index.md +27 -0
  75. package/docs/engines/sec_edgar.md +27 -0
  76. package/docs/engines/seltz.md +27 -0
  77. package/docs/engines/skillsmp.md +27 -0
  78. package/docs/engines/sspai.md +27 -0
  79. package/docs/engines/std_samr.md +27 -0
  80. package/docs/engines/train.md +27 -0
  81. package/docs/engines/un_comtrade.md +27 -0
  82. package/docs/engines/wikisource.md +27 -0
  83. package/docs/engines/zdic.md +27 -0
  84. package/docs/engines/zenodo.md +27 -0
  85. package/docs/engines/zhihu_hot_app.md +27 -0
  86. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  87. package/engines/README.md +10 -0
  88. package/engines/_template_cli.yaml +39 -0
  89. package/engines/_template_http.yaml +37 -0
  90. package/engines/plugins/_template_plugin.py +50 -0
  91. package/engines/specs/_cisa_kev.yaml +54 -0
  92. package/engines/specs/_osv.yaml +63 -0
  93. package/engines/specs/aljazeera.yaml +43 -0
  94. package/engines/specs/antara.yaml +43 -0
  95. package/engines/specs/artic.yaml +20 -0
  96. package/engines/specs/aviation_weather.yaml +39 -0
  97. package/engines/specs/bangumi.yaml +21 -0
  98. package/engines/specs/biorxiv.yaml +24 -0
  99. package/engines/specs/carbon_intensity.yaml +32 -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/deps_dev.yaml +21 -0
  113. package/engines/specs/digup.yaml +36 -0
  114. package/engines/specs/doi.yaml +39 -0
  115. package/engines/specs/douban_movie.yaml +22 -0
  116. package/engines/specs/dw_news.yaml +47 -0
  117. package/engines/specs/egov_law.yaml +21 -0
  118. package/engines/specs/elpais.yaml +43 -0
  119. package/engines/specs/endoflife.yaml +21 -0
  120. package/engines/specs/energy_charts.yaml +21 -0
  121. package/engines/specs/factcheck_org.yaml +52 -0
  122. package/engines/specs/faz.yaml +43 -0
  123. package/engines/specs/federal_register.yaml +57 -0
  124. package/engines/specs/firecrawl.yaml +53 -0
  125. package/engines/specs/flk_law.yaml +24 -0
  126. package/engines/specs/folha.yaml +43 -0
  127. package/engines/specs/france24.yaml +44 -0
  128. package/engines/specs/full_fact.yaml +47 -0
  129. package/engines/specs/fxtwitter.yaml +31 -0
  130. package/engines/specs/gbfs_nyc.yaml +32 -0
  131. package/engines/specs/gdacs.yaml +21 -0
  132. package/engines/specs/gdelt.yaml +64 -0
  133. package/engines/specs/google_news.yaml +22 -0
  134. package/engines/specs/gov_regulations.yaml +38 -0
  135. package/engines/specs/guardian_rss.yaml +59 -0
  136. package/engines/specs/hurriyet.yaml +43 -0
  137. package/engines/specs/iplant.yaml +25 -0
  138. package/engines/specs/jikan.yaml +24 -0
  139. package/engines/specs/jolpica.yaml +20 -0
  140. package/engines/specs/k10plus.yaml +20 -0
  141. package/engines/specs/keenable.yaml +44 -0
  142. package/engines/specs/lefigaro.yaml +43 -0
  143. package/engines/specs/listenbrainz.yaml +19 -0
  144. package/engines/specs/matichon.yaml +43 -0
  145. package/engines/specs/met_museum.yaml +21 -0
  146. package/engines/specs/nasa_images.yaml +45 -0
  147. package/engines/specs/netease_music.yaml +38 -0
  148. package/engines/specs/nhk.yaml +43 -0
  149. package/engines/specs/nhtsa_vpic.yaml +20 -0
  150. package/engines/specs/noaa_swpc.yaml +20 -0
  151. package/engines/specs/nvd.yaml +38 -0
  152. package/engines/specs/obis.yaml +20 -0
  153. package/engines/specs/opencitations.yaml +52 -0
  154. package/engines/specs/openf1.yaml +21 -0
  155. package/engines/specs/openfoodfacts.yaml +45 -0
  156. package/engines/specs/openligadb.yaml +20 -0
  157. package/engines/specs/openreview.yaml +37 -0
  158. package/engines/specs/openstd.yaml +21 -0
  159. package/engines/specs/people_daily.yaml +22 -0
  160. package/engines/specs/realtime_index.yaml +34 -0
  161. package/engines/specs/ror.yaml +20 -0
  162. package/engines/specs/satnogs.yaml +20 -0
  163. package/engines/specs/sec_edgar.yaml +46 -0
  164. package/engines/specs/skillsmp.yaml +50 -0
  165. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  166. package/engines/specs/soilgrids.yaml +25 -0
  167. package/engines/specs/sspai.yaml +36 -0
  168. package/engines/specs/stackexchange.yaml +46 -0
  169. package/engines/specs/std_samr.yaml +21 -0
  170. package/engines/specs/tass.yaml +43 -0
  171. package/engines/specs/tle_mirror.yaml +21 -0
  172. package/engines/specs/train.yaml +37 -0
  173. package/engines/specs/tvmaze.yaml +20 -0
  174. package/engines/specs/twitter_syndication.yaml +31 -0
  175. package/engines/specs/un_comtrade.yaml +23 -0
  176. package/engines/specs/unpaywall.yaml +54 -0
  177. package/engines/specs/vnpress.yaml +43 -0
  178. package/engines/specs/weather.yaml +32 -0
  179. package/engines/specs/who_don.yaml +21 -0
  180. package/engines/specs/who_gho.yaml +20 -0
  181. package/engines/specs/wikimedia_commons.yaml +34 -0
  182. package/engines/specs/wikisource.yaml +22 -0
  183. package/engines/specs/worms.yaml +19 -0
  184. package/engines/specs/x_trending.yaml +34 -0
  185. package/engines/specs/yna.yaml +43 -0
  186. package/engines/specs/ynet.yaml +43 -0
  187. package/engines/specs/zbmath.yaml +40 -0
  188. package/engines/specs/zdic.yaml +22 -0
  189. package/engines/specs/zenodo.yaml +27 -0
  190. package/engines/specs/zhihu_hot_app.yaml +35 -0
  191. package/mcp/clients.yaml +74 -0
  192. package/package.json +51 -12
  193. package/packages/dsh-plugin/README.md +134 -0
  194. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  195. package/packages/dsh-plugin/dsh/client.js +1244 -0
  196. package/packages/dsh-plugin/dsh/index.js +2290 -0
  197. package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
  198. package/packages/dsh-plugin/package.json +38 -0
  199. package/packages/dsh-plugin/test/client.test.js +232 -0
  200. package/packages/dsh-plugin/test/config.test.js +30 -0
  201. package/packages/dsh-plugin/test/index.test.js +1338 -0
  202. package/references/query-templates.md +96 -0
  203. package/references/query-templates.yaml +118 -0
  204. package/scripts/ab_eval.py +219 -0
  205. package/scripts/ab_eval_p0p1.py +266 -0
  206. package/scripts/adaptive.py +271 -70
  207. package/scripts/answer.py +154 -0
  208. package/scripts/archive_run.py +591 -0
  209. package/scripts/argo_engine_registry.py +172 -0
  210. package/scripts/argo_paths.py +728 -0
  211. package/scripts/article.py +136 -0
  212. package/scripts/arxiv_source.py +201 -0
  213. package/scripts/badcase_ledger.py +252 -0
  214. package/scripts/batch_probe.py +241 -0
  215. package/scripts/benchmark.py +19 -0
  216. package/scripts/bounded_run.py +144 -0
  217. package/scripts/browser_auth.py +323 -0
  218. package/scripts/burstiness_detector.py +147 -0
  219. package/scripts/cache.py +1078 -67
  220. package/scripts/cache_guard.py +271 -0
  221. package/scripts/cache_key_vdom.py +56 -0
  222. package/scripts/cache_similarity.py +170 -0
  223. package/scripts/caliber_isolation.py +195 -0
  224. package/scripts/calibrate_lowquality.py +210 -0
  225. package/scripts/candidate_envelope.py +366 -0
  226. package/scripts/chrome_cdp.py +871 -0
  227. package/scripts/circuit_breaker.py +389 -0
  228. package/scripts/citations.py +334 -0
  229. package/scripts/cite_cli.py +59 -0
  230. package/scripts/cjk_tokens.py +95 -0
  231. package/scripts/clarify.py +47 -24
  232. package/scripts/cli_io.py +240 -0
  233. package/scripts/config.py +683 -45
  234. package/scripts/config_cache.py +402 -0
  235. package/scripts/conn_pool.py +220 -0
  236. package/scripts/content_security.py +685 -0
  237. package/scripts/content_signals.py +730 -0
  238. package/scripts/crawl.py +74 -35
  239. package/scripts/daemon.py +866 -0
  240. package/scripts/digup_bridge.py +214 -0
  241. package/scripts/engine_admission.py +366 -0
  242. package/scripts/engine_dispatch.py +1019 -0
  243. package/scripts/engine_env.py +534 -0
  244. package/scripts/engine_failure.py +411 -0
  245. package/scripts/engine_families.py +732 -0
  246. package/scripts/engine_policy.py +218 -0
  247. package/scripts/engine_requires.py +190 -0
  248. package/scripts/engine_status.py +322 -0
  249. package/scripts/engine_validate.py +638 -0
  250. package/scripts/engines.py +778 -294
  251. package/scripts/engines_base.py +1521 -0
  252. package/scripts/engines_builders.py +298 -0
  253. package/scripts/engines_builders_batch10.py +159 -0
  254. package/scripts/engines_builders_batch11.py +174 -0
  255. package/scripts/engines_builders_batch12.py +103 -0
  256. package/scripts/engines_builders_batch13.py +340 -0
  257. package/scripts/engines_builders_batch9.py +1468 -0
  258. package/scripts/engines_builders_cn.py +2283 -0
  259. package/scripts/engines_builders_data.py +2923 -0
  260. package/scripts/engines_builders_data_macro.py +775 -0
  261. package/scripts/engines_builders_feeds.py +255 -0
  262. package/scripts/engines_builders_intl.py +1239 -0
  263. package/scripts/engines_builders_search.py +440 -0
  264. package/scripts/engines_builders_tech.py +1360 -0
  265. package/scripts/evidence.py +588 -162
  266. package/scripts/evidence_loop.py +592 -0
  267. package/scripts/evidence_tier.py +298 -0
  268. package/scripts/except_sets.py +25 -0
  269. package/scripts/extract.py +55 -16
  270. package/scripts/fact_align.py +207 -0
  271. package/scripts/fact_crosscheck.py +230 -0
  272. package/scripts/fetch.py +25 -12
  273. package/scripts/fetch_browser.py +87 -0
  274. package/scripts/fetch_jsrun.py +122 -0
  275. package/scripts/fetch_quality.py +225 -0
  276. package/scripts/fetch_render_tinyfish.py +117 -0
  277. package/scripts/fetch_v3.py +1779 -0
  278. package/scripts/focus_extract.py +350 -0
  279. package/scripts/fulltext_store.py +169 -0
  280. package/scripts/fxtwitter_api.py +325 -0
  281. package/scripts/gen_engine_catalog.py +364 -0
  282. package/scripts/gen_native_tools.py +91 -0
  283. package/scripts/gen_routing_snapshot.py +184 -0
  284. package/scripts/health_check.py +140 -402
  285. package/scripts/health_probe.py +23 -8
  286. package/scripts/hot_state.py +134 -0
  287. package/scripts/http_client.py +1053 -0
  288. package/scripts/image_ops.py +430 -0
  289. package/scripts/image_vision/vision_probe +0 -0
  290. package/scripts/image_vision/vision_probe.swift +126 -0
  291. package/scripts/install.ps1 +148 -0
  292. package/scripts/install.sh +206 -0
  293. package/scripts/job.py +1186 -0
  294. package/scripts/jump_resolver.py +235 -0
  295. package/scripts/lang_capability.py +150 -0
  296. package/scripts/lang_detect.py +289 -0
  297. package/scripts/lang_matrix_probe.py +329 -0
  298. package/scripts/lang_pref.py +311 -0
  299. package/scripts/link_source.py +340 -0
  300. package/scripts/local_engines.py +321 -0
  301. package/scripts/local_image.py +970 -0
  302. package/scripts/local_seek.py +366 -0
  303. package/scripts/macro_countries.py +82 -0
  304. package/scripts/matrix_search_eval.py +1105 -0
  305. package/scripts/mcp_diag.py +8 -3
  306. package/scripts/mcp_handlers.py +1074 -0
  307. package/scripts/mcp_handlers_surface.py +157 -0
  308. package/scripts/mcp_launch.sh +122 -0
  309. package/scripts/mcp_server.py +118 -417
  310. package/scripts/mcp_setup.py +407 -0
  311. package/scripts/mcp_tools.py +330 -0
  312. package/scripts/mcp_transport.py +191 -0
  313. package/scripts/min_version_scan.py +335 -0
  314. package/scripts/net_proxy.py +258 -0
  315. package/scripts/network_aware.py +152 -0
  316. package/scripts/paper.py +274 -0
  317. package/scripts/pdf_extract.py +518 -0
  318. package/scripts/phrase_filter.py +66 -0
  319. package/scripts/plan.py +422 -0
  320. package/scripts/postprocess_signals.py +150 -0
  321. package/scripts/pre-commit +48 -0
  322. package/scripts/query_enhance.py +157 -0
  323. package/scripts/query_operators.py +196 -0
  324. package/scripts/query_rewriter.py +116 -7
  325. package/scripts/query_signals.py +300 -0
  326. package/scripts/query_templates.py +167 -0
  327. package/scripts/query_understanding.py +411 -0
  328. package/scripts/query_variants.py +298 -0
  329. package/scripts/quota.py +488 -76
  330. package/scripts/rank_signals.py +775 -0
  331. package/scripts/ranking_eval.py +319 -0
  332. package/scripts/readability_extract.py +433 -0
  333. package/scripts/recompute.py +415 -0
  334. package/scripts/recovery.py +616 -0
  335. package/scripts/recovery_l0.py +160 -0
  336. package/scripts/redskill/redskill_engine.py +332 -0
  337. package/scripts/regression_p0p1.py +318 -0
  338. package/scripts/replay_eval.py +640 -0
  339. package/scripts/rerank_tfidf.py +225 -0
  340. package/scripts/research.py +591 -403
  341. package/scripts/research_cli.py +361 -0
  342. package/scripts/research_dossier.py +492 -0
  343. package/scripts/research_expand.py +146 -0
  344. package/scripts/research_gates.py +272 -0
  345. package/scripts/research_report.py +209 -0
  346. package/scripts/research_strategy.py +37 -0
  347. package/scripts/research_work_packages.py +195 -0
  348. package/scripts/result_lang.py +311 -0
  349. package/scripts/robots_guard.py +259 -0
  350. package/scripts/route.py +921 -254
  351. package/scripts/route_bangs.py +40 -0
  352. package/scripts/route_cache.py +225 -0
  353. package/scripts/route_combo.py +910 -0
  354. package/scripts/route_domains.py +328 -0
  355. package/scripts/route_lang.py +605 -0
  356. package/scripts/route_log.py +63 -0
  357. package/scripts/route_policy.py +250 -0
  358. package/scripts/rrf_classic.py +66 -0
  359. package/scripts/screenshot.py +73 -0
  360. package/scripts/search.py +897 -405
  361. package/scripts/search_attribution.py +88 -0
  362. package/scripts/search_benchmark.py +407 -0
  363. package/scripts/search_cli.py +627 -0
  364. package/scripts/search_entry.py +389 -0
  365. package/scripts/search_output.py +606 -0
  366. package/scripts/search_pipeline.py +1077 -0
  367. package/scripts/search_rank.py +991 -0
  368. package/scripts/search_types.py +27 -12
  369. package/scripts/seek_locator.py +68 -0
  370. package/scripts/semantic_evidence.py +215 -0
  371. package/scripts/serp_guard.py +228 -0
  372. package/scripts/serp_kill_log.py +51 -0
  373. package/scripts/single_flight.py +120 -0
  374. package/scripts/social_engines/__init__.py +10 -0
  375. package/scripts/social_engines/bilibili_engine.py +131 -0
  376. package/scripts/social_engines/hackernews_engine.py +113 -0
  377. package/scripts/social_engines/reddit_engine.py +172 -0
  378. package/scripts/social_engines/twitter_engine.py +397 -0
  379. package/scripts/social_engines/v2ex_engine.py +101 -0
  380. package/scripts/social_engines/weibo_engine.py +128 -0
  381. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  382. package/scripts/social_engines/zhihu_engine.py +139 -0
  383. package/scripts/social_research.py +201 -0
  384. package/scripts/sogou_weixin_browser.py +318 -0
  385. package/scripts/source_ledger.py +249 -0
  386. package/scripts/stage_timing.py +58 -0
  387. package/scripts/stats_cli.py +95 -0
  388. package/scripts/straggler_policy.py +60 -0
  389. package/scripts/stylometry_detector.py +231 -0
  390. package/scripts/sync_backends.py +353 -0
  391. package/scripts/sync_installs.py +29 -0
  392. package/scripts/template_cli.py +127 -0
  393. package/scripts/text_lexicon.py +72 -0
  394. package/scripts/tfidf_router.py +109 -37
  395. package/scripts/time_utils.py +233 -0
  396. package/scripts/topic_research_profiles.py +643 -0
  397. package/scripts/train.py +737 -0
  398. package/scripts/tweet.py +596 -0
  399. package/scripts/url_canon.py +217 -0
  400. package/scripts/url_optimize.py +56 -0
  401. package/scripts/url_safety.py +236 -0
  402. package/scripts/usage_log.py +116 -0
  403. package/scripts/usage_log_clean.py +113 -0
  404. package/scripts/v2ex_nodes.py +311 -0
  405. package/scripts/variant_recall.py +188 -0
  406. package/scripts/watch.py +205 -0
  407. package/scripts/wechat_article.py +258 -0
  408. package/scripts/wide_research.py +402 -0
  409. package/scripts/wx.py +349 -0
  410. package/scripts/x_article_fetch.py +141 -0
  411. package/scripts/x_search.py +234 -0
  412. package/scripts/x_trending.py +189 -0
  413. package/scripts/yaml_load.py +67 -0
  414. package/sub-skills/ego-search/SKILL.md +253 -0
  415. package/sub-skills/ego-search/references/browser-runtime.md +43 -0
  416. package/sub-skills/ego-search/references/install.md +67 -0
  417. package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
  418. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  419. package/sub-skills/ego-search/scripts/merge.py +226 -0
  420. package/sub-skills/ego-search/scripts/quality.py +64 -0
  421. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  422. package/sub-skills/ego-search/scripts/safety.py +109 -0
  423. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  424. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  425. package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
  426. package/sub-skills/js-run/SKILL.md +67 -0
  427. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  428. package/sub-skills/js-run/scripts/shim.js +188 -0
  429. package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
  430. package/sub-skills/js-run/tests/test_jsrun.py +165 -0
  431. package/sub-skills/local-search/SKILL.md +50 -53
  432. package/sub-skills/local-search/bm25.py +162 -0
  433. package/sub-skills/local-search/config.yaml +152 -55
  434. package/sub-skills/local-search/engine_registry.py +91 -26
  435. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  436. package/sub-skills/local-search/local_search_adapter.py +13 -2
  437. package/sub-skills/local-search/parse_maps.yaml +40 -42
  438. package/sub-skills/local-search/search_v3.py +1182 -65
  439. package/sub-skills/local-search/smart_router.py +47 -7
  440. package/sub-skills/local-seek/SKILL.md +110 -0
  441. package/sub-skills/local-seek/config/domains.yaml +20 -0
  442. package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
  443. package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
  444. package/sub-skills/local-seek/references/git-integration.md +54 -0
  445. package/sub-skills/local-seek/references/remote-handoff.md +46 -0
  446. package/sub-skills/local-seek/references/rg-recipes.md +96 -0
  447. package/sub-skills/local-seek/references/strategies.md +52 -0
  448. package/sub-skills/local-seek/references/structural-search.md +59 -0
  449. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  450. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  451. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  452. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  453. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  454. package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
  455. package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
  456. package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
  457. package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
  458. package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
  459. package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
  460. package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
  461. package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
  462. package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
  463. package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
package/bin/argo ADDED
@@ -0,0 +1,703 @@
1
+ #!/usr/bin/env python3
2
+ """argo — unified search CLI. Usage: argo <subcommand> [args...]
3
+
4
+ 本文件**必须保持 Python 3.8+ 可执行**:它是 entrypoint,会先被
5
+ `/usr/bin/env python3` 解释一次(macOS 上就是 3.9),然后才轮到
6
+ `_pick_python()` 去挑符合 MIN_PYTHON 的解释器。因此这里禁止使用
7
+ `X | None` 之类的 3.10+ 语法——它不是注解解析问题,而是在
8
+ 函数定义求值期就抛 TypeError,让「选解释器」的兜底逻辑根本没机会执行。
9
+ (历史 bug:`def _engine_count() -> int | None:` 让 `argo --help`
10
+ 在系统 python3 下直接崩在 import 阶段。)
11
+
12
+ 这个「entrypoint 必须比目标版本更低」的约束同时说明:本文件自身的语法下限
13
+ (3.8)与受托管脚本的下限(MIN_PYTHON)是两回事,不要合并。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys, os, json, subprocess, shutil, time
19
+
20
+ ARGO_HOME = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
21
+ SCRIPTS = os.path.join(ARGO_HOME, "scripts")
22
+
23
+
24
+ # 解释器探测结果缓存。探测必须在子进程里真 import 依赖才敢用,
25
+ # 实测一次约 80–170 ms,而这是**每次** argo 调用都要付的固定成本。缓存命中时
26
+ # 只做一次「版本 + 依赖存在性」的廉价校验(find_spec 不 import,约 15 ms),
27
+ # 用它挡住解释器被删/降级/换版本的情况。
28
+ #
29
+ # 探测的依赖只认 PyYAML(argo 唯一的必需第三方依赖,install.sh 装它)。
30
+ # 此前还要求 requests——那是历史误留下的幽灵依赖:全仓没有任何
31
+ # `import requests`(真实的可选增强是 curl_cffi,TLS 指纹用,缺了只是降级)。
32
+ # 后果是新机器(照 install.sh 装完)里 6 个候选解释器全部探测失败、缓存
33
+ # 永远不命中,每次调用白付一遍探测。
34
+ #
35
+ # 版本判据另有独立的历史 bug,见 MIN_PYTHON 上方说明:真正在选人并写缓存的
36
+ # 那条路径反而没有版本闸门,3.9 会被选中并固化进缓存。
37
+ # 缓存根的平台惯例与 argo_paths 保持一致(bin/argo 是 entrypoint,不 import
38
+ # scripts/ 里的模块以保持零依赖):Windows 走 LOCALAPPDATA,POSIX 走 XDG。
39
+ def _default_cache_root():
40
+ if os.name == "nt":
41
+ base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
42
+ return os.path.join(base, "unified-search")
43
+ xdg = os.environ.get("XDG_CACHE_HOME", "").strip()
44
+ if xdg:
45
+ return os.path.join(xdg, "unified-search")
46
+ return os.path.expanduser("~/.cache/unified-search")
47
+
48
+
49
+ _PY_CACHE = os.path.join(
50
+ os.environ.get("ARGO_STATE_DIR", "").strip() or _default_cache_root(),
51
+ ".argo_python",
52
+ )
53
+
54
+ # argo 脚本支持的最低 Python 版本。改这里就是改契约,install.sh 与此处必须一致。
55
+ #
56
+ # 为什么是 3.9 而不是 3.10:早期这里写着 3.10,理由是「脚本用了 `X | None` 语法」。
57
+ # 该理由已被实测证伪——全仓 0 处 PEP604 注解,且几乎所有脚本都有
58
+ # `from __future__ import annotations`(注解不求值,3.9 也能解析)。真实的最低
59
+ # 门槛由**语法**决定:逐文件跨解释器 parse 对比后,全仓只剩 job.py 一处越界,
60
+ # 而那处是 PEP 701(3.12+ 的 f-string 内嵌同类引号),已按 3.9 写法改写。
61
+ MIN_PYTHON = (3, 9)
62
+
63
+ # 两个校验脚本只差「怎么确认 PyYAML 可用」,**版本判据必须完全一致**:
64
+ # 廉价档 find_spec(不 import,约 15 ms),严格档真 import(约 80–170 ms)。
65
+ #
66
+ # 历史 bug(2026-09-16 定位):早先 strict=True 那条只有 `import yaml`、**没有版本
67
+ # 判据**,而真正负责选人并写缓存的候选链走的正是 strict=True——于是 3.9 通过探测、
68
+ # 被选中、被写进缓存,此后 `argo job` 永久 SyntaxError,同一缓存下其它子命令却照常
69
+ # 运行(静默跑在不支持的解释器上)。根因是把「版本」和「依赖」两个判据拆到了两条
70
+ # 分支上,谁抄漏一个就少一半闸门。现在两个脚本同源生成,只换 yaml 那一段。
71
+ _PY_MIN_VER = ",".join(str(part) for part in MIN_PYTHON)
72
+ _PY_CHEAP_CHECK = (
73
+ "import sys, importlib.util as u; sys.exit(0 if sys.version_info >= (%s,) "
74
+ "and u.find_spec('yaml') is not None else 1)" % _PY_MIN_VER
75
+ )
76
+ _PY_STRICT_CHECK = (
77
+ "import sys; sys.exit(0 if sys.version_info >= (%s,) else 1); import yaml"
78
+ % _PY_MIN_VER
79
+ )
80
+
81
+
82
+ def _probe_python(py, strict: bool) -> bool:
83
+ """strict=True 真 import 依赖(首次探测);False 只做廉价存在性校验。
84
+
85
+ 两条路径都必须查版本(见 _PY_*_CHECK 的说明):strict 不是「跳过校验」,
86
+ 只是把依赖检查做得更实。少写版本判据会让 3.9 被选中并写进缓存。
87
+
88
+ py 既可以是可执行文件路径,也可以是**参数列表**(Windows 官方启动器
89
+ `py -3` 的形式——它本身不是解释器,要带参数才会选版本)。
90
+ """
91
+ code = _PY_STRICT_CHECK if strict else _PY_CHEAP_CHECK
92
+ argv = [py] if isinstance(py, str) else list(py)
93
+ if not argv:
94
+ return False
95
+ try:
96
+ r = subprocess.run(argv + ["-c", code], capture_output=True, timeout=10)
97
+ return r.returncode == 0
98
+ except Exception:
99
+ return False
100
+
101
+
102
+ def _resolve_launcher(argv: list[str]) -> str:
103
+ """把「启动器 + 参数」(如 Windows 的 `py -3`)解析成真实解释器绝对路径。
104
+
105
+ 缓存与 execv 都只认单个绝对路径,所以必须在探测阶段就把启动器展开一次:
106
+ 否则写进缓存的是 `py -3` 这样的组合,`_cached_python` 的 `os.path.isabs`
107
+ 校验会直接判它不可用,缓存永远不命中。
108
+ """
109
+ try:
110
+ r = subprocess.run(argv + ["-c", "import sys; print(sys.executable)"],
111
+ capture_output=True, text=True, timeout=10,
112
+ encoding="utf-8", errors="replace")
113
+ except Exception:
114
+ return ""
115
+ if r.returncode != 0:
116
+ return ""
117
+ lines = [ln.strip() for ln in (r.stdout or "").splitlines() if ln.strip()]
118
+ p = lines[-1] if lines else ""
119
+ return p if p and os.path.isabs(p) else ""
120
+
121
+
122
+ def _cached_python() -> str:
123
+ """读缓存的解释器绝对路径;不可用/校验不过返回空串。"""
124
+ try:
125
+ with open(_PY_CACHE, "r", encoding="utf-8") as f:
126
+ p = f.read().strip()
127
+ except OSError:
128
+ return ""
129
+ if p and os.path.isabs(p) and os.access(p, os.X_OK) and _probe_python(p, False):
130
+ return p
131
+ return ""
132
+
133
+
134
+ def _remember_python(p: str) -> None:
135
+ """原子写入缓存;失败静默(只读环境下退回每次探测的老行为)。"""
136
+ try:
137
+ d = os.path.dirname(_PY_CACHE)
138
+ if d:
139
+ os.makedirs(d, exist_ok=True)
140
+ tmp = "%s.tmp.%d" % (_PY_CACHE, os.getpid())
141
+ with open(tmp, "w", encoding="utf-8") as f:
142
+ f.write(p)
143
+ os.replace(tmp, _PY_CACHE)
144
+ except OSError:
145
+ pass
146
+
147
+
148
+ def _pick_python() -> str:
149
+ """选择可用解释器:ARGO_PYTHON 优先,其次高版本 python3.x,兜底当前解释器。
150
+
151
+ 逐个探测版本并验证 PyYAML 依赖后选第一个可用的(判据见 _PY_*_CHECK)。
152
+ 候选链里的 `sys.executable` 排在具体版本号之后:它可能是 3.9(macOS 系统
153
+ python3),只有在没有更高版本时才被选中——那时 MIN_PYTHON 仍会兜住,
154
+ 不会静默选一个跑不动的解释器。
155
+
156
+ 探测结果落盘缓存(见 _PY_CACHE):命中时不再起子进程 import 依赖。显式
157
+ 指定 ARGO_PYTHON 时不查也不写缓存——用户的覆盖是权威,不该被缓存遮蔽,
158
+ 也不该把它固化下来。
159
+ """
160
+ env_py = os.environ.get("ARGO_PYTHON", "").strip()
161
+ if env_py:
162
+ p = shutil.which(env_py) if not os.path.isabs(env_py) else env_py
163
+ if p and _probe_python(p, True):
164
+ return p
165
+ else:
166
+ cached = _cached_python()
167
+ if cached:
168
+ return cached
169
+
170
+ cands = ["python3.14", "python3.13", "python3.12",
171
+ "python3.11", "python3.10", sys.executable,
172
+ # Windows:本机没有 `python3` 这个名字(上游 v2.8.5 的社区贡献已经
173
+ # 在 node 启动器里按平台分流,这里是 `bin/argo` 自身的对应分支)。
174
+ # `py` 是官方启动器,`py -3` 取最新 3.x;解析成真实路径后才入库。
175
+ ["py", "-3"], ["py"], "python3", "python", "python3.exe", "python.exe"]
176
+ for cand in cands:
177
+ if isinstance(cand, str):
178
+ if not cand:
179
+ continue
180
+ py = cand if os.path.isabs(cand) else shutil.which(cand)
181
+ else:
182
+ launcher = shutil.which(cand[0])
183
+ py = _resolve_launcher([launcher] + list(cand[1:])) if launcher else ""
184
+ if not py:
185
+ continue
186
+ if _probe_python(py, True):
187
+ if not env_py:
188
+ _remember_python(py)
189
+ return py
190
+ return sys.executable
191
+
192
+
193
+ def _self_capable() -> bool:
194
+ """当前解释器是否已满足 argo 的运行要求(版本 + 依赖),进程内判定。
195
+
196
+ 与 _probe_python 的分工:那个要 spawn 一个 python 进程才问出一句话,
197
+ 这个只查 importlib 的 find_spec,零子进程。
198
+
199
+ 为什么需要它:shebang 是 `#!/usr/bin/env python3`,在装了高版本 python
200
+ 的机器上(如 homebrew 3.14)落到的往往就是 _pick_python 会选中的那个
201
+ 解释器。此时「探测一遍 + execv 重启」纯粹是把同一个解释器再启一次,
202
+ 每次调用白付一次进程启动与一次校验子进程。达标就地执行即可。
203
+
204
+ 依赖口径与 _PY_CHEAP_CHECK / _probe_python 完全一致:只认 PyYAML 这一个
205
+ 必需第三方依赖。这里曾多要求 requests,但全仓没有任何 `import requests`
206
+ (它是早已移除的幽灵依赖,可选增强是 curl_cffi)。多要求一个用户机器上
207
+ 不存在的包,会让本函数在「照 install.sh 只装了 pyyaml」的正常环境里恒为
208
+ False,于是每次调用都被迫重选解释器并 execv 重启——本该省掉的重复启动,
209
+ 在所有终端用户机器上静默失效,只在恰好装了 requests 的开发机上才生效。
210
+ """
211
+ # 版本判据必须与 _PY_*_CHECK 同源(MIN_PYTHON)。这里曾写死 (3, 10),与
212
+ # 探测门槛各写一份——正是「两处各写版本号、改一处漏一处」那个形态,
213
+ # 一旦 MIN_PYTHON 调整就会造成「探测放行但原地执行拒绝」的不一致。
214
+ if sys.version_info[:2] < MIN_PYTHON:
215
+ return False
216
+ import importlib.util as _util
217
+ return _util.find_spec("yaml") is not None
218
+
219
+
220
+ def _tree_tag():
221
+ """本检出目录短指纹(与 scripts/daemon.py._tree_tag 同算法同输入)。
222
+
223
+ daemon 的 socket/pid/指针/自启标记按检出目录隔离(踩雷记录见 daemon.py
224
+ 该函数 docstring):共享状态根下多个 worktree/clone 会互抢 socket、互覆盖
225
+ pid、互踩自启节流。两边都取 sha1(ARGO_HOME realpath)[:12];hashlib 在
226
+ 函数内 import,不进 entrypoint 顶层零 import 纪律路径。
227
+ """
228
+ import hashlib
229
+ return hashlib.sha1(ARGO_HOME.encode("utf-8")).hexdigest()[:12]
230
+
231
+
232
+ def _daemon_env():
233
+ """透传给 daemon 的请求级环境变量(ARGO_* 全量 + 常见密钥命名)。
234
+
235
+ 为什么需要:daemon 启动时同步一次 env 文件,此后按次覆盖
236
+ (`SERPER_API_KEY=x argo search`、`ARGO_LOCAL_RERANK=0 argo search`)
237
+ 在快路径会被静默忽略——同一条命令冷路径生效、热路径不生效。daemon
238
+ 侧请求结束即还原,不会污染后续请求。
239
+ """
240
+ env = {}
241
+ size = 0
242
+ for k, v in os.environ.items():
243
+ if not (k.startswith("ARGO_") or k.endswith(
244
+ ("_KEY", "_TOKEN", "_SECRET", "_PASSWORD", "_APIKEY"))):
245
+ continue
246
+ size += len(k) + len(v)
247
+ if size > 32768:
248
+ return {}
249
+ env[k] = v
250
+ return env
251
+
252
+
253
+ def _clear_autostart_marker(root):
254
+ """删自启节流 marker。仅 daemon 陈尸场景用(见 _daemon_run ECONNREFUSED 分支)。"""
255
+ try:
256
+ os.unlink(os.path.join(root, ".daemon_autostart-" + _tree_tag()))
257
+ except OSError:
258
+ pass
259
+
260
+
261
+ def _daemon_autostart(root, py):
262
+ """指针缺失/失效时后台拉起 daemon(fire-and-forget,不拖慢本次调用)。
263
+
264
+ 2026-10-18 起改为**显式 opt-in**:只有 `ARGO_DAEMON=1` 才后台拉起,
265
+ `argo daemon start` 手动启动的实例照常服务。理由是本机实测的净收益
266
+ 只有中位 33.5ms/笔(缓存命中 41 vs 75ms,网络查询 1–3s 时无感),
267
+ 却要养 20–27MB 常驻进程、866 行生命周期代码与 bind/stale 竞态。
268
+ 首次调用仍走冷路径,下一笔起享受快路径;失败静默(daemon 是加速器
269
+ 不是依赖);60s 节流防每次调用都拉一个;ARGO_NO_DAEMON=1 优先关闭。
270
+ py 必须是 _pick_python/_self_capable 判定的达标解释器(带 PyYAML、版本
271
+ 达标)——用当前解释器盲拉,会在「当前解释器不达标」的机器上起一个跑不动
272
+ search 的 daemon:它每次 run 都以 exit 1 告终,而客户端对「已执行」的响应
273
+ 不回退(与冷路径 execv 升级解释器的行为不符)。
274
+ """
275
+ if os.environ.get("ARGO_DAEMON", "").strip().lower() not in ("1", "true", "yes", "on"):
276
+ return 1
277
+ if os.name == "nt":
278
+ return 1 # Windows 无后台化,别拉(start 在前台阻塞)
279
+ marker = os.path.join(root, ".daemon_autostart-" + _tree_tag())
280
+ try:
281
+ if time.time() - os.stat(marker).st_mtime < 60:
282
+ return 0
283
+ except OSError:
284
+ pass
285
+ try:
286
+ devnull = open(os.devnull, "wb")
287
+ subprocess.Popen(
288
+ [py or sys.executable, os.path.join(SCRIPTS, "daemon.py"), "start"],
289
+ stdout=devnull, stderr=devnull, stdin=devnull,
290
+ start_new_session=True, close_fds=True)
291
+ except Exception:
292
+ return 1
293
+ # 节流 marker 写在 Popen **成功之后**:先写后拉的话,Popen 抛异常时
294
+ # marker 已落盘,之后 60s 不再尝试拉起(2026-10-17 审查发现 5)。
295
+ # 拉起风暴由 daemon start 自身的 stub 互斥 + _alive 检查兜底,不靠它。
296
+ try:
297
+ d = os.path.dirname(marker)
298
+ if d:
299
+ os.makedirs(d, exist_ok=True)
300
+ with open(marker, "w", encoding="utf-8") as f:
301
+ f.write(str(int(time.time())))
302
+ except OSError:
303
+ pass
304
+ return 0
305
+
306
+
307
+ def _daemon_run(rest, py="") -> bool:
308
+ """UDS 快路径:daemon 在位时零 import 直达常驻进程执行 search。
309
+
310
+ 收益来源是消「解释器启动 + engines 全家 import」约 85–100ms/次的固定成本
311
+ (详见 scripts/daemon.py 模块注释),不是快在语言。任何失败——无指针文件、
312
+ 连不上、响应异常、非 ok、daemon 正忙/源码已变更(busy/stale)——都静默
313
+ 返回 False 走常态路径:daemon 是加速器不是依赖,坏了 CLI 照常能跑,这里
314
+ 绝不打印错误(会污染 agent 的 stdout 契约)。
315
+
316
+ ARGO_NO_DAEMON=1 显式关掉;ARGO_DAEMON_TIMEOUT 覆盖客户端等待上限(秒)。
317
+ 指针文件 .daemon_sock-<树指纹> 由 daemon 写在 _default_cache_root() 下(bin 不读
318
+ yaml、不 import scripts 就能算出的位置),内容是真实 socket 路径——两端
319
+ 状态根解析不一致时退化为快路径永不命中,不会错。
320
+ """
321
+ if os.environ.get("ARGO_NO_DAEMON", "").strip().lower() in ("1", "true", "yes"):
322
+ return False
323
+ root = os.environ.get("ARGO_STATE_DIR", "").strip() or _default_cache_root()
324
+ pointer = os.path.join(root, ".daemon_sock-" + _tree_tag())
325
+ sock = ""
326
+ try:
327
+ with open(pointer, encoding="utf-8") as f:
328
+ sock = f.read().strip()
329
+ except OSError:
330
+ _daemon_autostart(root, py) # 首次调用:本次冷跑,后台把 daemon 拉起来
331
+ return False
332
+ if not sock or not os.path.exists(sock):
333
+ _daemon_autostart(root, py)
334
+ return False
335
+ try:
336
+ tmo = float(os.environ.get("ARGO_DAEMON_TIMEOUT", "").strip() or 120)
337
+ except ValueError:
338
+ tmo = 120.0
339
+ try:
340
+ import socket
341
+ req = {"op": "run", "script": "search", "args": rest,
342
+ "cwd": os.getcwd(), "env": _daemon_env(),
343
+ # 客户端自己的等待预算随请求下达:daemon 的发送超时以它为准
344
+ # (服务端的固定值不可能知道用户把 ARGO_DAEMON_TIMEOUT 调到
345
+ # 多大——硬编码 300s 会在 deep 长查询上复发「响应被丢 → 双跑」)。
346
+ "send_timeout": tmo + 5}
347
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
348
+ try:
349
+ s.settimeout(tmo + 5)
350
+ s.connect(sock)
351
+ s.sendall((json.dumps(req) + "\n").encode("utf-8"))
352
+ buf = b""
353
+ while not buf.endswith(b"\n"):
354
+ chunk = s.recv(65536)
355
+ if not chunk:
356
+ break
357
+ buf += chunk
358
+ finally:
359
+ try:
360
+ s.close()
361
+ except OSError:
362
+ pass
363
+ except Exception as e:
364
+ # 整段(含 socket 创建)静默回退:契约是「任何失败不打断 search」,
365
+ # 此前 socket() 在 try 外,创建失败会穿出 main 直接 traceback。
366
+ # 顺带自愈:daemon 被 kill -9/OOM 后 socket 文件还在、connect 恒
367
+ # ECONNREFUSED,而清理陈尸只发生在 daemon 自己的 _alive()/start 里,
368
+ # 客户端从不调用它——不补这一手,快路径会一直失效到有人手跑
369
+ # `argo daemon start/status/stop`(2026-10-12 审查发现 4)。
370
+ #
371
+ # ECONNREFUSED 是「对端明确死亡」的信号(2026-10-17 审查发现 1):
372
+ # 此时若 60s 节流 marker 还新鲜,_daemon_autostart 会静默 return——
373
+ # 既不重拉也不清尸,窗口内每笔 search 反复付 ~2.2s 冷跑。先删
374
+ # marker 再拉,让这次自愈真正发生;防重复拉起由 daemon start 的
375
+ # stub 互斥 + _alive 兜底,不会引发拉起风暴。空闲自杀路径无此问题
376
+ # (daemon 退出时连 marker 一起清,scripts/daemon.py 退役清理段)。
377
+ if isinstance(e, ConnectionRefusedError):
378
+ _clear_autostart_marker(root)
379
+ _daemon_autostart(root, py)
380
+ return False
381
+ if not buf:
382
+ return False
383
+ try:
384
+ resp = json.loads(buf.decode("utf-8", "replace"))
385
+ except ValueError:
386
+ return False
387
+ if not resp.get("ok"):
388
+ if not resp.get("busy"): # busy=daemon 在跑别的,等它自然空出即可
389
+ _daemon_autostart(root, py) # stale/异常:daemon 已退役或不在位,拉新的
390
+ return False
391
+ out = resp.get("stdout") or ""
392
+ err = resp.get("stderr") or ""
393
+ if out:
394
+ sys.stdout.write(out)
395
+ sys.stdout.flush()
396
+ if err:
397
+ sys.stderr.write(err)
398
+ sys.stderr.flush()
399
+ sys.exit(int(resp.get("exit_code") or 0))
400
+
401
+
402
+ def _engine_count():
403
+ """收录的引擎数(config.yaml + engines/specs/*.yaml 合并口径,单一真源)。
404
+
405
+ 只报「收录数」这个便宜且不随环境变的数:以前这里报的是「启用数」,
406
+ 既与文档里的收录数对不上,配置不可读时还回退到一个写死的旧值(87),
407
+ 用户看到的是个假数字。真读不出来就不显示数量,别编。
408
+
409
+ 快路径:读 `state_root/.engine_count`(`<count>\\n<config.yaml mtime_ns>`),
410
+ mtime 匹配 → 直接返回(~2ms);冷启动或 config.yaml 变更 → fallback
411
+ 到完整 load_config()(~30ms)并回填。这样 --help 稳态下省 ~28ms,
412
+ 且不会重蹈「写死 87」的假数字覆辙——缓存键含 config.yaml mtime,
413
+ config 一变就会走完整解析刷新。
414
+ """
415
+ try:
416
+ state_root = (os.environ.get("ARGO_STATE_DIR", "").strip()
417
+ or os.path.expanduser("~/.cache/unified-search"))
418
+ cfg_path = os.path.join(ARGO_HOME, "config.yaml")
419
+ cache_file = os.path.join(state_root, ".engine_count")
420
+ try:
421
+ cfg_mtime = str(os.stat(cfg_path).st_mtime_ns)
422
+ except OSError:
423
+ cfg_mtime = None
424
+ if cfg_mtime:
425
+ try:
426
+ with open(cache_file, encoding="utf-8") as f:
427
+ cached_count, cached_mtime = f.read().split("\n", 1)
428
+ if cached_mtime.strip() == cfg_mtime:
429
+ return int(cached_count) or None
430
+ except (OSError, ValueError):
431
+ pass # 冷启动 / 缓存损坏 → 走完整解析并回填
432
+ sys.path.insert(0, SCRIPTS)
433
+ from config import load_config
434
+ engines = load_config().get("engines") or {}
435
+ n = len([k for k, v in engines.items() if isinstance(v, dict)]) or None
436
+ if n and cfg_mtime:
437
+ try:
438
+ os.makedirs(state_root, exist_ok=True)
439
+ tmp = f"{cache_file}.tmp.{os.getpid()}"
440
+ with open(tmp, "w", encoding="utf-8") as f:
441
+ f.write(f"{n}\n{cfg_mtime}")
442
+ os.replace(tmp, cache_file) # 原子写:与 argo_paths.atomic_write_* 同构
443
+ except OSError:
444
+ pass # 只读环境等:不阻断,下次仍走完整解析
445
+ return n
446
+ except Exception:
447
+ return None
448
+
449
+
450
+ def _version():
451
+ """版本号:唯一真源是 package.json(本文件不存副本,防再添一处漂移点)。
452
+
453
+ 此前 `argo --version` 落进「Unknown subcommand」——连「报版本」这个最
454
+ 基础的环境探针都没有。只在本分支读 package.json(一次性小文件),不碰
455
+ scripts/ 里的重模块。
456
+ """
457
+ try:
458
+ import json
459
+ with open(os.path.join(ARGO_HOME, "package.json"), encoding="utf-8") as f:
460
+ return "argo " + str(json.load(f).get("version") or "unknown")
461
+ except Exception:
462
+ return "argo unknown(package.json 不可读)"
463
+
464
+
465
+ def _usage() -> str:
466
+ n = _engine_count()
467
+ head = f"{n} 个源" if n else "多源"
468
+ # 花括号在 f-string 里是替换字段语法:字面量花括号必须写成 {{ }},
469
+ # 漏写会抛 NameError({status|inject|undo} 曾让 --help 直接崩溃)。
470
+ return f"""argo — 统一搜索与证据核验 CLI(收录 {head})
471
+
472
+ Usage:
473
+ argo search "query" 统一搜索({head},TF-IDF路由+RRF融合)
474
+ argo research "query" 深度研究(问题分解→多源采集→综合报告)
475
+ argo evidence "query" 可信度评估(自动搜索 → 评分,或 pipe 搜索结果)
476
+ argo clarify "query" 意图消歧(歧义检测+意图分类+路由建议)
477
+ argo fetch "url" 抓取单页正文(多级降级:HTTP 内容协商→md 变体回探→TLS 指纹→jina→Parallel→浏览器;--full 读全文存档)
478
+ argo crawl "url" 站点爬取(sitemap/BFS,多页抓取)
479
+ argo extract "url" 结构化提取(表格/Meta/JSON-LD)
480
+ argo article "url" 微信公众号文章全文抓取(标题/正文/图片)
481
+ argo wx "url" 微信文章多级降级抓取(HTTP→CDP→搜狗;推荐)
482
+ argo screenshot "url" 网页截图(--full-page 整页 / --output 指定路径)
483
+ argo auth {{login|status|logout}} 浏览器登录态(自持持久 profile;登录一次抓取带会话)
484
+ argo pdf "url" PDF 正文提取(--pages 1-5 / --password 口令)
485
+ argo paper "arxiv-id" 论文深读(--outline/--section/--latex/--cited-by)
486
+ argo tweet "<url|id>" X 帖子完整打包(正文+串/引用/转发+媒体;--out 落盘)
487
+ argo x-search "query" X 关键词搜索(原生布尔语法 from:/-is:retweet/lang:;ego-browser 通道)
488
+ argo template {{--list|<id>}} 查询模板(命名检索模式,展开并执行;--add 追加词)
489
+ argo answer "query" 直答(Seltz 带引用合成答案,置信口径=引用计数)
490
+ argo watch {{add|check|list|remove}} 观察模式(网页快照与变化检测,cron 可用)
491
+ argo job "query" 招聘岗位多平台聚合(BOSS/猎聘/智联/前程无忧/597/今日招聘)
492
+ argo preflight "url1 url2 ..." 批量 URL 预检(登录墙/已死/已归档分类,--probe 联网探测)
493
+ argo cite "doi" [doi...] DOI → 引用条目(GB/T 7714/顺序编码/APA/BibTeX,--style 可选)
494
+ argo local-image index [目录...] 本地图片建库(Vision;默认关,需 ARGO_LOCAL_IMAGE=1)
495
+ argo local-image search "词" 搜本地图片(--similar-to 给图找相似;--sheet 出联络表)
496
+ argo local-image stats 索引概况
497
+ argo mcp {{status|inject|undo}} 多客户端 MCP 一键注入/诊断/还原
498
+ argo paths [--check] [--migrate] 路径自省 / 本机自检 / 搬历史状态到惯例目录
499
+ argo daemon {{start|status|stop}} 常驻进程(search 自动走 UDS 快路径,消每次 ~85ms 启动+import;ARGO_NO_DAEMON=1 关)
500
+ argo stats [-n N] 使用日志与反馈状态(本地使用日志只读出口)
501
+
502
+ Common flags: --json --mode {{fast|auto|deep|budget}} --max-results N --depth {{fast|balanced|deep}}
503
+
504
+ 本机此刻可用几个源:argo search --list-engines --routable-only
505
+ 逐源费用/密钥/状态:docs/ENGINE_CATALOG.md
506
+ """
507
+
508
+ def _pin_stdio():
509
+ """把本进程 stdout/stderr 的编码钉住(实现只有一处:cli_io.ensure_utf8_stdio)。
510
+
511
+ 为什么在入口而不是模块 import 期:本文件先被系统 `python3` 解释一次、
512
+ 再按需换解释器,**import 期不碰仓库内模块**是既有纪律(见文件头),
513
+ 所以这层薄封装只负责「按需导入 + 兜异常」,判据与实现都留在 cli_io。
514
+
515
+ 为什么必须最早调用:非 UTF-8 控制台(Windows 老终端)或显式
516
+ `PYTHONIOENCODING=ascii` 下,连 `argo --help` 的中文 usage 都会
517
+ UnicodeEncodeError——它在选解释器之前就发生了。
518
+ """
519
+ try:
520
+ sys.path.insert(0, SCRIPTS)
521
+ from cli_io import ensure_utf8_stdio
522
+ ensure_utf8_stdio()
523
+ except Exception:
524
+ # 纯加固:拿不到实现就按原样跑,不影响任何子命令
525
+ pass
526
+
527
+
528
+ def main():
529
+ _pin_stdio()
530
+ if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help", "help"):
531
+ print(_usage())
532
+ # 帮助不是错误:--help/help 正常退出(0),无参数调用才是用法错误(1)
533
+ sys.exit(0 if len(sys.argv) >= 2 else 1)
534
+
535
+ if sys.argv[1] in ("--version", "-v", "version"):
536
+ print(_version())
537
+ sys.exit(0)
538
+
539
+ sub = sys.argv[1]
540
+ rest = sys.argv[2:]
541
+
542
+ # env 文件 → os.environ 同步(只填缺失,不覆盖已有值):子进程按标准
543
+ # os.environ 直读密钥(读取方零改动,含其他 AGT 既有集成),未经
544
+ # mcp_launch.sh 注入的裸 shell 也能拿到。
545
+ #
546
+ # **必须在选解释器之前**:`_pick_python` 认的权威覆盖就是 ARGO_PYTHON,
547
+ # 而它读的是 os.environ——同步排在后面,用户照文档把 ARGO_PYTHON 写进
548
+ # `~/.config/argo/env` 就等于没写。`mcp_launch.sh` 是「先 source 再探测」
549
+ # (line 32 早于 line 67),两个启动器对同一个文件给出不同答案,正是本仓
550
+ # 反复踩过的「装了没通电」。这个变量只在「当前解释器不够用」时才被读到,
551
+ # 而那恰恰是用户会去写它的情形。
552
+ try:
553
+ sys.path.insert(0, SCRIPTS)
554
+ from engine_env import sync_envfile_to_environ
555
+ sync_envfile_to_environ()
556
+ except Exception:
557
+ pass
558
+
559
+ # 达标就地执行;否则按 _pick_python 选解释器并 execv 重启。
560
+ # 判定前置于探测:达标时连 _pick_python 的子进程校验都省掉。
561
+ inplace = _self_capable()
562
+ py = sys.executable if inplace else _pick_python()
563
+
564
+ scripts = {
565
+ "search": ("search.py", ["--json"]),
566
+ "research": ("research.py", ["--json"]),
567
+ "evidence": ("evidence.py", ["--stdin", "--json"]),
568
+ "clarify": ("clarify.py", ["--json"]),
569
+ # fetch 对应 MCP 的 argo_fetch(正文抓取):CLI 与 MCP 工具面保持一一对应,
570
+ # 少一个入口就意味着该能力只能靠 MCP(默认关)或 --engine 绕路才够得着。
571
+ "fetch": ("fetch_v3.py", []),
572
+ "crawl": ("crawl.py", []),
573
+ "extract": ("extract.py", []),
574
+ "article": ("article.py", ["--json"]),
575
+ # wx:微信文章多级降级(HTTP→CDP→搜狗),article 的升级版
576
+ "wx": ("wechat_article.py", ["--json"]),
577
+ # screenshot/pdf 对应 MCP 的 argo_screenshot / argo_pdf:CLI 与 MCP 工具面
578
+ # 保持一一对应,少一个入口就意味着该能力只能靠 MCP(默认关)够得着
579
+ "screenshot": ("screenshot.py", []),
580
+ "pdf": ("pdf_extract.py", []),
581
+ "paper": ("paper.py", []),
582
+ # tweet:X 帖子完整打包(搜索用 argo search;这里是「给定链接吸收整份内容」)。
583
+ # 与 paper/article 同类:站点专用结构化提取各占一个子命令,不进搜索契约。
584
+ "tweet": ("tweet.py", []),
585
+ # answer/watch:MCP 工具面 2026-09-26 补齐(argo_answer/argo_watch),
586
+ # CLI 与 MCP 能力面自此一一对应
587
+ "answer": ("answer.py", ["--json"]),
588
+ "watch": ("watch.py", []),
589
+ # auth:浏览器登录态(A2,持久 profile)。login 开可见窗口扫码/登录一次,
590
+ # 抓取链对已登录站点自动带会话;cookie 由 Chrome 自持加密,argo 不读取。
591
+ # 不进 MCP 工具面:登录是用户的一次性交互动作,不是模型该调的工具。
592
+ "auth": ("browser_auth.py", []),
593
+ "job": ("job.py", []),
594
+ "preflight": ("batch_probe.py", []),
595
+ "mcp": ("mcp_setup.py", []),
596
+ # stats:本地使用日志只读出口(日志与反馈的规范化入口,见 references/usage.md)
597
+ "stats": ("stats_cli.py", ["--json"]),
598
+ # paths:路径自省(状态目录/密钥文件到底解析到了哪里)。支持类问题的
599
+ # 第一问,且不必进 MCP 工具面(与 answer/watch 同类)。
600
+ "paths": ("argo_paths.py", []),
601
+ # daemon:常驻进程管理(start/status/stop)。search 已自动走 UDS 快路径
602
+ # (_daemon_run),这里管生命周期;详见 scripts/daemon.py 模块注释。
603
+ "daemon": ("daemon.py", []),
604
+ # cite:DOI → 引用条目(GB/T 7714/APA/BibTeX,Crossref+OpenAlex,免 key)
605
+ "cite": ("cite_cli.py", []),
606
+ # local-image:本地图片检索(Vision 索引 + 以图找相似 + 联络表)。
607
+ # 网络图走 `argo search`(image_search 域),本地图走这里——两者是
608
+ # 「全网找图」与「在自己素材里找图」的分工,不重叠。
609
+ "local-image": ("local_image.py", []),
610
+ # x-search:X 原生关键词搜索(ego-browser 通道)。X 布尔语法
611
+ # (from: / -is:retweet / min_faves: / lang:)只有 x.com/search 本体认,
612
+ # 通用网页引擎一律忽略——所以要「盯一批官方号 / 按关键词找发布」只能走它。
613
+ # 站点专用通道,与 tweet(单条打包)互补,不进搜索契约(同 paper/article)。
614
+ "x-search": ("x_search.py", []),
615
+ # template:查询模板(命名检索模式 → 展开并转发给 search / x-search)。
616
+ # 注册表 references/query-templates.yaml,用户可加 ~/.config/argo/query-templates.yaml。
617
+ "template": ("template_cli.py", []),
618
+ }
619
+
620
+ if sub not in scripts:
621
+ print(f"Unknown subcommand: {sub}", file=sys.stderr)
622
+ print(_usage(), file=sys.stderr)
623
+ sys.exit(1)
624
+
625
+ script, defaults = scripts[sub]
626
+ path = os.path.join(SCRIPTS, script)
627
+
628
+ # evidence: auto search → evidence pipeline (when no stdin data)
629
+ # 用法查询(-h/--help)不走这条管道判定:它既没有查询词也没有 stdin,
630
+ # 会被当成「用错了」报错并 exit 1——查用法却拿到失败码,脚本里
631
+ # `argo evidence --help || fallback` 会走错分支(2026-09-15 实测)。
632
+ # 交给通用执行路径,由 evidence.py 自己的 argparse 输出帮助并 exit 0。
633
+ if sub == "evidence" and not any(a in ("-h", "--help") for a in rest):
634
+ # 判据必须是 fd 类型而非 isatty():非交互环境(脚本/CI/cron/agent)的
635
+ # 空 stdin 也不是 tty,用 isatty 会把「无数据」判成「有数据」,让
636
+ # `argo evidence "query"` 走进读管道分支后崩在 json.load 上。
637
+ sys.path.insert(0, SCRIPTS)
638
+ try:
639
+ from cli_io import read_stdin_if_piped
640
+ except ImportError: # 脚本目录不可导入时按「无管道数据」处理,不猜
641
+ read_stdin_if_piped = lambda: "" # type: ignore[assignment]
642
+ stdin_text = read_stdin_if_piped()
643
+ query = rest[0] if rest and not rest[0].startswith("-") else ""
644
+
645
+ if not stdin_text.strip() and query:
646
+ # 自动搜索 → 管道 → evidence
647
+ search_path = os.path.join(SCRIPTS, "search.py")
648
+ search_cmd = [py, search_path, query, "--json",
649
+ "--mode", "fast", "--max-results", "10"]
650
+ result = subprocess.run(search_cmd, capture_output=True, text=True, timeout=30,
651
+ encoding="utf-8", errors="replace")
652
+ if result.returncode != 0:
653
+ print(result.stderr, file=sys.stderr)
654
+ sys.exit(result.returncode)
655
+ stdin_text = result.stdout
656
+
657
+ if stdin_text.strip():
658
+ # 管道模式:stdin → evidence(内容已读出,用 input= 转发给子进程)
659
+ evidence_args = defaults + rest
660
+ proc = subprocess.run([py, path] + evidence_args, input=stdin_text,
661
+ capture_output=True, text=True, timeout=60,
662
+ encoding="utf-8", errors="replace")
663
+ print(proc.stdout, end="")
664
+ if proc.stderr:
665
+ print(proc.stderr, file=sys.stderr)
666
+ sys.exit(proc.returncode)
667
+
668
+ print("evidence 需要查询词。用法:argo evidence \"query\"", file=sys.stderr)
669
+ sys.exit(1)
670
+
671
+ # --explain 与 --json 冲突(explain 输出纯文本干扰 JSON),自动去 --json
672
+ if "--explain" in rest and "--json" in defaults:
673
+ defaults.remove("--json")
674
+
675
+ args = defaults + rest
676
+
677
+ # daemon 快路径:search 是被 agent 循环调用最密的子命令,「解释器启动 +
678
+ # engines 全家 import」~85ms 固定成本放大最狠。命中常驻进程时把拼好的
679
+ # args 原样发过去(daemon 侧 runpy 语义与这里的原地路径一字不差),本地
680
+ # 只付 stdio/env 同步/_self_capable 约 20ms。--help 不走快路径(argparse
681
+ # 本地直接答)。任何失败在 _daemon_run 内部静默退回常态,绝不污染输出。
682
+ if sub == "search" and not any(a in ("-h", "--help") for a in rest):
683
+ if _daemon_run(args, py):
684
+ return
685
+
686
+ if inplace:
687
+ # 原地执行:省一次解释器启动,并复用本进程已 import 的模块。
688
+ # runpy 会正确设置 __file__ / __name__,目标脚本的
689
+ # `if __name__ == "__main__"` 分支照常触发,sys.exit 照常传播。
690
+ #
691
+ # run_module 而非 run_path:run_path 把源文件当「无名脚本」执行,每次
692
+ # 重新编译(实测 search.py 113KB ≈ 13ms),`__pycache__` 永不命中;
693
+ # run_module 走 importlib 的缓存校验,同一份源码只在首次或改动后编译。
694
+ # SCRIPTS 已在 sys.path 首位,模块名即去掉 .py 的文件名。
695
+ sys.argv = [path] + args
696
+ sys.path.insert(0, SCRIPTS)
697
+ import runpy
698
+ runpy.run_module(script[:-3], run_name="__main__")
699
+ return
700
+ os.execv(py, [py, path] + args)
701
+
702
+ if __name__ == "__main__":
703
+ main()