From 3ce5ea8cab3fe32a6d99b4a20d7c85a5c63767bd Mon Sep 17 00:00:00 2001 From: igor-ctrl Date: Tue, 5 May 2026 07:25:07 -0500 Subject: [PATCH 1/3] fix(version): read __version__ from package metadata, bump to 0.1.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/bcli/_version.py had a hardcoded `__version__ = "0.1.0"` that the release process didn't bump alongside `pyproject.toml`. The 0.1.3 release shipped with `bcli --version` reporting "0.1.0" — the wheel contents and entry-point logic were correct, only the version string was stale. Switch _version.py to read from importlib.metadata.version("bc-cli"), which is the canonical Python-packaging single source of truth. pyproject.toml's `version` field is now the only place release.sh needs to coordinate with — no duplicate constant to forget. Bump pyproject.toml to 0.1.4 so the next release ships with both the fix and a correct self-reported version. (Republishing 0.1.3 isn't possible on PyPI — versions are immutable once published.) Verified locally: `from bcli._version import __version__; print(__version__)` returns "0.1.4" after `uv tool install`. --- pyproject.toml | 2 +- src/bcli/_version.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ee4b03d..2e8fb26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "hatchling.build" # installed CLI binary (`bcli`) are unaffected — only `pip install` / # `uv tool install` use this name. name = "bc-cli" -version = "0.1.3" +version = "0.1.4" description = "Python SDK and CLI for Microsoft Dynamics 365 Business Central APIs" readme = "README.md" license = "Apache-2.0" diff --git a/src/bcli/_version.py b/src/bcli/_version.py index 3dc1f76..91f483f 100644 --- a/src/bcli/_version.py +++ b/src/bcli/_version.py @@ -1 +1,18 @@ -__version__ = "0.1.0" +"""Single source of truth: the installed package's metadata. + +Reading from importlib.metadata avoids the drift bug where pyproject.toml +got bumped but a hardcoded __version__ string in this module didn't — +the symptom was `bcli --version` reporting an older version than the +wheel actually shipped. + +Falls back to a placeholder for editable installs that haven't been +registered with metadata yet (rare, but cleaner than crashing). +""" +from __future__ import annotations + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("bc-cli") +except PackageNotFoundError: # pragma: no cover — only triggers in dev sandboxes + __version__ = "0.0.0+unknown" From 03e54e2c18ff5439b991e6f76f85609ab3b36b09 Mon Sep 17 00:00:00 2001 From: igor-ctrl Date: Tue, 5 May 2026 13:14:57 -0500 Subject: [PATCH 2/3] fix(uv.lock): sync lockfile to 0.1.4 Previous commit 3ce5ea8 bumped pyproject.toml to 0.1.4 but did not update uv.lock, so CI failed on `uv sync --locked` across all three Python versions. --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 37620f9..4a940d7 100644 --- a/uv.lock +++ b/uv.lock @@ -302,7 +302,7 @@ wheels = [ [[package]] name = "bc-cli" -version = "0.1.2" +version = "0.1.4" source = { editable = "." } dependencies = [ { name = "httpx" }, From a9b0a70a9dd3075b03fd2ca897f3afc2486efd5b Mon Sep 17 00:00:00 2001 From: igor-ctrl Date: Tue, 5 May 2026 13:16:27 -0500 Subject: [PATCH 3/3] feat(cli): hide route-override flags + add agent anti-recipe Stops AI agents from generating bloated invocations like `bcli -c LLC get fixedAssets --publisher beautech --group finance --version v1.5 --all -f json` when `bcli -c LLC get fixedAssets` is enough. - Mark `--publisher`, `--group`, `--version` as `hidden=True` in get/post/patch/delete/attach commands. The flags still work as escape hatches for power users; they just don't appear in `--help`, so agents can't pattern-match them in. - Extend AGENTS.md "Profiles do most of the work" to cover the registry-resolved route, then add a "Don't write this" anti-recipe block with the literal bad command and minimal alternative, explaining why each flag was redundant (registry resolves publisher/group/version; `--all` paginates everything; `-f json` is auto-detected via CLAUDECODE=1). --- AGENTS.md | 41 +++++++++++++++++++++++++++++ src/bcli_cli/commands/attach_cmd.py | 12 ++++----- src/bcli_cli/commands/delete_cmd.py | 6 ++--- src/bcli_cli/commands/get_cmd.py | 6 ++--- src/bcli_cli/commands/patch_cmd.py | 6 ++--- src/bcli_cli/commands/post_cmd.py | 6 ++--- 6 files changed, 59 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 47126be..0fa41ec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,47 @@ Use `-e` / `--env` only when the user explicitly asks you to hit a Same for `--company` / `-c`: only pass it when switching off the profile's default company. +The endpoint **registry** does the same job for the API route. When you +write `bcli get fixedAssets`, the registry already knows the +publisher / group / version for `fixedAssets` and builds the URL for +you. You do **not** need `--publisher … --group … --version …` — those +are escape hatches for the rare case where an admin hasn't imported the +endpoint yet, and they're hidden from `--help` for that reason. If +`bcli get ` errors with `RegistryError`, the fix is to import the +endpoint into the registry, not to pass override flags. + +--- + +## Don't write this — minimal command, please + +The single biggest tell that an agent is over-pattern-matching: +redundant flags that the profile + registry already supply. + +```bash +# ❌ Don't write this: +bcli -c LLC get fixedAssets --publisher beautech --group finance --version v1.5 --all -f json + +# ✅ Write this: +bcli -c LLC get fixedAssets +``` + +Why each flag was wrong: + +- `--publisher beautech --group finance --version v1.5` — the + registry resolves these automatically. Only pass them if the + endpoint isn't in the registry (and even then, prefer importing it). +- `--all` — pulls **every** page. Most asks need `--top 5` or no + pagination flag at all. Use `--all` only when the user explicitly + asks for a full export. +- `-f json` — bcli already auto-detects agents (`CLAUDECODE=1`, + `BCLI_AGENT=1`, or non-TTY stdout) and emits markdown. Pass `-f + json` only when you actually need to feed the result into `jq` or + another tool call. + +Rule of thumb: **start with the shortest command that names the +action** (`bcli get fixedAssets`), then add flags one at a time only +when the user's question demands them. + --- ## Endpoint discovery — don't guess names diff --git a/src/bcli_cli/commands/attach_cmd.py b/src/bcli_cli/commands/attach_cmd.py index db742d0..0999a26 100644 --- a/src/bcli_cli/commands/attach_cmd.py +++ b/src/bcli_cli/commands/attach_cmd.py @@ -41,9 +41,9 @@ def upload_command( parent_type: str = typer.Option("Purchase Invoice", "--parent-type", help="BC parent entity type"), file_name: Optional[str] = typer.Option(None, "--file-name", help="Override the attachment filename (defaults to the source filename)"), content_type: Optional[str] = typer.Option(None, "--content-type", help="Override Content-Type for the binary PATCH (defaults to mime-guess or application/octet-stream)"), - publisher: Optional[str] = typer.Option(None, "--publisher", help="Custom API publisher (e.g. 'mycompany')"), - group: Optional[str] = typer.Option(None, "--group", help="Custom API group (e.g. 'finance')"), - version: Optional[str] = typer.Option(None, "--version", help="Custom API version (e.g. 'v1.5')"), + publisher: Optional[str] = typer.Option(None, "--publisher", hidden=True, help="Custom API publisher override (e.g. 'mycompany') — registry resolves this automatically"), + group: Optional[str] = typer.Option(None, "--group", hidden=True, help="Custom API group override (e.g. 'finance') — registry resolves this automatically"), + version: Optional[str] = typer.Option(None, "--version", hidden=True, help="Custom API version override (e.g. 'v1.5') — registry resolves this automatically"), standard: bool = typer.Option(False, "--standard", "--no-registry", help="Bypass the custom registry and force Microsoft's standard /api/v2.0/documentAttachments route. Use when a custom page isn't persisting (zero-GUID ids)."), format: Optional[str] = typer.Option(None, "--format", "-f", help="Output format: table, json, csv, ndjson, raw"), yes: bool = typer.Option(False, "--yes", "-y", help="Skip the read-only-profile warning prompt"), @@ -103,9 +103,9 @@ def test_command( invoice_date: Optional[str] = typer.Option(None, "--invoice-date", help="Invoice date (YYYY-MM-DD); defaults to today"), file_name: Optional[str] = typer.Option(None, "--file-name", help="Override the attachment filename"), content_type: Optional[str] = typer.Option(None, "--content-type", help="Override Content-Type for the binary PATCH"), - publisher: Optional[str] = typer.Option(None, "--publisher", help="Custom API publisher for the attach step"), - group: Optional[str] = typer.Option(None, "--group", help="Custom API group for the attach step"), - version: Optional[str] = typer.Option(None, "--version", help="Custom API version for the attach step"), + publisher: Optional[str] = typer.Option(None, "--publisher", hidden=True, help="Custom API publisher for the attach step — registry resolves this automatically"), + group: Optional[str] = typer.Option(None, "--group", hidden=True, help="Custom API group for the attach step — registry resolves this automatically"), + version: Optional[str] = typer.Option(None, "--version", hidden=True, help="Custom API version for the attach step — registry resolves this automatically"), standard: bool = typer.Option(False, "--standard", "--no-registry", help="Bypass the custom registry for the attach step (forces /api/v2.0/documentAttachments)"), format: Optional[str] = typer.Option(None, "--format", "-f", help="Output format: table, json, csv, ndjson, raw"), ) -> None: diff --git a/src/bcli_cli/commands/delete_cmd.py b/src/bcli_cli/commands/delete_cmd.py index 01ef196..efb1274 100644 --- a/src/bcli_cli/commands/delete_cmd.py +++ b/src/bcli_cli/commands/delete_cmd.py @@ -19,9 +19,9 @@ def delete_command( record_id: str = typer.Argument(help="Record ID to delete"), etag: str = typer.Option("*", "--etag", help="ETag for optimistic concurrency"), format: Optional[str] = typer.Option(None, "--format", "-f", help="Output format (unused, for flag consistency)"), - publisher: Optional[str] = typer.Option(None, "--publisher"), - group: Optional[str] = typer.Option(None, "--group"), - version: Optional[str] = typer.Option(None, "--version"), + publisher: Optional[str] = typer.Option(None, "--publisher", hidden=True), + group: Optional[str] = typer.Option(None, "--group", hidden=True), + version: Optional[str] = typer.Option(None, "--version", hidden=True), yes: bool = typer.Option(False, "--yes", "-y", help="Skip the read-only-profile warning prompt"), ) -> None: """DELETE a record.""" diff --git a/src/bcli_cli/commands/get_cmd.py b/src/bcli_cli/commands/get_cmd.py index cf3cc5a..5638b67 100644 --- a/src/bcli_cli/commands/get_cmd.py +++ b/src/bcli_cli/commands/get_cmd.py @@ -27,9 +27,9 @@ def get_command( count: bool = typer.Option(False, "--count", help="Include total record count"), all_pages: bool = typer.Option(False, "--all", help="Follow pagination to get all records"), format: Optional[str] = typer.Option(None, "--format", "-f", help="Output format: table, json, csv, ndjson, raw"), - publisher: Optional[str] = typer.Option(None, "--publisher", help="Custom API publisher override"), - group: Optional[str] = typer.Option(None, "--group", help="Custom API group override"), - version: Optional[str] = typer.Option(None, "--version", help="Custom API version override"), + publisher: Optional[str] = typer.Option(None, "--publisher", hidden=True, help="Custom API publisher override (escape hatch — registry resolves this automatically)"), + group: Optional[str] = typer.Option(None, "--group", hidden=True, help="Custom API group override (escape hatch — registry resolves this automatically)"), + version: Optional[str] = typer.Option(None, "--version", hidden=True, help="Custom API version override (escape hatch — registry resolves this automatically)"), ) -> None: """GET records from a Business Central entity. diff --git a/src/bcli_cli/commands/patch_cmd.py b/src/bcli_cli/commands/patch_cmd.py index 6296da3..9dcfa23 100644 --- a/src/bcli_cli/commands/patch_cmd.py +++ b/src/bcli_cli/commands/patch_cmd.py @@ -22,9 +22,9 @@ def patch_command( data: str = typer.Option(..., "--data", "-d", help="JSON data or @filename"), etag: str = typer.Option("*", "--etag", help="ETag for optimistic concurrency"), format: Optional[str] = typer.Option(None, "--format", "-f", help="Output format: table, json, csv, ndjson, raw"), - publisher: Optional[str] = typer.Option(None, "--publisher"), - group: Optional[str] = typer.Option(None, "--group"), - version: Optional[str] = typer.Option(None, "--version"), + publisher: Optional[str] = typer.Option(None, "--publisher", hidden=True), + group: Optional[str] = typer.Option(None, "--group", hidden=True), + version: Optional[str] = typer.Option(None, "--version", hidden=True), yes: bool = typer.Option(False, "--yes", "-y", help="Skip the read-only-profile warning prompt"), ) -> None: """PATCH (update) an existing record.""" diff --git a/src/bcli_cli/commands/post_cmd.py b/src/bcli_cli/commands/post_cmd.py index 6d669ec..a66805b 100644 --- a/src/bcli_cli/commands/post_cmd.py +++ b/src/bcli_cli/commands/post_cmd.py @@ -20,9 +20,9 @@ def post_command( endpoint: str = typer.Argument(help="Entity set name"), data: str = typer.Option(..., "--data", "-d", help="JSON data or @filename"), format: Optional[str] = typer.Option(None, "--format", "-f", help="Output format: table, json, csv, ndjson, raw"), - publisher: Optional[str] = typer.Option(None, "--publisher"), - group: Optional[str] = typer.Option(None, "--group"), - version: Optional[str] = typer.Option(None, "--version"), + publisher: Optional[str] = typer.Option(None, "--publisher", hidden=True), + group: Optional[str] = typer.Option(None, "--group", hidden=True), + version: Optional[str] = typer.Option(None, "--version", hidden=True), yes: bool = typer.Option(False, "--yes", "-y", help="Skip the read-only-profile warning prompt"), ) -> None: """POST (create) a new record."""