docpull 5.5.0__tar.gz → 5.5.1__tar.gz

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 (173) hide show
  1. {docpull-5.5.0/src/docpull.egg-info → docpull-5.5.1}/PKG-INFO +2 -2
  2. {docpull-5.5.0 → docpull-5.5.1}/README.md +1 -1
  3. {docpull-5.5.0 → docpull-5.5.1}/pyproject.toml +1 -1
  4. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/__init__.py +1 -1
  5. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/search.py +24 -0
  6. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/visuals.py +114 -4
  7. {docpull-5.5.0 → docpull-5.5.1/src/docpull.egg-info}/PKG-INFO +2 -2
  8. {docpull-5.5.0 → docpull-5.5.1}/tests/test_ci_policy.py +26 -0
  9. {docpull-5.5.0 → docpull-5.5.1}/tests/test_context_packs.py +59 -0
  10. {docpull-5.5.0 → docpull-5.5.1}/LICENSE +0 -0
  11. {docpull-5.5.0 → docpull-5.5.1}/setup.cfg +0 -0
  12. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/__main__.py +0 -0
  13. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/accounting.py +0 -0
  14. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/auth_cli.py +0 -0
  15. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/benchmark.py +0 -0
  16. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/__init__.py +0 -0
  17. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/frontier.py +0 -0
  18. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/manager.py +0 -0
  19. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cache/streaming_dedup.py +0 -0
  20. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/cli.py +0 -0
  21. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/__init__.py +0 -0
  22. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/brand.py +0 -0
  23. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/cli.py +0 -0
  24. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/common.py +0 -0
  25. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/product.py +0 -0
  26. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/schema_extract.py +0 -0
  27. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/context_packs/styleguide.py +0 -0
  28. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/__init__.py +0 -0
  29. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/chunking.py +0 -0
  30. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/extractor.py +0 -0
  31. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/filings.py +0 -0
  32. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/markdown.py +0 -0
  33. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/protocols.py +0 -0
  34. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/special_cases.py +0 -0
  35. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/conversion/trafilatura_extractor.py +0 -0
  36. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/core/__init__.py +0 -0
  37. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/core/fetcher.py +0 -0
  38. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/__init__.py +0 -0
  39. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/_fetch.py +0 -0
  40. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/composite.py +0 -0
  41. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/contracts.py +0 -0
  42. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/crawler.py +0 -0
  43. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/filters.py +0 -0
  44. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/__init__.py +0 -0
  45. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
  46. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/protocols.py +0 -0
  47. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/link_extractors/static.py +0 -0
  48. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/protocols.py +0 -0
  49. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery/sitemap.py +0 -0
  50. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/discovery_cli.py +0 -0
  51. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/doctor.py +0 -0
  52. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/evidence_pack.py +0 -0
  53. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/exports.py +0 -0
  54. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/fixtures/__init__.py +0 -0
  55. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/fixtures/parallel-search-extract.json +0 -0
  56. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/graph.py +0 -0
  57. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/hosted.py +0 -0
  58. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/__init__.py +0 -0
  59. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/client.py +0 -0
  60. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/protocols.py +0 -0
  61. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/http/rate_limiter.py +0 -0
  62. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/judge.py +0 -0
  63. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/local_workflows.py +0 -0
  64. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/__init__.py +0 -0
  65. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/server.py +0 -0
  66. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/sources.py +0 -0
  67. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/mcp/tools.py +0 -0
  68. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/metadata_extractor.py +0 -0
  69. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/__init__.py +0 -0
  70. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/config.py +0 -0
  71. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/document.py +0 -0
  72. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/events.py +0 -0
  73. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/profiles.py +0 -0
  74. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/models/run.py +0 -0
  75. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/monitor.py +0 -0
  76. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pack_reader.py +0 -0
  77. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pack_tools.py +0 -0
  78. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/parallel_workflows.py +0 -0
  79. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/parity.py +0 -0
  80. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/parity_cli.py +0 -0
  81. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/passk.py +0 -0
  82. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/__init__.py +0 -0
  83. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/base.py +0 -0
  84. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/manifest.py +0 -0
  85. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/__init__.py +0 -0
  86. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/chunk.py +0 -0
  87. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/convert.py +0 -0
  88. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/dedup.py +0 -0
  89. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/fetch.py +0 -0
  90. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/metadata.py +0 -0
  91. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/render.py +0 -0
  92. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save.py +0 -0
  93. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_json.py +0 -0
  94. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
  95. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_okf.py +0 -0
  96. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
  97. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/pipeline/steps/validate.py +0 -0
  98. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/policy.py +0 -0
  99. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/policy_cli.py +0 -0
  100. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/project.py +0 -0
  101. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_adapters.py +0 -0
  102. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_capabilities.py +0 -0
  103. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_cli.py +0 -0
  104. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_keys.py +0 -0
  105. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/provider_probes.py +0 -0
  106. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/py.typed +0 -0
  107. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/rendering.py +0 -0
  108. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/scraper.py +0 -0
  109. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/__init__.py +0 -0
  110. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/download_policy.py +0 -0
  111. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/robots.py +0 -0
  112. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/security/url_validator.py +0 -0
  113. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/server.py +0 -0
  114. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/share.py +0 -0
  115. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/skill_export.py +0 -0
  116. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/source_scoring.py +0 -0
  117. {docpull-5.5.0 → docpull-5.5.1}/src/docpull/time_utils.py +0 -0
  118. {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/SOURCES.txt +0 -0
  119. {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/dependency_links.txt +0 -0
  120. {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/entry_points.txt +0 -0
  121. {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/requires.txt +0 -0
  122. {docpull-5.5.0 → docpull-5.5.1}/src/docpull.egg-info/top_level.txt +0 -0
  123. {docpull-5.5.0 → docpull-5.5.1}/tests/test_accounting.py +0 -0
  124. {docpull-5.5.0 → docpull-5.5.1}/tests/test_auth_cli.py +0 -0
  125. {docpull-5.5.0 → docpull-5.5.1}/tests/test_benchmark.py +0 -0
  126. {docpull-5.5.0 → docpull-5.5.1}/tests/test_cache_conditional_get.py +0 -0
  127. {docpull-5.5.0 → docpull-5.5.1}/tests/test_chunking.py +0 -0
  128. {docpull-5.5.0 → docpull-5.5.1}/tests/test_cli.py +0 -0
  129. {docpull-5.5.0 → docpull-5.5.1}/tests/test_conversion.py +0 -0
  130. {docpull-5.5.0 → docpull-5.5.1}/tests/test_convert_step_new.py +0 -0
  131. {docpull-5.5.0 → docpull-5.5.1}/tests/test_discovery.py +0 -0
  132. {docpull-5.5.0 → docpull-5.5.1}/tests/test_discovery_contracts.py +0 -0
  133. {docpull-5.5.0 → docpull-5.5.1}/tests/test_doctor.py +0 -0
  134. {docpull-5.5.0 → docpull-5.5.1}/tests/test_document_record.py +0 -0
  135. {docpull-5.5.0 → docpull-5.5.1}/tests/test_evidence_pack.py +0 -0
  136. {docpull-5.5.0 → docpull-5.5.1}/tests/test_exports.py +0 -0
  137. {docpull-5.5.0 → docpull-5.5.1}/tests/test_frontier_resume.py +0 -0
  138. {docpull-5.5.0 → docpull-5.5.1}/tests/test_graph.py +0 -0
  139. {docpull-5.5.0 → docpull-5.5.1}/tests/test_hosted.py +0 -0
  140. {docpull-5.5.0 → docpull-5.5.1}/tests/test_integration.py +0 -0
  141. {docpull-5.5.0 → docpull-5.5.1}/tests/test_judge.py +0 -0
  142. {docpull-5.5.0 → docpull-5.5.1}/tests/test_link_extractors.py +0 -0
  143. {docpull-5.5.0 → docpull-5.5.1}/tests/test_local_workflows.py +0 -0
  144. {docpull-5.5.0 → docpull-5.5.1}/tests/test_mcp_server.py +0 -0
  145. {docpull-5.5.0 → docpull-5.5.1}/tests/test_mcp_tools.py +0 -0
  146. {docpull-5.5.0 → docpull-5.5.1}/tests/test_metadata_extractor.py +0 -0
  147. {docpull-5.5.0 → docpull-5.5.1}/tests/test_monitor.py +0 -0
  148. {docpull-5.5.0 → docpull-5.5.1}/tests/test_naming.py +0 -0
  149. {docpull-5.5.0 → docpull-5.5.1}/tests/test_outputs_e2e.py +0 -0
  150. {docpull-5.5.0 → docpull-5.5.1}/tests/test_pack_server.py +0 -0
  151. {docpull-5.5.0 → docpull-5.5.1}/tests/test_pack_tools.py +0 -0
  152. {docpull-5.5.0 → docpull-5.5.1}/tests/test_parallel_workflows.py +0 -0
  153. {docpull-5.5.0 → docpull-5.5.1}/tests/test_parity_workflows.py +0 -0
  154. {docpull-5.5.0 → docpull-5.5.1}/tests/test_passk.py +0 -0
  155. {docpull-5.5.0 → docpull-5.5.1}/tests/test_pipeline.py +0 -0
  156. {docpull-5.5.0 → docpull-5.5.1}/tests/test_policy.py +0 -0
  157. {docpull-5.5.0 → docpull-5.5.1}/tests/test_policy_cli.py +0 -0
  158. {docpull-5.5.0 → docpull-5.5.1}/tests/test_project.py +0 -0
  159. {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_adapters.py +0 -0
  160. {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_cli.py +0 -0
  161. {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_keys.py +0 -0
  162. {docpull-5.5.0 → docpull-5.5.1}/tests/test_provider_probes.py +0 -0
  163. {docpull-5.5.0 → docpull-5.5.1}/tests/test_rate_limiter.py +0 -0
  164. {docpull-5.5.0 → docpull-5.5.1}/tests/test_real_site_regressions.py +0 -0
  165. {docpull-5.5.0 → docpull-5.5.1}/tests/test_rendering.py +0 -0
  166. {docpull-5.5.0 → docpull-5.5.1}/tests/test_save_ndjson.py +0 -0
  167. {docpull-5.5.0 → docpull-5.5.1}/tests/test_save_sqlite.py +0 -0
  168. {docpull-5.5.0 → docpull-5.5.1}/tests/test_security_hardening.py +0 -0
  169. {docpull-5.5.0 → docpull-5.5.1}/tests/test_share.py +0 -0
  170. {docpull-5.5.0 → docpull-5.5.1}/tests/test_special_cases.py +0 -0
  171. {docpull-5.5.0 → docpull-5.5.1}/tests/test_surface_contract.py +0 -0
  172. {docpull-5.5.0 → docpull-5.5.1}/tests/test_time_utils.py +0 -0
  173. {docpull-5.5.0 → docpull-5.5.1}/tests/test_trafilatura_extractor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 5.5.0
3
+ Version: 5.5.1
4
4
  Summary: Keep AI agents synced with changing public docs and web sources
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -115,7 +115,7 @@ Dynamic: license-file
115
115
  **Keep AI agents synced with changing public docs. Browser-free by default.**
116
116
 
117
117
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
118
- [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
118
+ [![PyPI version](https://img.shields.io/pypi/v/docpull.svg?label=package)](https://pypi.org/project/docpull/)
119
119
  [![PyPI downloads](https://img.shields.io/pepy/dt/docpull?label=downloads)](https://pepy.tech/project/docpull)
120
120
  [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
121
121
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
@@ -3,7 +3,7 @@
3
3
  **Keep AI agents synced with changing public docs. Browser-free by default.**
4
4
 
5
5
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
6
- [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
6
+ [![PyPI version](https://img.shields.io/pypi/v/docpull.svg?label=package)](https://pypi.org/project/docpull/)
7
7
  [![PyPI downloads](https://img.shields.io/pepy/dt/docpull?label=downloads)](https://pepy.tech/project/docpull)
8
8
  [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
9
9
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "docpull"
7
- version = "5.5.0"
7
+ version = "5.5.1"
8
8
  dynamic = []
9
9
  description = "Keep AI agents synced with changing public docs and web sources"
10
10
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -14,7 +14,7 @@ Usage:
14
14
  print(event)
15
15
  """
16
16
 
17
- __version__ = "5.5.0"
17
+ __version__ = "5.5.1"
18
18
 
19
19
  from .cache import CacheManager, StreamingDeduplicator
20
20
  from .context_packs import (
@@ -95,6 +95,14 @@ def _build_local_search_pack(
95
95
  output_dir.mkdir(parents=True, exist_ok=True)
96
96
  local = search_local_pack(pack_dir, query, required_domains=required_domains, limit=max_results)
97
97
  results = _normalize_local_results(local)
98
+ replay_config = {
99
+ "query": query,
100
+ "provider": "local",
101
+ "pack_dir": str(pack_dir),
102
+ "include_domains": required_domains,
103
+ "max_results": max_results,
104
+ "scrape": scrape,
105
+ }
98
106
  result_path = output_dir / "search.result.json"
99
107
  results_path = output_dir / "search.results.ndjson"
100
108
  markdown_path = output_dir / "SEARCH.md"
@@ -120,6 +128,7 @@ def _build_local_search_pack(
120
128
  "output_dir": str(output_dir),
121
129
  "query": query,
122
130
  "input": {"pack_dir": str(pack_dir), "scrape": scrape},
131
+ "replay_config": replay_config,
123
132
  "summary": {
124
133
  "result_count": len(results),
125
134
  "max_results": max_results,
@@ -155,6 +164,7 @@ def _build_local_search_pack(
155
164
  "query": query,
156
165
  "summary": payload["summary"],
157
166
  "request_options": payload["request_options"],
167
+ "replay_config": replay_config,
158
168
  "artifacts": {**artifacts, "pack_metadata": artifact_ref(output_dir, pack_path)},
159
169
  },
160
170
  )
@@ -219,6 +229,17 @@ def _build_provider_search_pack(
219
229
  "max_results": max_results,
220
230
  "scrape": scrape,
221
231
  }
232
+ replay_config = {
233
+ "query": query,
234
+ "provider": provider,
235
+ "include_domains": include_domains,
236
+ "exclude_domains": exclude_domains,
237
+ "max_results": max_results,
238
+ "scrape": scrape,
239
+ "dry_run": dry_run,
240
+ "budget": budget,
241
+ "max_estimated_cost": max_estimated_cost,
242
+ }
222
243
  if dry_run or blocked:
223
244
  result_path = output_dir / "search.result.json"
224
245
  results_path = output_dir / "search.results.ndjson"
@@ -245,6 +266,7 @@ def _build_provider_search_pack(
245
266
  "status": "dry_run" if dry_run and not blocked else "blocked_by_budget",
246
267
  "output_dir": str(output_dir),
247
268
  "query": query,
269
+ "replay_config": replay_config,
248
270
  "summary": {
249
271
  "result_count": 0,
250
272
  "estimated_cost_usd": estimated,
@@ -283,6 +305,7 @@ def _build_provider_search_pack(
283
305
  "query": query,
284
306
  "summary": payload["summary"],
285
307
  "request_options": request_options,
308
+ "replay_config": replay_config,
286
309
  "artifacts": payload["artifacts"],
287
310
  },
288
311
  )
@@ -324,6 +347,7 @@ def _build_provider_search_pack(
324
347
  "status": "completed",
325
348
  "output_dir": str(output_dir),
326
349
  "query": query,
350
+ "replay_config": replay_config,
327
351
  "summary": {
328
352
  "result_count": _parallel_result_count(pack_path),
329
353
  "estimated_cost_usd": estimated,
@@ -375,11 +375,121 @@ def _run_screenshot_command(
375
375
  text=True,
376
376
  timeout=45,
377
377
  )
378
+ legacy_error = ""
379
+ if result.returncode == 0:
380
+ try:
381
+ png = _png_from_agent_browser_stdout(result.stdout)
382
+ except ContextPackError as err:
383
+ legacy_error = str(err)
384
+ else:
385
+ output_path.write_bytes(png)
386
+ return _screenshot_payload(
387
+ url=url,
388
+ output_path=output_path,
389
+ viewport=viewport,
390
+ full_page=full_page,
391
+ png=png,
392
+ command=[part if part != url else public_url(url) for part in command],
393
+ )
394
+ else:
395
+ legacy_error = (result.stderr or result.stdout).strip()[:500]
396
+ try:
397
+ png, command = _run_agent_browser_batch_screenshot(
398
+ binary=binary,
399
+ url=url,
400
+ output_path=output_path,
401
+ viewport=viewport,
402
+ full_page=full_page,
403
+ wait_for=wait_for,
404
+ )
405
+ except ContextPackError as err:
406
+ detail = f"{legacy_error}; batch fallback: {err}" if legacy_error else str(err)
407
+ raise ContextPackError(
408
+ f"agent-browser screenshot failed with status {result.returncode}: {detail}"
409
+ ) from err
410
+ return _screenshot_payload(
411
+ url=url,
412
+ output_path=output_path,
413
+ viewport=viewport,
414
+ full_page=full_page,
415
+ png=png,
416
+ command=command,
417
+ )
418
+
419
+
420
+ def _run_agent_browser_batch_screenshot(
421
+ *,
422
+ binary: str,
423
+ url: str,
424
+ output_path: Path,
425
+ viewport: str,
426
+ full_page: bool,
427
+ wait_for: str,
428
+ ) -> tuple[bytes, list[str]]:
429
+ width, height = _viewport_dimensions(viewport)
430
+ wait_ms = {"domcontentloaded": "500", "load": "1000", "networkidle": "2000"}[wait_for]
431
+ screenshot_command = ["screenshot", str(output_path)]
432
+ if full_page:
433
+ screenshot_command.append("--full")
434
+ batch = [
435
+ ["open", url],
436
+ ["set", "viewport", str(width), str(height)],
437
+ ["wait", wait_ms],
438
+ screenshot_command,
439
+ ]
440
+ batch_json = json.dumps(batch, separators=(",", ":"))
441
+ command = [binary, "batch", "--bail", "--json"]
442
+ result = subprocess.run( # nosec B603
443
+ command,
444
+ input=batch_json,
445
+ check=False,
446
+ capture_output=True,
447
+ text=True,
448
+ timeout=45,
449
+ )
378
450
  if result.returncode != 0:
379
451
  detail = (result.stderr or result.stdout).strip()[:500]
380
- raise ContextPackError(f"agent-browser screenshot failed with status {result.returncode}: {detail}")
381
- png = _png_from_agent_browser_stdout(result.stdout)
382
- output_path.write_bytes(png)
452
+ raise ContextPackError(
453
+ f"agent-browser batch screenshot failed with status {result.returncode}: {detail}"
454
+ )
455
+ _validate_agent_browser_batch_stdout(result.stdout)
456
+ if not output_path.exists():
457
+ raise ContextPackError("agent-browser batch screenshot did not write the expected PNG path.")
458
+ png = output_path.read_bytes()
459
+ if not png.startswith(b"\x89PNG\r\n\x1a\n"):
460
+ raise ContextPackError("agent-browser batch screenshot did not write PNG data.")
461
+ public_batch = [[public_url(item) if item == url else item for item in step] for step in batch]
462
+ return png, [*command, json.dumps(public_batch, separators=(",", ":"))]
463
+
464
+
465
+ def _viewport_dimensions(viewport: str) -> tuple[int, int]:
466
+ match = VIEWPORT_RE.fullmatch(viewport)
467
+ if not match:
468
+ raise ContextPackError("viewport must use WIDTHxHEIGHT with positive integer dimensions.")
469
+ return int(match.group("width")), int(match.group("height"))
470
+
471
+
472
+ def _validate_agent_browser_batch_stdout(stdout: str) -> None:
473
+ try:
474
+ payload = json.loads(stdout)
475
+ except json.JSONDecodeError as err:
476
+ raise ContextPackError("agent-browser batch screenshot did not return JSON output.") from err
477
+ if not isinstance(payload, list):
478
+ raise ContextPackError("agent-browser batch screenshot JSON was not a command result array.")
479
+ for item in payload:
480
+ if not isinstance(item, dict) or item.get("success") is not True:
481
+ raise ContextPackError("agent-browser batch screenshot reported a failed command.")
482
+
483
+
484
+ def _screenshot_payload(
485
+ *,
486
+ url: str,
487
+ output_path: Path,
488
+ viewport: str,
489
+ full_page: bool,
490
+ png: bytes,
491
+ command: list[str],
492
+ ) -> dict[str, Any]:
383
493
  return {
384
494
  "schema_version": CONTEXT_PACK_SCHEMA_VERSION,
385
495
  "url": url,
@@ -389,7 +499,7 @@ def _run_screenshot_command(
389
499
  "bytes": len(png),
390
500
  "sha256": hashlib.sha256(png).hexdigest(),
391
501
  "content_type": "image/png",
392
- "command": [part if part != url else public_url(url) for part in command],
502
+ "command": command,
393
503
  }
394
504
 
395
505
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docpull
3
- Version: 5.5.0
3
+ Version: 5.5.1
4
4
  Summary: Keep AI agents synced with changing public docs and web sources
5
5
  Author-email: Zachary Roth <support@raintree.technology>
6
6
  Maintainer-email: Raintree Technology <support@raintree.technology>
@@ -115,7 +115,7 @@ Dynamic: license-file
115
115
  **Keep AI agents synced with changing public docs. Browser-free by default.**
116
116
 
117
117
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
118
- [![PyPI version](https://badge.fury.io/py/docpull.svg)](https://badge.fury.io/py/docpull)
118
+ [![PyPI version](https://img.shields.io/pypi/v/docpull.svg?label=package)](https://pypi.org/project/docpull/)
119
119
  [![PyPI downloads](https://img.shields.io/pepy/dt/docpull?label=downloads)](https://pepy.tech/project/docpull)
120
120
  [![GitHub stars](https://img.shields.io/github/stars/raintree-technology/docpull?style=social)](https://github.com/raintree-technology/docpull/stargazers)
121
121
  [![License: MIT](https://img.shields.io/github/license/raintree-technology/docpull)](https://github.com/raintree-technology/docpull/blob/main/LICENSE)
@@ -124,6 +124,32 @@ def test_publish_workflow_builds_artifact_before_unlocked_release_gates() -> Non
124
124
  assert "needs: [build, release-gates]" in publish_section
125
125
 
126
126
 
127
+ def test_publish_workflow_creates_github_release_after_pypi_publish() -> None:
128
+ publish = (WORKFLOW_DIR / "publish.yml").read_text()
129
+ _, publish_section = publish.split("\n publish:\n", 1)
130
+
131
+ assert "contents: write" in publish_section
132
+ assert "id-token: write" in publish_section
133
+ assert "pypa/gh-action-pypi-publish@release/v1" in publish_section
134
+ assert "name: Create GitHub release" in publish_section
135
+ assert "if: github.event_name == 'push' && github.ref_type == 'tag'" in publish_section
136
+ assert "GH_TOKEN: ${{ github.token }}" in publish_section
137
+ assert 'NOTES_FILE="docs/release-post-v${MINOR_VERSION}.md"' in publish_section
138
+ assert (
139
+ 'gh release edit "$TAG" --title "$RELEASE_TITLE" --latest --notes-file "$NOTES_FILE"'
140
+ in publish_section
141
+ )
142
+ assert 'gh release edit "$TAG" --title "$RELEASE_TITLE" --latest' in publish_section
143
+ assert (
144
+ 'gh release create "$TAG" --title "$RELEASE_TITLE" --verify-tag --latest --notes-file "$NOTES_FILE"'
145
+ in publish_section
146
+ )
147
+ assert (
148
+ 'gh release create "$TAG" --title "$RELEASE_TITLE" --verify-tag --latest --generate-notes'
149
+ in publish_section
150
+ )
151
+
152
+
127
153
  def test_security_and_publish_bandit_scan_scripts() -> None:
128
154
  security = (WORKFLOW_DIR / "security.yml").read_text()
129
155
  publish = (WORKFLOW_DIR / "publish.yml").read_text()
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import base64
5
6
  import json
6
7
  from pathlib import Path
7
8
  from types import SimpleNamespace
@@ -173,8 +174,11 @@ def test_search_pack_local_searches_existing_pack(tmp_path: Path) -> None:
173
174
  payload = build_search_pack("cited JSON", pack_dir=pack, output_dir=tmp_path / "search")
174
175
 
175
176
  assert payload["provider"] == "local"
177
+ assert payload["replay_config"]["pack_dir"] == str(pack)
176
178
  assert payload["summary"]["result_count"] >= 1
177
179
  assert payload["artifacts"]["accounting"] == "run.accounting.json"
180
+ written = json.loads((tmp_path / "search" / "search.pack.json").read_text(encoding="utf-8"))
181
+ assert written["replay_config"]["provider"] == "local"
178
182
  assert (tmp_path / "search" / "search.results.ndjson").exists()
179
183
  assert (tmp_path / "search" / "run.accounting.json").exists()
180
184
 
@@ -189,7 +193,10 @@ def test_search_pack_provider_dry_run_writes_pack_artifacts(tmp_path: Path) -> N
189
193
 
190
194
  assert payload["status"] == "dry_run"
191
195
  assert payload["output_dir"] == str((tmp_path / "search").resolve())
196
+ assert payload["replay_config"]["provider"] == "tavily"
192
197
  assert payload["artifacts"]["source_policy"] == "source_policy.json"
198
+ written = json.loads((tmp_path / "search" / "search.pack.json").read_text(encoding="utf-8"))
199
+ assert written["replay_config"]["dry_run"] is True
193
200
  assert (tmp_path / "search" / "search.pack.json").exists()
194
201
  assert (tmp_path / "search" / "SEARCH.md").exists()
195
202
  assert (tmp_path / "search" / "run.accounting.json").exists()
@@ -203,6 +210,58 @@ def test_screenshot_pack_validates_options_before_renderer_gate(tmp_path: Path)
203
210
  capture_screenshot_pack("https://acme.test", output_dir=tmp_path / "shot", wait_for="sleep")
204
211
 
205
212
 
213
+ def test_screenshot_pack_falls_back_to_agent_browser_batch(
214
+ tmp_path: Path,
215
+ monkeypatch: pytest.MonkeyPatch,
216
+ ) -> None:
217
+ from docpull.context_packs import visuals
218
+
219
+ png = base64.b64decode(
220
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR4nGNgYGBgAAAABQABDQottAAAAABJRU5ErkJggg=="
221
+ )
222
+ calls: list[dict[str, object]] = []
223
+
224
+ def fake_run(command: list[str], **kwargs: object) -> SimpleNamespace:
225
+ calls.append({"command": command, "kwargs": kwargs})
226
+ if "--timeout" in command:
227
+ return SimpleNamespace(
228
+ returncode=1,
229
+ stdout='{"error":"Unknown command: --timeout","success":false}',
230
+ stderr="",
231
+ )
232
+ assert command == ["agent-browser", "batch", "--bail", "--json"]
233
+ batch = json.loads(str(kwargs["input"]))
234
+ assert batch[0] == ["open", "https://acme.test/"]
235
+ assert batch[1] == ["set", "viewport", "800", "600"]
236
+ screenshot_path = Path(batch[-1][1])
237
+ screenshot_path.write_bytes(png)
238
+ return SimpleNamespace(
239
+ returncode=0,
240
+ stdout=json.dumps(
241
+ [{"command": step, "success": True, "result": {}} for step in batch[:-1]]
242
+ + [{"command": batch[-1], "success": True, "result": {"path": str(screenshot_path)}}]
243
+ ),
244
+ stderr="",
245
+ )
246
+
247
+ monkeypatch.setenv("DOCPULL_RENDER_TRUSTED_BROWSER_TARGETS", "1")
248
+ monkeypatch.setattr(visuals.shutil, "which", lambda binary: f"/bin/{binary}")
249
+ monkeypatch.setattr(visuals.subprocess, "run", fake_run)
250
+
251
+ payload = capture_screenshot_pack(
252
+ "https://acme.test",
253
+ output_dir=tmp_path / "shot",
254
+ viewport="800x600",
255
+ agent_browser_binary="agent-browser",
256
+ )
257
+
258
+ assert payload["status"] == "completed"
259
+ assert payload["screenshots"][0]["bytes"] == len(png)
260
+ assert payload["screenshots"][0]["command"][1:4] == ["batch", "--bail", "--json"]
261
+ assert (tmp_path / "shot" / "screenshots" / "page.png").read_bytes() == png
262
+ assert len(calls) == 2
263
+
264
+
206
265
  @pytest.mark.parametrize(
207
266
  "argv",
208
267
  [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes