bc-cli 0.2.0__tar.gz → 0.3.0__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 (211) hide show
  1. {bc_cli-0.2.0 → bc_cli-0.3.0}/.gitignore +3 -0
  2. {bc_cli-0.2.0 → bc_cli-0.3.0}/CHANGELOG.md +17 -0
  3. {bc_cli-0.2.0 → bc_cli-0.3.0}/PKG-INFO +14 -1
  4. bc_cli-0.3.0/docs/extraction.md +236 -0
  5. bc_cli-0.3.0/docs/plans/team-deployment.md +309 -0
  6. bc_cli-0.3.0/examples/extract/purchase_invoice_lines.yaml +61 -0
  7. {bc_cli-0.2.0 → bc_cli-0.3.0}/pyproject.toml +15 -2
  8. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/__init__.py +2 -0
  9. bc_cli-0.3.0/src/bcli/bundle/__init__.py +47 -0
  10. bc_cli-0.3.0/src/bcli/bundle/_apply.py +323 -0
  11. bc_cli-0.3.0/src/bcli/bundle/_fetch.py +197 -0
  12. bc_cli-0.3.0/src/bcli/bundle/_manifest.py +127 -0
  13. bc_cli-0.3.0/src/bcli/bundle/_publish.py +110 -0
  14. bc_cli-0.3.0/src/bcli/bundle/_verify.py +166 -0
  15. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/_model.py +48 -0
  16. bc_cli-0.3.0/src/bcli/diagnostics/__init__.py +26 -0
  17. bc_cli-0.3.0/src/bcli/diagnostics/_checks.py +510 -0
  18. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/errors.py +4 -0
  19. bc_cli-0.3.0/src/bcli/extract/__init__.py +43 -0
  20. bc_cli-0.3.0/src/bcli/extract/_claude.py +248 -0
  21. bc_cli-0.3.0/src/bcli/extract/_factory.py +106 -0
  22. bc_cli-0.3.0/src/bcli/extract/_openai.py +318 -0
  23. bc_cli-0.3.0/src/bcli/extract/_pdf.py +68 -0
  24. bc_cli-0.3.0/src/bcli/extract/_protocol.py +97 -0
  25. bc_cli-0.3.0/src/bcli/extract/_schema.py +208 -0
  26. bc_cli-0.3.0/src/bcli/extract/_yaml_writer.py +173 -0
  27. bc_cli-0.3.0/src/bcli/workflow/_query_search.py +154 -0
  28. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/_resolver.py +4 -2
  29. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_state.py +5 -0
  30. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/app.py +47 -2
  31. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/config_cmd.py +10 -0
  32. bc_cli-0.3.0/src/bcli_cli/commands/doctor_cmd.py +171 -0
  33. bc_cli-0.3.0/src/bcli_cli/commands/extract_cmd.py +193 -0
  34. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/get_cmd.py +2 -1
  35. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/query_cmd.py +260 -20
  36. bc_cli-0.3.0/src/bcli_cli/commands/refresh_cmd.py +298 -0
  37. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/output/_formatters.py +103 -3
  38. bc_cli-0.3.0/tests/test_bundle/test_bundle_roundtrip.py +394 -0
  39. bc_cli-0.3.0/tests/test_cli/test_pipe_handling.py +65 -0
  40. bc_cli-0.3.0/tests/test_cli/test_records_format.py +137 -0
  41. bc_cli-0.3.0/tests/test_diagnostics/test_checks.py +306 -0
  42. bc_cli-0.3.0/tests/test_extract/test_claude.py +224 -0
  43. bc_cli-0.3.0/tests/test_extract/test_factory.py +131 -0
  44. bc_cli-0.3.0/tests/test_extract/test_openai.py +303 -0
  45. bc_cli-0.3.0/tests/test_extract/test_pdf.py +53 -0
  46. bc_cli-0.3.0/tests/test_extract/test_schema.py +192 -0
  47. bc_cli-0.3.0/tests/test_extract/test_yaml_writer.py +149 -0
  48. bc_cli-0.3.0/tests/test_telemetry/__init__.py +0 -0
  49. bc_cli-0.3.0/tests/test_url/__init__.py +0 -0
  50. bc_cli-0.3.0/tests/test_workflow/__init__.py +0 -0
  51. bc_cli-0.3.0/tests/test_workflow/test_query_search.py +139 -0
  52. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_resolver.py +10 -0
  53. {bc_cli-0.2.0 → bc_cli-0.3.0}/uv.lock +201 -2
  54. {bc_cli-0.2.0 → bc_cli-0.3.0}/.env.example +0 -0
  55. {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  56. {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  57. {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  58. {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  59. {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/workflows/tests.yml +0 -0
  60. {bc_cli-0.2.0 → bc_cli-0.3.0}/AGENTS.md +0 -0
  61. {bc_cli-0.2.0 → bc_cli-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  62. {bc_cli-0.2.0 → bc_cli-0.3.0}/CONTRIBUTING.md +0 -0
  63. {bc_cli-0.2.0 → bc_cli-0.3.0}/LICENSE +0 -0
  64. {bc_cli-0.2.0 → bc_cli-0.3.0}/NOTICE +0 -0
  65. {bc_cli-0.2.0 → bc_cli-0.3.0}/README.md +0 -0
  66. {bc_cli-0.2.0 → bc_cli-0.3.0}/SECURITY.md +0 -0
  67. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/authentication.md +0 -0
  68. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/batch-operations.md +0 -0
  69. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/business-central-admin-setup.md +0 -0
  70. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/command-reference.md +0 -0
  71. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/configuration.md +0 -0
  72. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/contributing.md +0 -0
  73. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/custom-apis.md +0 -0
  74. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/demo-setup.md +0 -0
  75. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/getting-started.md +0 -0
  76. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/mcp-server.md +0 -0
  77. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/multi-company.md +0 -0
  78. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/querying.md +0 -0
  79. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/saved-queries.md +0 -0
  80. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/sdk-usage.md +0 -0
  81. {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/write-operations.md +0 -0
  82. {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/ap-monthly-review.yaml +0 -0
  83. {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/attach-purchase-invoice-pdf.yaml +0 -0
  84. {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/create-purchase-invoice.yaml +0 -0
  85. {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/month-end-cronus.yaml +0 -0
  86. {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/queries/sample.yaml +0 -0
  87. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/_url.py +0 -0
  88. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/_version.py +0 -0
  89. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/__init__.py +0 -0
  90. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/_factory.py +0 -0
  91. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/_protocol.py +0 -0
  92. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/_redact.py +0 -0
  93. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/__init__.py +0 -0
  94. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_base.py +0 -0
  95. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_browser.py +0 -0
  96. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_credentials.py +0 -0
  97. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_device_code.py +0 -0
  98. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_secure_io.py +0 -0
  99. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_token_cache.py +0 -0
  100. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/__init__.py +0 -0
  101. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_async.py +0 -0
  102. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_safety.py +0 -0
  103. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_sync.py +0 -0
  104. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_transport.py +0 -0
  105. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/__init__.py +0 -0
  106. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/_defaults.py +0 -0
  107. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/_loader.py +0 -0
  108. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/__init__.py +0 -0
  109. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_auth.py +0 -0
  110. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_bridge.py +0 -0
  111. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_client.py +0 -0
  112. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_generic.py +0 -0
  113. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_polaris.py +0 -0
  114. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_stampers.py +0 -0
  115. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/__init__.py +0 -0
  116. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_escape.py +0 -0
  117. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_filter_fields.py +0 -0
  118. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_pagination.py +0 -0
  119. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_query.py +0 -0
  120. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_response.py +0 -0
  121. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/py.typed +0 -0
  122. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/__init__.py +0 -0
  123. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/_importers.py +0 -0
  124. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/_registry.py +0 -0
  125. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/_schema.py +0 -0
  126. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/standard_v2.json +0 -0
  127. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/__init__.py +0 -0
  128. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/_azure_monitor.py +0 -0
  129. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/_factory.py +0 -0
  130. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/_protocol.py +0 -0
  131. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/events.py +0 -0
  132. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/__init__.py +0 -0
  133. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/_loader.py +0 -0
  134. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/_models.py +0 -0
  135. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/__init__.py +0 -0
  136. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_audit_wrap.py +0 -0
  137. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_dry_run.py +0 -0
  138. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_safety.py +0 -0
  139. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_url_resolve.py +0 -0
  140. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/__init__.py +0 -0
  141. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/attach_cmd.py +0 -0
  142. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/auth_cmd.py +0 -0
  143. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/batch_cmd.py +0 -0
  144. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/company_cmd.py +0 -0
  145. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/context_cmd.py +0 -0
  146. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/delete_cmd.py +0 -0
  147. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/endpoint_cmd.py +0 -0
  148. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/env_cmd.py +0 -0
  149. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/etl_cmd.py +0 -0
  150. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/patch_cmd.py +0 -0
  151. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/post_cmd.py +0 -0
  152. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/registry_cmd.py +0 -0
  153. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/test_cmd.py +0 -0
  154. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/output/__init__.py +0 -0
  155. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/output/_display.py +0 -0
  156. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/__init__.py +0 -0
  157. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/__main__.py +0 -0
  158. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/_runner.py +0 -0
  159. {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/_server.py +0 -0
  160. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/conftest.py +0 -0
  161. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/fixtures/sample_postman_collection.json +0 -0
  162. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/__init__.py +0 -0
  163. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/test_factory.py +0 -0
  164. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/test_redact.py +0 -0
  165. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/test_sink.py +0 -0
  166. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_auth/__init__.py +0 -0
  167. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_auth/test_browser_auth.py +0 -0
  168. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_auth/test_secure_io.py +0 -0
  169. {bc_cli-0.2.0/tests/test_cli → bc_cli-0.3.0/tests/test_bundle}/__init__.py +0 -0
  170. {bc_cli-0.2.0/tests/test_client → bc_cli-0.3.0/tests/test_cli}/__init__.py +0 -0
  171. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_audit_wrap.py +0 -0
  172. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_batch_safety.py +0 -0
  173. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_company_cmd.py +0 -0
  174. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_config_cmd.py +0 -0
  175. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_dry_run.py +0 -0
  176. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_output_format.py +0 -0
  177. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_query_cmd.py +0 -0
  178. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_safety.py +0 -0
  179. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_state.py +0 -0
  180. {bc_cli-0.2.0/tests/test_config → bc_cli-0.3.0/tests/test_client}/__init__.py +0 -0
  181. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_resolve_url.py +0 -0
  182. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_safety.py +0 -0
  183. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_transport.py +0 -0
  184. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_upload_attachment.py +0 -0
  185. {bc_cli-0.2.0/tests/test_etl → bc_cli-0.3.0/tests/test_config}/__init__.py +0 -0
  186. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_config/test_config.py +0 -0
  187. {bc_cli-0.2.0/tests/test_mcp → bc_cli-0.3.0/tests/test_diagnostics}/__init__.py +0 -0
  188. {bc_cli-0.2.0/tests/test_odata → bc_cli-0.3.0/tests/test_etl}/__init__.py +0 -0
  189. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_etl/test_bridge.py +0 -0
  190. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_etl/test_generic.py +0 -0
  191. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_etl/test_stampers.py +0 -0
  192. {bc_cli-0.2.0/tests/test_registry → bc_cli-0.3.0/tests/test_extract}/__init__.py +0 -0
  193. {bc_cli-0.2.0/tests/test_telemetry → bc_cli-0.3.0/tests/test_mcp}/__init__.py +0 -0
  194. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_mcp/test_runner.py +0 -0
  195. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_mcp/test_server_tools.py +0 -0
  196. {bc_cli-0.2.0/tests/test_url → bc_cli-0.3.0/tests/test_odata}/__init__.py +0 -0
  197. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_odata/test_escape.py +0 -0
  198. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_odata/test_filter_fields.py +0 -0
  199. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_odata/test_query.py +0 -0
  200. {bc_cli-0.2.0/tests/test_workflow → bc_cli-0.3.0/tests/test_registry}/__init__.py +0 -0
  201. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_caution.py +0 -0
  202. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_importers.py +0 -0
  203. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_metadata_fields.py +0 -0
  204. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_registry.py +0 -0
  205. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_telemetry/test_events.py +0 -0
  206. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_telemetry/test_sink.py +0 -0
  207. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_url/test_origin_allowlist.py +0 -0
  208. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_url/test_url_builder.py +0 -0
  209. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_batch_integration.py +0 -0
  210. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_loader.py +0 -0
  211. {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_models.py +0 -0
@@ -33,3 +33,6 @@ PRD-*.md
33
33
  TODOS.md
34
34
  bcapi_cli_prd.md
35
35
  .planning/
36
+
37
+ # Codex CLI session metadata — local-only, not part of the repo
38
+ .context/
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - **Clean SIGPIPE handling for piped output** — `bcli <cmd> | head`,
13
+ `| grep -m 1`, and similar pipe-truncating consumers now terminate
14
+ the CLI silently, matching `cat` and `grep` conventions, instead of
15
+ emitting a `BrokenPipeError: [Errno 32] Broken pipe` traceback at
16
+ interpreter shutdown. Implemented as a new `bcli_cli.app:main`
17
+ console-script entry point that installs `SIGPIPE -> SIG_DFL` on
18
+ POSIX with a `BrokenPipeError` safety net for Windows.
19
+ - **Hyphenated saved-query param names** — the workflow template
20
+ resolver now accepts hyphens in identifiers, so references like
21
+ `${{ params.vendor-no }}` substitute correctly. Previously the regex
22
+ matched only `[\w.]`, silently leaving the literal `${{ … }}` token
23
+ in the rendered filter (BC then 400'd or, worse, returned mismatched
24
+ rows). Affects both `bcli q` saved queries and `bcli batch`
25
+ workflows.
26
+
10
27
  ## [0.2.0] — 2026-05-06
11
28
 
12
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bc-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python SDK and CLI for Microsoft Dynamics 365 Business Central APIs
5
5
  Project-URL: Homepage, https://github.com/igor-ctrl/bcli
6
6
  Project-URL: Repository, https://github.com/igor-ctrl/bcli
@@ -32,14 +32,27 @@ Requires-Dist: tomlkit>=0.13
32
32
  Requires-Dist: typer>=0.12
33
33
  Provides-Extra: cli
34
34
  Provides-Extra: dev
35
+ Requires-Dist: anthropic>=0.40; extra == 'dev'
35
36
  Requires-Dist: dlt[filesystem,parquet,s3]>=1.0; extra == 'dev'
36
37
  Requires-Dist: mcp>=1.0; extra == 'dev'
38
+ Requires-Dist: openai>=1.50; extra == 'dev'
39
+ Requires-Dist: pypdf>=4.0; extra == 'dev'
37
40
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
38
41
  Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
39
42
  Requires-Dist: pytest>=8.0; extra == 'dev'
40
43
  Requires-Dist: ruff>=0.5; extra == 'dev'
41
44
  Provides-Extra: etl
42
45
  Requires-Dist: dlt[filesystem,parquet,s3]>=1.0; extra == 'etl'
46
+ Provides-Extra: extract
47
+ Requires-Dist: anthropic>=0.40; extra == 'extract'
48
+ Requires-Dist: openai>=1.50; extra == 'extract'
49
+ Requires-Dist: pypdf>=4.0; extra == 'extract'
50
+ Provides-Extra: extract-claude
51
+ Requires-Dist: anthropic>=0.40; extra == 'extract-claude'
52
+ Requires-Dist: pypdf>=4.0; extra == 'extract-claude'
53
+ Provides-Extra: extract-openai
54
+ Requires-Dist: openai>=1.50; extra == 'extract-openai'
55
+ Requires-Dist: pypdf>=4.0; extra == 'extract-openai'
43
56
  Provides-Extra: mcp
44
57
  Requires-Dist: mcp>=1.0; extra == 'mcp'
45
58
  Provides-Extra: polaris
@@ -0,0 +1,236 @@
1
+ # PDF Extraction (`bcli extract`)
2
+
3
+ Extract structured records from PDFs (scans, forms, tabular reports — vendor
4
+ invoices, packing slips, statements, anything tabular) via an AI vision
5
+ backend, then promote the result to Business Central through the existing
6
+ batch runner.
7
+
8
+ ```
9
+ PDF + YAML schema
10
+
11
+
12
+ bcli extract ──► <pdf>.batch.yaml ◄── operator reviews
13
+ <pdf>.extracted.json against the source PDF
14
+
15
+
16
+ bcli batch run … --profile sandbox --dry-run
17
+ bcli batch run … --profile sandbox
18
+ │ (verify in BC sandbox UI)
19
+
20
+ bcli batch run … --profile production
21
+ ```
22
+
23
+ The extraction layer never writes to BC. It produces files; humans
24
+ review; the existing `bcli batch run` machinery (with `disable_writes`,
25
+ production confirmation, audit log) handles the actual mutation. This
26
+ review step matters most when the extracted data has high blast radius
27
+ (regulated records, financial postings) — emitting batch.yaml + sidecar
28
+ instead of writing directly gives a deterministic, auditable review seam.
29
+
30
+ ## Install
31
+
32
+ Pick a backend:
33
+
34
+ ```bash
35
+ # Claude (Anthropic)
36
+ uv pip install -e ".[extract-claude]"
37
+ export ANTHROPIC_API_KEY=sk-ant-…
38
+
39
+ # OpenAI
40
+ uv pip install -e ".[extract-openai]"
41
+ export OPENAI_API_KEY=sk-…
42
+
43
+ # Both
44
+ uv pip install -e ".[extract]"
45
+ ```
46
+
47
+ Then enable the backend in `~/.config/bcli/config.toml`. With the
48
+ defaults shown, just setting `backend` is enough — each backend fills
49
+ in its own model + env-var name:
50
+
51
+ ```toml
52
+ [extract]
53
+ backend = "claude" # or "openai"
54
+
55
+ # Optional overrides — leave blank to use backend-appropriate defaults:
56
+ # claude: model = "claude-sonnet-4-6", api_key_env = "ANTHROPIC_API_KEY"
57
+ # openai: model = "gpt-5", api_key_env = "OPENAI_API_KEY"
58
+ # model = "claude-sonnet-4-6"
59
+ # api_key_env = "ANTHROPIC_API_KEY"
60
+ # schemas_dir = "~/.config/bcli/extract/schemas"
61
+ # max_pdf_bytes = 33554432 # 32 MiB
62
+ # max_pdf_pages = 100
63
+ # max_output_tokens = 8000
64
+ # openai_base_url = "" # e.g. an Azure OpenAI / proxy endpoint
65
+ # openai_organization = "" # OpenAI org id (optional)
66
+ ```
67
+
68
+ Switching between backends is a one-line config change — schemas and
69
+ generated batch.yamls are backend-agnostic, so iterating on a
70
+ schema with one provider and running production with another is fine.
71
+
72
+ ## Schemas
73
+
74
+ A schema is a YAML file that tells the backend *what* to extract and
75
+ *how* to map the result onto a BC endpoint. Drop one into
76
+ `~/.config/bcli/extract/schemas/` and `bcli extract list-schemas` picks
77
+ it up.
78
+
79
+ Minimal example:
80
+
81
+ ```yaml
82
+ name: "Purchase invoice line items"
83
+ description: "One PDF = one invoice with many line items."
84
+ prompt: |
85
+ Extract one record per line item in this vendor invoice. Skip the
86
+ header row, subtotal, tax, and grand-total rows. If a row is
87
+ illegible, OMIT it — do not guess.
88
+
89
+ list: true # one PDF → many records
90
+
91
+ fields:
92
+ item_no:
93
+ type: string
94
+ description: "Item number / SKU."
95
+ required: true
96
+ description:
97
+ type: string
98
+ description: "Item description."
99
+ required: true
100
+ quantity:
101
+ type: number
102
+ description: "Quantity ordered."
103
+ required: true
104
+ unit_price:
105
+ type: number
106
+ description: "Unit price."
107
+
108
+ output:
109
+ endpoint: purchaseLines
110
+ action: post
111
+ parent_field: documentNo
112
+ parent_param: invoice_no
113
+ field_map:
114
+ "no": item_no
115
+ description: description
116
+ quantity: quantity
117
+ directUnitCost: unit_price
118
+ constants:
119
+ documentType: "Invoice"
120
+ type: "Item"
121
+ ```
122
+
123
+ `parent_param` + `parent_field` emit a `${{ params.invoice_no }}`
124
+ placeholder in the generated `batch.yaml`. The operator fills it in
125
+ before `batch run` (or passes `--set invoice_no=…`). This is the
126
+ intentional human-in-the-loop seam: extraction can't know which BC
127
+ record the rows belong to, so it asks.
128
+
129
+ See `examples/extract/purchase_invoice_lines.yaml` for the fully-worked
130
+ schema. Author your own under `~/.config/bcli/extract/schemas/` — one
131
+ YAML per document type.
132
+
133
+ ## Use
134
+
135
+ ```bash
136
+ # Drop the schema in the well-known location, or pass a path.
137
+ bcli extract list-schemas
138
+
139
+ # Run extraction. Emits two files next to the PDF.
140
+ bcli extract run ./invoice-acme-1234.pdf --schema purchase_invoice_lines
141
+
142
+ # Output:
143
+ # invoice-acme-1234.batch.yaml ← workflow to run
144
+ # invoice-acme-1234.extracted.json ← traceability sidecar
145
+
146
+ # Promote to sandbox (dry-run first, then real).
147
+ bcli batch run invoice-acme-1234.batch.yaml \
148
+ --set invoice_no=<bc-invoice-number> \
149
+ --profile sandbox --dry-run
150
+
151
+ bcli batch run invoice-acme-1234.batch.yaml \
152
+ --set invoice_no=<bc-invoice-number> \
153
+ --profile sandbox
154
+
155
+ # Eyeball in the BC sandbox UI, then production.
156
+ bcli batch run invoice-acme-1234.batch.yaml \
157
+ --set invoice_no=<bc-invoice-number> \
158
+ --profile production
159
+ ```
160
+
161
+ ## Traceability sidecar
162
+
163
+ Every `bcli extract` run drops `<pdf>.extracted.json` next to the
164
+ batch.yaml. It contains:
165
+
166
+ - The schema name + endpoint.
167
+ - The Claude model + token usage.
168
+ - Every record, including the raw model output and the 1-indexed PDF
169
+ pages the values were read from.
170
+ - Any warnings (e.g. `list: false` schema with multiple records).
171
+
172
+ Reviewers open this side by side with the PDF to verify each value
173
+ before the batch runs. The sidecar is the deterministic, auditable
174
+ artifact your reviewer signs off on — never run the batch without
175
+ it for high-blast-radius data (regulated records, financial postings,
176
+ anything where a wrong identifier has real-world consequences).
177
+
178
+ ## PDF size limits
179
+
180
+ Anthropic caps each document block at **32 MB** and **100 pages**.
181
+ `bcli extract` checks both before sending. If your PDF is too big:
182
+
183
+ ```bash
184
+ # Split with qpdf (homebrew: brew install qpdf)
185
+ qpdf --split-pages=50 big_report.pdf split-%d.pdf
186
+
187
+ # Extract each split, then concatenate batch.yamls (or run them serially).
188
+ for f in split-*.pdf; do
189
+ bcli extract run "$f" --schema <your_schema_slug>
190
+ done
191
+ ```
192
+
193
+ Chunked-extract orchestration is a follow-up; the primitive is shipped
194
+ first.
195
+
196
+ ## Pluggable backends
197
+
198
+ `[extract] backend` accepts:
199
+
200
+ - `"null"` — no extraction (default). Returns an empty result with a warning.
201
+ - `"claude"` — Anthropic Claude (built-in, `[extract-claude]`).
202
+ - `"openai"` — OpenAI Responses API + Files API (built-in, `[extract-openai]`).
203
+ - `"my_pkg.module:MyExtractor"` — any class implementing
204
+ `bcli.extract.ExtractorBackend`. The class needs `is_active`,
205
+ `extract(pdf_path, schema)`, and a `from_config(cls, config)`
206
+ classmethod. AWS Textract, Firecrawl, OpenDataLoader, a Vertex AI
207
+ Gemini wrapper, or a self-hosted vision model all fit this shape.
208
+
209
+ Custom-backend failures fall back to `NullExtractor` with a one-shot
210
+ warning — extraction never crashes the CLI on a config mistake.
211
+
212
+ ### Backend choice tips
213
+
214
+ - Both built-ins accept the same schema. Switching is a one-line
215
+ config change; you can iterate a schema cheaply on one provider and
216
+ promote with the other.
217
+ - Aviation/regulated data: pick the provider with the residency /
218
+ compliance posture your org accepts. Neither built-in routes through
219
+ Beautech infrastructure — your API key, your traffic.
220
+ - Cost: at time of writing, both providers price PDF input in the same
221
+ ballpark for short documents. Long tabular reports tend to favor
222
+ whichever provider has the cheaper input-token rate.
223
+
224
+ ## Safety / regulated-data note
225
+
226
+ If the extracted data has real-world consequences (regulated records,
227
+ financial postings, anything where a wrong identifier matters), the
228
+ design enforces four reviews before bytes hit production:
229
+
230
+ 1. **Sidecar review** — `extracted.json` against the source PDF, by a human.
231
+ 2. **Sandbox dry-run** — `bcli batch run … --dry-run` against a non-prod profile.
232
+ 3. **Sandbox write + UI verification** — `bcli batch run … --profile sandbox` then eyeball.
233
+ 4. **Production** — only after the first three pass.
234
+
235
+ Skipping any of these defeats the design. The CLI doesn't enforce the
236
+ sequence (yet); the schema-author and the operator do.
@@ -0,0 +1,309 @@
1
+ # Beautech Team Deployment Plan
2
+
3
+ Status: draft. **Beautech-internal bootstrap document, not part of the OSS bcli roadmap.** The bcli OSS tool ships independently of this plan; the work below describes how Beautech rolls bundles + diagnostics to its own finance and technical teams on top of the upstream substrate.
4
+
5
+ Target: finance team (~10 users) + engine technical team (~10 users) on the existing scoped-profile substrate.
6
+
7
+ ## Why this plan exists
8
+
9
+ bcli is moving from solo developer to a real team rollout. The current substrate already supports it (sandboxed profiles, curated registries, scoped saved queries, device-code auth, read-only-by-permission-set), but three workflow gaps will dominate first-month support load:
10
+
11
+ 1. **No team-wide registry/query distribution.** Whoever last updated the JSON wins. New hires re-discover everything.
12
+ 2. **No diagnostic surface.** Users with broken setups can't self-rescue. "Wrong profile / stale bundle / not authenticated / wrong company" will be every other support ticket.
13
+ 3. **No discoverability for the saved-query library.** Finance ops will not learn YAML. They will ask "is there a query for overdue intercompany invoices?" via email.
14
+
15
+ This plan ships three boring high-leverage things to address (1)–(3), explicitly defers Redis and response caching until telemetry justifies them, and locks in trigger conditions so the deferral doesn't become indefinite.
16
+
17
+ ## Phase 0 — pre-flight (this sprint)
18
+
19
+ - Decide bundle storage backend: pick whichever of `S3`, `Azure Blob`, or `GitHub Releases` the org already authenticates to cleanly. Decision lives in `docs/plans/team-deployment.md` once made.
20
+ - Identify two bundle owners: one finance, one technical. They are the publish path.
21
+ - Land a minimal telemetry sink config so phase 4 has data when it's time. The pluggable `[telemetry]` substrate already exists at `src/bcli/telemetry/`; pick `console` for dev, set up Azure Monitor or a custom HTTP sink for prod. Capture `bcli.command`, `bcli.query`, `bcli.error` at minimum. **Do not** capture filter text or UPN unless privacy review approves.
22
+
23
+ ## Phase 1 — `bcli doctor` (ships first)
24
+
25
+ Self-rescue command for non-technical users. This alone will eliminate most week-one tickets.
26
+
27
+ ### Surface
28
+
29
+ ```
30
+ bcli doctor [--profile <name>] [--json]
31
+ ```
32
+
33
+ Default output: human-readable, color-coded (green/yellow/red per check), with a one-line verdict at the bottom (`OK`, `WARN`, or `FAIL`). `--json` for scripting. Non-zero exit on `FAIL`.
34
+
35
+ ### Checks
36
+
37
+ | Check | Source | Fail condition |
38
+ |---|---|---|
39
+ | Active profile | `CLIState` resolved profile | Missing or unknown profile name |
40
+ | Bundle version | `manifest.json` `version` field (phase 2) | Missing manifest, or `last_refresh > 30d` |
41
+ | Signature verified | bundle signature check (phase 2) | Signature missing or invalid |
42
+ | Last refresh time | bundle metadata | > 7d warn, > 30d fail |
43
+ | Registry endpoint count | `EndpointRegistry.list_all()` | 0 endpoints in scoped profile |
44
+ | Saved query count | `queries/<profile>.yaml` | File missing in scoped profile |
45
+ | Field-list coverage | count of endpoints with `field_names` populated | Warn under 50% for scoped profiles |
46
+ | Auth mode | profile config | Unknown mode |
47
+ | Auth status | non-blocking probe of cached token | Expired and no refresh path |
48
+ | Company | `--company` resolution | No default and no override available |
49
+ | Environment | profile config | Missing |
50
+ | Tenant ID | profile config | Missing |
51
+ | BC connectivity | one-shot `GET companies` with 5s timeout | Non-2xx |
52
+ | Local overlay present | overlay file exists (phase 2) | Informational only |
53
+
54
+ ### Output sketch
55
+
56
+ ```
57
+ bcli doctor — profile: finance
58
+
59
+ ✓ Active profile finance
60
+ ✓ Bundle version 2026.05.07-1 (signed by ops-bcli-bot)
61
+ ✓ Last refresh 2 days ago
62
+ ✓ Registry 42 endpoints, 38 with field lists (90%)
63
+ ✓ Saved queries 17 queries
64
+ ✓ Auth device_code, token valid for 47 min
65
+ ✓ Tenant contoso.onmicrosoft.com
66
+ ✓ Environment production
67
+ ✓ Company BTALI (default)
68
+ ✓ BC connectivity reachable, 312 ms
69
+ ⚠ Field coverage 2 endpoints below 80% (run `bcli endpoint fields ...`)
70
+
71
+ Verdict: OK
72
+ ```
73
+
74
+ ### Files to add/modify
75
+
76
+ - New: `src/bcli_cli/commands/doctor_cmd.py`
77
+ - New: `src/bcli/diagnostics/_checks.py` (testable check primitives, returns `CheckResult` with status + message)
78
+ - Modify: `src/bcli_cli/app.py` to register the command group
79
+ - New: `tests/test_diagnostics/test_checks.py`
80
+
81
+ ### Done when
82
+
83
+ - `bcli doctor` runs in under 3 seconds for a healthy install.
84
+ - Each check is independently unit-tested with parametrized fail cases.
85
+ - Engine-tech and finance both ran it on their own laptop and the output made sense without explanation.
86
+
87
+ ## Phase 2 — signed bundle distribution
88
+
89
+ ### Bundle layout
90
+
91
+ ```
92
+ finance-2026.05.07-1.tar.gz
93
+ manifest.json
94
+ registry.json # mirrors current ~/.config/bcli/registries/<profile>.json
95
+ queries.yaml # mirrors current ~/.config/bcli/queries/<profile>.yaml
96
+ field_lists.json # pre-warmed field discovery (avoids first-touch tax)
97
+ README.md # human notes for this bundle version
98
+ ```
99
+
100
+ `manifest.json`:
101
+
102
+ ```json
103
+ {
104
+ "schema_version": 1,
105
+ "profile": "finance",
106
+ "version": "2026.05.07-1",
107
+ "published_at": "2026-05-07T14:32:00Z",
108
+ "publisher": "ops-bcli-bot",
109
+ "checksum_sha256": "…",
110
+ "signature": "…",
111
+ "min_bcli_version": "0.3.0",
112
+ "previous_version": "2026.04.30-2",
113
+ "release_notes": "Added overdue-ic and posted-invoice-by-id queries"
114
+ }
115
+ ```
116
+
117
+ ### Storage + transport
118
+
119
+ - Backend: signed HTTPS, single source of truth per profile. Bundles published to versioned object keys, e.g. `https://bundles.example.com/bcli/finance/2026.05.07-1.tar.gz` with a `latest.json` pointer for resolution. The org-specific URL lives in `~/.config/bcli/config.toml` as `[bundle.finance] url = "..."` so different teams can self-host.
120
+ - Signing: detached signature (`minisign` or `cosign`, decision pending) with the public key shipped in the user's profile config. Refresh fails closed if signature does not verify.
121
+ - **Not** `git pull`. Maintainers can author bundles in a private GitHub repo and ship via Release assets, but the user-facing transport is a signed tarball over HTTPS.
122
+
123
+ ### `bcli config refresh` UX
124
+
125
+ ```
126
+ bcli config refresh # refresh active profile
127
+ bcli config refresh --profile technical # explicit profile
128
+ bcli config refresh --dry-run # show diff vs local, no writes
129
+ bcli config refresh --rollback # restore previous version
130
+ bcli config refresh --check # exit code only, no output (cron-friendly)
131
+ ```
132
+
133
+ Non-interactive, atomic. Output:
134
+
135
+ ```
136
+ Refreshing finance from https://bundles.example.com/bcli/finance/latest.json
137
+ Current: 2026.04.30-2
138
+ Latest: 2026.05.07-1 (published 2 days ago by ops-bcli-bot)
139
+ Verifying signature… ok
140
+ Diff:
141
+ + 2 endpoints (postedSalesInvoices, salesInvoiceLines)
142
+ + 3 saved queries (overdue-ic, posted-by-id, customer-aging)
143
+ ~ 1 query updated (open-pos: added customer parameter)
144
+ Applied. Previous version retained at ~/.config/bcli/registries/finance.2026.04.30-2.json
145
+ ```
146
+
147
+ ### Overlay semantics
148
+
149
+ - Team bundle is **authoritative** for scoped profiles. `bcli config refresh` overwrites `registry.json` and `queries.yaml` atomically (write-temp + rename). Previous version retained for one rollback.
150
+ - Local overlay file `~/.config/bcli/overlays/<profile>.yaml` exists *only if* the profile config has `allow_local_overrides = true`. Off by default for sandboxed domain profiles.
151
+ - Effective view: team bundle merged with overlay, **team wins on name conflicts**. No interactive merge prompts ever. Non-technical users never see a conflict.
152
+ - `bcli endpoint fields` discovery for sandboxed profiles writes to overlay if enabled, otherwise informs the user to send the discovered fields to their bundle owner.
153
+
154
+ ### Files to add/modify
155
+
156
+ - New: `src/bcli/bundle/__init__.py` (manifest schema, signature verification, atomic apply)
157
+ - New: `src/bcli/bundle/_fetch.py`, `_verify.py`, `_apply.py`, `_rollback.py`
158
+ - New: `src/bcli_cli/commands/refresh_cmd.py` (registered as `bcli config refresh`)
159
+ - Modify: `src/bcli/config/_loader.py` to compose registry from team bundle + optional overlay
160
+ - Modify: `src/bcli/registry/_registry.py` to load from the new layered path
161
+ - New: `examples/bundles/sample-bundle.tar.gz` + a `make-bundle` script for admins
162
+ - New: `docs/team-bundles.md` covering the publish workflow
163
+
164
+ ### Done when
165
+
166
+ - An admin can produce a signed bundle with one command and publish it.
167
+ - A finance user can run `bcli config refresh` cold and have a working setup in under 30 seconds.
168
+ - `--rollback` restores the previous version verifiably.
169
+ - Tampered bundle is rejected with a clear error, not silently applied.
170
+
171
+ ## Phase 3 — query metadata extension
172
+
173
+ Saved queries get richer descriptive metadata so substring + tag search beats the discoverability problem without embeddings.
174
+
175
+ ### YAML schema additions
176
+
177
+ ```yaml
178
+ queries:
179
+ overdue-ic:
180
+ description: Overdue intercompany invoices for a vendor
181
+ aliases: [overdue-intercompany, ic-overdue, ar-overdue-ic]
182
+ tags: [period-close, ap, intercompany]
183
+ owner: finance-ops
184
+ freshness: live # one of: live, daily, reference
185
+ examples:
186
+ - bcli q overdue-ic vendor=ACME-IC
187
+ - bcli q overdue-ic vendor=ACME-IC days=30
188
+ related: [open-invoices, vendor-aging]
189
+ params:
190
+ vendor: { required: true, hint: "BC Vendor No." }
191
+ days: { default: 30, hint: "Days overdue" }
192
+ # Query body lives at the top level (matches the runtime — there is
193
+ # no `odata:` wrapper). The metadata block above is purely for
194
+ # discoverability; nothing in it changes how the query executes.
195
+ endpoint: vendorLedgerEntries
196
+ filter: "vendorNumber eq '${{ params.vendor }}' and dueDate lt now sub '${{ params.days }}d' and remainingAmount gt 0"
197
+ orderby: dueDate
198
+ ```
199
+
200
+ ### Search surface
201
+
202
+ ```
203
+ bcli q list # all queries, table
204
+ bcli q list --tag period-close # filter by tag
205
+ bcli q list --owner finance-ops # filter by owner
206
+ bcli q search "overdue invoices" # substring + alias + description match
207
+ bcli q info overdue-ic # full metadata view
208
+ ```
209
+
210
+ `bcli q search` ranks by: exact name > alias hit > tag hit > description substring > example substring. No embeddings. Honest "no match, did you mean X" output when the score floor isn't met.
211
+
212
+ ### Files to add/modify
213
+
214
+ - Modify: `src/bcli/workflow/` query schema (extend Pydantic model)
215
+ - Modify: `src/bcli_cli/commands/query_cmd.py` to add `list`, `search`, `info` subcommands
216
+ - New: `tests/test_workflow/test_query_metadata.py`
217
+ - Update: `docs/saved-queries.md` with the schema additions
218
+ - Migration: existing queries without the new fields keep working (all new fields optional)
219
+
220
+ ### Done when
221
+
222
+ - Existing queries still run unchanged.
223
+ - `bcli q search` finds an existing query when the user types a plausible NL phrase.
224
+ - Finance ops can browse the catalog by tag without reading YAML.
225
+
226
+ ## Phase 4 — response caching (deferred, telemetry-gated)
227
+
228
+ **Do not ship until all three triggers are met:**
229
+
230
+ 1. Telemetry shows P95 latency for posted-invoice / open-PO endpoints exceeding 2s under finance close-week load.
231
+ 2. Telemetry shows ≥ 5% of GETs returning 429 / 503 during close week.
232
+ 3. The two bundle owners agree the workflow pain is real, not theoretical.
233
+
234
+ If shipped:
235
+
236
+ - Backend: `hishel`-backed disk cache around `httpx`, **not** Redis. Single-process. Lives at `~/.config/bcli/cache/`.
237
+ - Cache key composition: `tenant_id + environment + company + profile + resolved_url + sorted(query_params) + select_hash`. Never less.
238
+ - TTL ceilings (max — actual values configurable per endpoint, can be lower):
239
+ - Vendor balances: no cache by default; 5-15s if forced, output labeled `(cached 8s ago)`
240
+ - Open POs / open invoices: 15-60s
241
+ - Inventory / utilization / preservation status: 10-60s
242
+ - Posted invoice **list** queries: 60-300s only when filtered to a closed period
243
+ - Posted invoice / journal entry **by exact record ID**: 1-24h (immutable post-posting)
244
+ - Never cache `--all`. Never cache write-adjacent commands. Cache hits are visible in output and structured logs.
245
+ - Opt-in per profile via `[cache] enabled = true`.
246
+
247
+ ## Phase 5 — Redis-for-AI (deferred, condition-gated)
248
+
249
+ **Do not ship until at least one of:**
250
+
251
+ 1. A centralized `bcli-mcp` service is running for multiple agents/users (cross-process state stops being free).
252
+ 2. Saved-query library exceeds 200 entries with measurable search misses in telemetry.
253
+ 3. Field-list "did you mean" produces wrong suggestions ≥ 5% of attempts measurably.
254
+
255
+ If shipped, the integration points are:
256
+
257
+ - Vector "did you mean" over discovered field names (replaces substring fuzzy in `src/bcli/client/_async.py:469`).
258
+ - Vector search over saved queries by NL intent. Caches **query plans**, never query results.
259
+ - Shared field-discovery cache for the centralized MCP path.
260
+
261
+ Backend: pluggable `cache_backend` with `redis` extra, mirroring the existing `[telemetry]` pattern. NullCache default. Redis is optional infrastructure.
262
+
263
+ ## Out of scope
264
+
265
+ - Semantic caching of OData result data. Stale balances / inventory / posted-document state silently returned would destroy trust in bcli as a BC truth source. If we want a low-risk reference subset later, it lands as an explicit feature with `cached as of` labels in output, not a silent layer.
266
+ - Token sharing across users. Each user authenticates as themselves. The BC permission set is the security boundary, not the bcli flag.
267
+ - Replacing the existing per-profile registry JSON layout. Bundles are a publish-and-distribute layer on top, not a replacement.
268
+
269
+ ## Risks and open questions
270
+
271
+ - **Beautech rollout gate: publisher signing.** The current
272
+ `Sha256Verifier` only proves internal consistency: each file matches
273
+ its declared hash, and the manifest's roll-up matches the contents
274
+ map. It does NOT authenticate the publisher. A compromised CDN can
275
+ mint a malicious `registry.json`, recompute the hashes, and pass
276
+ verification. Before Beautech rolls bundles to finance / technical,
277
+ either ship a real cryptographic signer (`minisign` / `cosign` /
278
+ ed25519 + pinned key) at the `bcli.bundle.Verifier` seam, or restrict
279
+ bundle distribution to private blob storage with org-level auth and
280
+ treat HTTPS+auth as the trust boundary. Document the choice
281
+ internally; do not enable `bcli config refresh` for finance/engine-
282
+ tech without one of the two. Note: this is a Beautech deployment
283
+ gate, not an OSS bcli release gate — the upstream tool can ship the
284
+ bundle infra without dictating how operators use it.
285
+ - **Signing key custody.** Who owns the bundle signing key, and how is it rotated when an owner leaves? Decide before phase 2 ships.
286
+ - **Bundle URL discovery.** First-time install needs to know where to refresh from. Likely `bcli config init --scoped --bundle-url <url>` extends the existing wizard. Verify this fits the wizard's current shape.
287
+ - **Field discovery in scoped profiles.** Today `bcli endpoint fields` writes back to the local registry. With overlay-off-by-default, sandboxed users can't improve their own setup. The plan: those discoveries get logged to a "candidate fields" file the user can email to their bundle owner. Better mechanism welcome.
288
+ - **Bundle drift between teams.** If finance and technical import the same standard endpoint into both bundles and they diverge, which wins? Today: each profile is isolated. Keep it that way.
289
+ - **Telemetry privacy.** Phase 0 telemetry must not capture filter text or UPN by default. Confirm with the legal/privacy reviewer.
290
+
291
+ ## Validation gates per phase
292
+
293
+ | Phase | Gate |
294
+ |---|---|
295
+ | 1 | `bcli doctor` runs on technical and finance laptops cold; output makes sense to a non-developer |
296
+ | 2 | Bundle round-trip works: admin publishes, user runs `refresh`, signature verifies, rollback works |
297
+ | 3 | Existing queries unchanged; `q search "overdue invoices"` finds `overdue-ic` |
298
+ | 4 | Telemetry triggers met before any code is written |
299
+ | 5 | At least one of three triggers met before any code is written |
300
+
301
+ ## Sequencing summary
302
+
303
+ 1. **Now:** phase 0 (pick backend + telemetry sink) and phase 1 (`bcli doctor`). Two weeks.
304
+ 2. **Next:** phase 2 (bundle distribution + `config refresh`). Three weeks.
305
+ 3. **After phase 2 ships and bakes for two weeks:** phase 3 (query metadata + search). One week.
306
+ 4. **On telemetry:** phase 4. Maybe never.
307
+ 5. **On condition trigger:** phase 5. Maybe never.
308
+
309
+ The honest read: phases 1–3 are most of the user-facing value. Phases 4–5 are the interesting features but only earn their cost under conditions we haven't observed yet.
@@ -0,0 +1,61 @@
1
+ name: "Purchase invoice line items"
2
+ description: |
3
+ Illustrative schema for extracting line items from a vendor PDF
4
+ invoice. One PDF typically contains many line items as a single
5
+ table; this schema captures the common columns.
6
+
7
+ Adjust the field map to match your tenant's purchaseLines field
8
+ names. Treat this file as a starting point, not production-ready —
9
+ every supplier formats invoices differently, and the prompt below
10
+ may need sharpening for your specific vendor templates.
11
+
12
+ prompt: |
13
+ This PDF is a vendor purchase invoice. Extract one record per
14
+ line item in the line-items table. Skip the header row, subtotal
15
+ rows, tax rows, and the grand total — those are not line items.
16
+
17
+ Expected columns (names vary by vendor; match by meaning):
18
+ - Item / SKU / Part number → item_no
19
+ - Description / Item name → description
20
+ - Quantity / Qty → quantity
21
+ - Unit price / Rate → unit_price
22
+ - Line total / Amount → line_total
23
+
24
+ If a row's item_no or quantity is illegible, OMIT the row rather
25
+ than guessing. Record the 1-indexed PDF page in source_pages.
26
+
27
+ list: true
28
+
29
+ fields:
30
+ item_no:
31
+ type: string
32
+ description: "Item number / SKU / part number from the leftmost identifier column."
33
+ required: true
34
+ description:
35
+ type: string
36
+ description: "Item description / name."
37
+ required: true
38
+ quantity:
39
+ type: number
40
+ description: "Quantity ordered. Numeric only."
41
+ required: true
42
+ unit_price:
43
+ type: number
44
+ description: "Unit price / unit cost. Numeric only, no currency symbol."
45
+ line_total:
46
+ type: number
47
+ description: "Line total (quantity × unit_price). Numeric only."
48
+
49
+ output:
50
+ endpoint: purchaseLines
51
+ action: post
52
+ parent_field: documentNo
53
+ parent_param: invoice_no
54
+ field_map:
55
+ "no": item_no
56
+ description: description
57
+ quantity: quantity
58
+ directUnitCost: unit_price
59
+ constants:
60
+ documentType: "Invoice"
61
+ type: "Item"