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.
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.gitignore +3 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/CHANGELOG.md +17 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/PKG-INFO +14 -1
- bc_cli-0.3.0/docs/extraction.md +236 -0
- bc_cli-0.3.0/docs/plans/team-deployment.md +309 -0
- bc_cli-0.3.0/examples/extract/purchase_invoice_lines.yaml +61 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/pyproject.toml +15 -2
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/__init__.py +2 -0
- bc_cli-0.3.0/src/bcli/bundle/__init__.py +47 -0
- bc_cli-0.3.0/src/bcli/bundle/_apply.py +323 -0
- bc_cli-0.3.0/src/bcli/bundle/_fetch.py +197 -0
- bc_cli-0.3.0/src/bcli/bundle/_manifest.py +127 -0
- bc_cli-0.3.0/src/bcli/bundle/_publish.py +110 -0
- bc_cli-0.3.0/src/bcli/bundle/_verify.py +166 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/_model.py +48 -0
- bc_cli-0.3.0/src/bcli/diagnostics/__init__.py +26 -0
- bc_cli-0.3.0/src/bcli/diagnostics/_checks.py +510 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/errors.py +4 -0
- bc_cli-0.3.0/src/bcli/extract/__init__.py +43 -0
- bc_cli-0.3.0/src/bcli/extract/_claude.py +248 -0
- bc_cli-0.3.0/src/bcli/extract/_factory.py +106 -0
- bc_cli-0.3.0/src/bcli/extract/_openai.py +318 -0
- bc_cli-0.3.0/src/bcli/extract/_pdf.py +68 -0
- bc_cli-0.3.0/src/bcli/extract/_protocol.py +97 -0
- bc_cli-0.3.0/src/bcli/extract/_schema.py +208 -0
- bc_cli-0.3.0/src/bcli/extract/_yaml_writer.py +173 -0
- bc_cli-0.3.0/src/bcli/workflow/_query_search.py +154 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/_resolver.py +4 -2
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_state.py +5 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/app.py +47 -2
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/config_cmd.py +10 -0
- bc_cli-0.3.0/src/bcli_cli/commands/doctor_cmd.py +171 -0
- bc_cli-0.3.0/src/bcli_cli/commands/extract_cmd.py +193 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/get_cmd.py +2 -1
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/query_cmd.py +260 -20
- bc_cli-0.3.0/src/bcli_cli/commands/refresh_cmd.py +298 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/output/_formatters.py +103 -3
- bc_cli-0.3.0/tests/test_bundle/test_bundle_roundtrip.py +394 -0
- bc_cli-0.3.0/tests/test_cli/test_pipe_handling.py +65 -0
- bc_cli-0.3.0/tests/test_cli/test_records_format.py +137 -0
- bc_cli-0.3.0/tests/test_diagnostics/test_checks.py +306 -0
- bc_cli-0.3.0/tests/test_extract/test_claude.py +224 -0
- bc_cli-0.3.0/tests/test_extract/test_factory.py +131 -0
- bc_cli-0.3.0/tests/test_extract/test_openai.py +303 -0
- bc_cli-0.3.0/tests/test_extract/test_pdf.py +53 -0
- bc_cli-0.3.0/tests/test_extract/test_schema.py +192 -0
- bc_cli-0.3.0/tests/test_extract/test_yaml_writer.py +149 -0
- bc_cli-0.3.0/tests/test_telemetry/__init__.py +0 -0
- bc_cli-0.3.0/tests/test_url/__init__.py +0 -0
- bc_cli-0.3.0/tests/test_workflow/__init__.py +0 -0
- bc_cli-0.3.0/tests/test_workflow/test_query_search.py +139 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_resolver.py +10 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/uv.lock +201 -2
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.env.example +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/.github/workflows/tests.yml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/AGENTS.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/CONTRIBUTING.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/LICENSE +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/NOTICE +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/README.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/SECURITY.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/authentication.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/batch-operations.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/business-central-admin-setup.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/command-reference.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/configuration.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/contributing.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/custom-apis.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/demo-setup.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/getting-started.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/mcp-server.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/multi-company.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/querying.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/saved-queries.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/sdk-usage.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/docs/write-operations.md +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/ap-monthly-review.yaml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/attach-purchase-invoice-pdf.yaml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/create-purchase-invoice.yaml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/month-end-cronus.yaml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/examples/queries/sample.yaml +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/_url.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/_version.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/_factory.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/_protocol.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/audit/_redact.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_base.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_browser.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_credentials.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_device_code.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_secure_io.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/auth/_token_cache.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_async.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_safety.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_sync.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/client/_transport.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/_defaults.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/config/_loader.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_auth.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_bridge.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_client.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_generic.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_polaris.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/etl/_stampers.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_escape.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_filter_fields.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_pagination.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_query.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/odata/_response.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/py.typed +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/_importers.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/_registry.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/_schema.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/registry/standard_v2.json +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/_azure_monitor.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/_factory.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/_protocol.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/telemetry/events.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/_loader.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli/workflow/_models.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_audit_wrap.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_dry_run.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_safety.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/_url_resolve.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/attach_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/auth_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/batch_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/company_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/context_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/delete_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/endpoint_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/env_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/etl_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/patch_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/post_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/registry_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/commands/test_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/output/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_cli/output/_display.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/__main__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/_runner.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/src/bcli_mcp/_server.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/conftest.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/fixtures/sample_postman_collection.json +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/test_factory.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/test_redact.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_audit/test_sink.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_auth/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_auth/test_browser_auth.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_auth/test_secure_io.py +0 -0
- {bc_cli-0.2.0/tests/test_cli → bc_cli-0.3.0/tests/test_bundle}/__init__.py +0 -0
- {bc_cli-0.2.0/tests/test_client → bc_cli-0.3.0/tests/test_cli}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_audit_wrap.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_batch_safety.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_company_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_config_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_dry_run.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_output_format.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_query_cmd.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_safety.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_cli/test_state.py +0 -0
- {bc_cli-0.2.0/tests/test_config → bc_cli-0.3.0/tests/test_client}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_resolve_url.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_safety.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_transport.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_client/test_upload_attachment.py +0 -0
- {bc_cli-0.2.0/tests/test_etl → bc_cli-0.3.0/tests/test_config}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_config/test_config.py +0 -0
- {bc_cli-0.2.0/tests/test_mcp → bc_cli-0.3.0/tests/test_diagnostics}/__init__.py +0 -0
- {bc_cli-0.2.0/tests/test_odata → bc_cli-0.3.0/tests/test_etl}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_etl/test_bridge.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_etl/test_generic.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_etl/test_stampers.py +0 -0
- {bc_cli-0.2.0/tests/test_registry → bc_cli-0.3.0/tests/test_extract}/__init__.py +0 -0
- {bc_cli-0.2.0/tests/test_telemetry → bc_cli-0.3.0/tests/test_mcp}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_mcp/test_runner.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_mcp/test_server_tools.py +0 -0
- {bc_cli-0.2.0/tests/test_url → bc_cli-0.3.0/tests/test_odata}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_odata/test_escape.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_odata/test_filter_fields.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_odata/test_query.py +0 -0
- {bc_cli-0.2.0/tests/test_workflow → bc_cli-0.3.0/tests/test_registry}/__init__.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_caution.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_importers.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_metadata_fields.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_registry/test_registry.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_telemetry/test_events.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_telemetry/test_sink.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_url/test_origin_allowlist.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_url/test_url_builder.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_batch_integration.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_loader.py +0 -0
- {bc_cli-0.2.0 → bc_cli-0.3.0}/tests/test_workflow/test_models.py +0 -0
|
@@ -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.
|
|
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"
|