feat: bcli pack system, ask oracle, and bcli-site v0 scaffold#22
Merged
Conversation
Lands the shared model-bound context layer (Part 0 of the bcli ask / agent plan). Standalone in this commit — no CLI consumers yet. The package shape and redaction posture are exercised by tests so the next two commits (CLI hook-up, tests) and the upcoming Part 2 `bcli ask` can build against a stable surface. - `bcli.context._protocol` — typed dataclasses (R4): ContextBundle, TokenBudget, BundlePolicy, BundleSource, RedactionRecord, ProfileSnapshot, HttpEvent, LastErrorRecord, Attachment. Frozen, JSON-serialisable, with `to_dict()` and `to_prompt_text()` renderers. - `bcli.context._redact` — three-layer redaction (R5) composing bcli/audit/_redact.py (layer 1 keys), bcli/telemetry/events.py (layer 2 patterns), and a new URL/GUID/attachment scrubber (layer 3). Every redaction lands in the bundle's audit trail with a stable `rule_id`. - `bcli.context._last_error` — captures BCLIError exits to `~/.config/bcli/last-error.json`. No tracebacks by default (R6); --debug runs also write a `last-error-debug.json` at mode 0600. - `bcli.context._http_tail` — opt-in RotatingFileHandler on `bcli.http` for the last ~200 events. Off by default. - `bcli.context._bundle` — pure builder: token-budgeted priority truncation (question > last_error > profile > http > describe > attachments), every step source-attributed.
- ContextConfig added to bcli.config._model with knobs: tail, redact_company_ids, attachment_max_bytes. Backwards-compatible via Pydantic default_factory. - Central BCLIError handler in bcli_cli.app calls bcli.context.capture_last_error so every error exits leaves a redacted JSON record at ~/.config/bcli/last-error.json (and the debug sidecar when --debug was active). - Root callback bootstraps the http-tail handler only when [context] tail = true. Default off — read-only home dirs, CI, and ephemeral containers see zero overhead. All hooks are try/except-wrapped so a misconfigured [context] section never blocks the CLI.
…rail 40 tests covering Part 0 surface: - test_protocol.py — ContextBundle round-trips through JSON; to_prompt_text renders priority sections; tracebacks excluded unless policy.include_debug. - test_redact.py — adversarial layered redaction: nested JSON with Bearer + JWT + client_secret; URL-encoded tokens; base64-wrapped JWTs; GUID policy gate; attachment truncation. Audit trail's rule_id set is asserted frozen + complete; redacted values never leak via repr. - test_last_error.py — capture without traceback by default, debug sidecar at mode 0600 only when --debug active, BC-message token pattern redaction, read-only config dir safety. - test_http_tail.py — handler idempotent, NDJSON round-trip, bad lines skipped, RotatingFileHandler size cap, limit slicing. - test_bundle.py — minimal-question bundle, last-error read from disk, budget-driven describe-excerpt drop, attachment scan + cap, layered audit trail completeness, --no-context policy path.
…3, R7, R8)
Lands the SDK surface for the pack mechanism. Three sub-modules
mirror the existing extract/telemetry package shape:
- `_protocol.py` — frozen dataclasses for Pack, PackManifest,
PackContents, AgentFragment (with per-fragment `targets:`,
default [agents] per R3), PackQuery, PackBatch,
PackRegistryPreset. VALID_TARGETS = {agents, claude}.
- `_loader.py` — parses pack.yaml + content files, validates
fragment targets, supports either list-of-strings or list-of-
dicts for each section.
- `_registry.py` — discovers packs from three sources: built-in
(packs/ at repo root, or bcli/packs/_builtin in the wheel),
entry-point group `bcli.packs`, and explicit paths. Broken packs
log a warning and are skipped.
- `_ledger.py` — frozen Ledger dataclass + round-trip JSON I/O at
~/.config/bcli/packs/<profile>/<pack>.json. Records every
artefact path with rendered_hash + owner for provenance-driven
uninstall (R2).
- `_installer.py` — plan_install / execute_install / install_pack /
uninstall_pack. Atomic writes via the same tmp+os.replace pattern
as skill_init_cmd. Conflict detection on registry presets refuses
overwrites unless --replace-owned --accept-conflicts (R7).
Marker blocks splice into AGENTS.md / CLAUDE.md per fragment's
targets list (R3); idempotent re-install via marker pair +
content_hash.
The package has no CLI consumers yet — the next commit wires up
bcli pack.
Typer group registered alongside skill / extract. Mirrors the skill_init UX: per-command confirmation prompts (skipped with --yes), dry-run mode renders the plan without touching disk, helpful diff when info-querying an installed pack. Flags on `install`: - --profile / -p (defaults to active profile) - --target (project root; defaults to CWD if .claude/ present, else $HOME) - --dry-run - --replace-owned + --accept-conflicts (R7 — two-flag override) - --yes / -y Flags on `uninstall`: - --profile / -p - --yes / -y Pack recommendations (R8) surface as a one-line hint after install: "This pack recommends enabling these `bcli ask` context providers". They are never auto-enabled — user config in [ask] context_providers is the binding decision.
Two reference packs in the OSS repo. Both fit through the standard v2.0 endpoint registry so they install cleanly against any BC tenant. starter-generic (v0.1.0) — the day-1 onboarding pack: - 6 saved queries — vendor-by-no, customer-by-no, open-pos, ar-aging-buckets, recent-posted-invoices, inventory-on-hand - 2 read-only batches — weekly-ar-snapshot, month-end-readonly-audit - 3 AGENTS.md fragments — endpoint-discovery, filter-syntax-cheatsheet, common-errors cronus-demo (v0.1.0) — Microsoft CRONUS-tenant demo: - 1 month-end batch (lifted from examples/month-end-cronus.yaml) - 1 sample queries file (lifted from examples/queries/sample.yaml) - 2 fragments — cronus-orientation and month-end-walkthrough Beautech-flavoured packs land in bcli-beautech-bootstrap via the bcli.packs entry-point group; this OSS repo only ships mechanism + two generic, standard-API-only examples.
- test_loader.py — manifest schema validation, missing fields, bad targets, broken YAML - test_registry.py — built-in discovery, skips non-pack dirs, warns on broken manifests - test_installer.py — install writes all artefacts with provenance; fragment targets route blocks to AGENTS.md vs CLAUDE.md; idempotent re-install; conflict refusal + --replace-owned override; uninstall round-trips; dry-run writes nothing - test_builtins.py — both shipped packs install end-to-end against a tmp config dir, co-exist on the same profile, and uninstall cleanly Also: - pyproject.toml — hatch force-include maps packs/ → bcli/packs/_builtin in the wheel so the shipped CLI sees them. sdist explicitly includes packs/, examples/, docs/. - CHANGELOG.md — Part 1 entry under [Unreleased].
…oviders Part 2 of the pack/ask/site plan. Single-call second-opinion oracle that bundles the operator's recent context (last-error, http-tail, profile, describe) via Part 0's bcli.context, ships it to a configured LLM, and prints the answer. SDK (bcli.ask): - _protocol.py — AskBackend Protocol + NullAsker (mirror of extract/_protocol.py) - _factory.py — get_asker dispatch with _BUILTIN_BACKENDS dict, module:Class fallback, Null fallback on any failure, one-shot warning (mirror of extract/_factory.py) - _claude.py — Anthropic backend; messages.create with bundle rendered as Markdown user-turn - _openai.py — OpenAI Responses API backend - _providers.py — bcli.ask.context_providers entry-point group (R8). Discovery + user-gated execution; pack recommendations surface as hints only and are NEVER auto-enabled. CLI (bcli_cli.commands.ask_cmd): - bcli ask "<question>" [--no-context] [--attach PATH] [--backend NAME] [--dry-run] [--include-bodies] [--include-debug] [--max-tokens N] Config: - AskConfig added under [ask] — backend, model, api_key_env, max_tokens, include_describe, include_http_tail, context_providers, base_url, organization. pyproject: - New extras [ask-claude], [ask-openai], [ask] (meta). - [dev] now also includes [ask]. Tests (16): - test_factory.py — Null fallback paths, custom backend import, malformed spec, from_config raise, missing from_config. - test_cli_dry_run.py — --dry-run prints the bundle without reaching the backend; attachments redacted + included. - test_providers.py — entry-point discovery, opt-in execution, provider failure silently logged, profile + last-error reach the provider callable.
Single-page landing site for bcli.sh. Files only — pnpm install
deliberately skipped since the agent harness has no guaranteed
network egress; first developer touch runs `pnpm install`.
Stack:
- Astro 4 + @astrojs/tailwind 5 + TypeScript 5 (strict)
- Tailwind palette/font scoped to bcli's terminal-y vibe (ink/mist
neutrals + accent blue/amber)
Content (R9-compliant — describes shipped features only):
- Hero: tagline "Business Central from the terminal. Designed for
humans and AI agents."
- Install: pip + uv tool install, noting the bc-cli vs bcli
distribution-name story
- Three example commands: pack install, saved query, ask oracle
- Features grid: packs, ask oracle, MCP server, discovery-first
- Footer with GitHub link
Build pipeline:
- .github/workflows/site.yml — actions/setup-node@v4 + corepack +
pnpm install + pnpm build on changes under bcli-site/**. Vercel
deploy step is wired but commented out (TODO once secrets exist).
- The workflow follows GitHub's command-injection guidance: secrets
flow through env: blocks, never directly interpolated into run:.
gitignore:
- bcli-site/{node_modules,dist,.astro,pnpm-lock.yaml,package-lock.json,yarn.lock}
…eads sidecar Two advisor-flagged bugs in bcli ask: 1. --no-context was leaking last-error.json into the bundle. build_bundle's "read from disk when caller passes None" default meant the CLI's `last_error=None` arg silently restored the prior failure even though --no-context says otherwise. Fix: new skip_last_error=True parameter on build_bundle that disables the implicit disk read; CLI threads it from the --no-context flag. 2. --include-debug was wired but inert. CLI now reads ~/.config/bcli/last-error-debug.json (mode 0600 sidecar) when the flag is set. Pairs with the existing policy.include_debug=True which already gates the traceback excerpt in to_prompt_text(). Two new regression tests in test_cli_dry_run.py: - test_no_context_suppresses_existing_last_error — writes a real last-error.json, asserts the phrase appears in the default bundle but is gone with --no-context. - test_include_debug_reads_traceback_sidecar — asserts Traceback absent without the flag, present with it. Also: IMPLEMENTATION-SUMMARY.md updated to record the fixes and a wheel-build smoke-test confirming packs ship at bcli/packs/_builtin/ in the produced wheel.
d306517 to
0cdd892
Compare
- pyproject.toml added [ask], [ask-claude], [ask-openai] extras and pulled bc-cli[ask] into [dev]; uv.lock wasn't regenerated so `uv sync --locked` rejected the lockfile on CI. - site.yml used node 20; corepack pulls pnpm 11.3.0 which requires node >=22.13 (imports node:sqlite, an only-in-22 builtin). Bumped to 22. No package additions — both ask extras reuse anthropic / openai already pulled in by the extract extras.
- 4 unused imports in tests (F401, autofixed by ruff) - 1 unused local variable in test_installer.py (F841, manual drop) - bcli-site/package.json: declare onlyBuiltDependencies=[esbuild, sharp] so pnpm 10+ runs their install scripts instead of failing the build. Both are first-party Astro deps with trusted publishers. 37 tests in test_packs/ + test_ask/ still pass post-cleanup.
pnpm 11.3 dropped the `pnpm.*` key from package.json (warning emitted in CI: "The 'pnpm' field in package.json is no longer read by pnpm"). The new home is pnpm-workspace.yaml even for single-package projects. Drop the dead package.json key. Both esbuild and sharp are first-party Astro deps that legitimately need install-time build scripts — without the allowlist, pnpm refuses to run them and the build fails with ERR_PNPM_IGNORED_BUILDS.
pnpm 11.3 didn't pick up onlyBuiltDependencies from a package-local pnpm-workspace.yaml — build still failed with ERR_PNPM_IGNORED_BUILDS. .npmrc is the canonical package-local pnpm config and is reliably read from the working directory pnpm install runs in.
pnpm 10+ enforces an "approve-builds" gate that requires onlyBuiltDependencies to be configured before install scripts (esbuild, sharp) can run. corepack downloaded pnpm 11.3.0 by default; neither package.json nor pnpm-workspace.yaml nor .npmrc made pnpm 11 honor the allowlist in CI (config read order varies across the 10/11 transition). pnpm 9.15.4 predates that strict-builds behavior and runs install scripts without the approval gate. corepack honors the packageManager field, so this pins deterministically across local dev and CI. .npmrc kept in place — harmless for pnpm 9, future-proof if we re-bump.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three coordinated additions toward bcli-as-an-agent (Parts 0–3 of an approved plan; Parts 4
bcli agentREPL and 5 cloud runtime are deferred to their own plans):bcli.context— shared infra. TypedContextBundle, 3-layer redaction (audit keys + telemetry patterns + new URL/GUID/attachment scrub), last-error capture without tracebacks by default, opt-in http-tail. Futurebcli askandbcli agentboth consume it.bcli pack— installable bundles of saved queries + batch templates + endpoint registry presets + AGENTS.md/CLAUDE.md fragments. JSON ledger at~/.config/bcli/packs/<profile>/<pack>.jsonfor safe install/uninstall, per-fragmenttargets:declaration (default[agents]), registry-preset conflict detection (--replace-owned --accept-conflicts), entry-point groupbcli.packs. Ships two built-in packs:starter-generic(6 queries against standard v2.0 endpoints, day-1 onboarding for any tenant) andcronus-demo(Microsoft CRONUS-sample-data workflows).bcli ask— one-shot LLM oracle that bundles last-error + http-tail + profile snapshot, redacts, and ships to Claude or OpenAI. Factory pattern mirrorsbcli.extractexactly. New entry-point groupbcli.ask.context_providerslets downstream packages (e.g.bcli-beautech-bootstrap) inject domain glossaries — opt-in via user config, never auto-enabled.bcli-site/— Astro + Tailwind v0 marketing scaffold (files only, nopnpm installrun). Hero + install + 3 example commands. GH Actions build workflow stubbed; Vercel deploy step commented for the user to wire up with secrets.Approved plan with full context including the /codex review (9 findings, all integrated as R1–R9):
/Users/igor/.claude/plans/how-would-the-bcli-zippy-lantern.md.Independently inspired by @steipete's pattern of "small sharp CLIs for every external tool, designed for humans AND agents."
What's in the 12 commits
f3f448fbcli.contextSDK — ContextBundle + 3-layer redactionfa0ebcb2eb26a3e3b1714e3eb1ebbcli.packsSDK — Pack/Manifest/Ledger + installer (R2, R3, R7, R8)c61627bbcli pack list / info / install / uninstallCLIa6a8c46starter-generic+cronus-demoebc054430e7545bcli askoracle — Claude/OpenAI backends + dry-run + R8 providersd306517--no-contexttruly suppresses last-error;--include-debugreads sidecar44a13f9bcli-sitev0 — Astro + Tailwind landing scaffoldbd633e9Test plan
uv pip install -e ".[dev]"cleanuv run pytest— 908 passed, 5 skipped, 0 faileduv run ruff check src/bcli/{context,packs,ask} src/bcli_cli/commands/{pack_cmd,ask_cmd}.py— all greenbcli pack list— both built-in packs visiblebcli pack info starter-generic— manifest reads correctly (3 fragments / 6 queries / 2 batches)bcli pack install starter-generic --dry-run— splices into AGENTS.md only (R3 default target), merges 6 queries, writes 2 batches; nothing actually writtenbcli ask --help— full flag surfacebcli ask --dry-run --no-context "smoke test"— bundle renders, no networkbcli pack install starter-genericfor real (against a sandbox profile), thenbcli skill installto project as slash commands, thenbcli pack uninstall starter-genericto verify ledger-driven removal[ask] backend = "claude"+ANTHROPIC_API_KEY, runbcli ask "what is bcli pack?"and confirm the model gets the bundlebcli-site/files parse (nopnpm installneeded for review; CI workflow is stubbed)Known follow-ups (called out in
IMPLEMENTATION-SUMMARY.md)bcli describeexcerpt isn't yet wired into the defaultbcli askcontext bundle. The stub exists; integration is a small follow-up PR.bcli pack uninstall --forcefor partial-marker recovery. Ledger code handles it; the UX flag isn't wired yet.Neither blocks landing this PR; both are clearly scoped follow-ups.
Out of scope
bcli-beautech-bootstrap), needs its own PR once this lands and ships to PyPI.bcli agentREPL loop (Part 4) and cloud runtime (Part 5) — deliberately deferred to their own plans.src/bcli/context/is shaped so Part 4 can import it directly.Release
Recommend bumping
pyproject.toml0.4.0 → 0.5.0 when merging, sincebcli pack,bcli ask, andbcli.contextare net-new SDK surface.