blq-cli 1.1.0__tar.gz → 1.2.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.
- {blq_cli-1.1.0 → blq_cli-1.2.0}/CHANGELOG.md +31 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/PKG-INFO +1 -1
- {blq_cli-1.1.0 → blq_cli-1.2.0}/pyproject.toml +1 -1
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/serve.py +138 -15
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_mcp_ci_tools.py +12 -1
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_mcp_server.py +12 -1
- blq_cli-1.2.0/tests/test_output_schemas.py +90 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.claude/hooks/blq-suggest.sh +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.github/workflows/ci.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.github/workflows/docs.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.github/workflows/publish.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.gitignore +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.mcp.json +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/.readthedocs.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/AGENT.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/AGENTS.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/CLAUDE.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/CONTRIBUTING.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/README.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/SKILL.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/api-reference.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/capture.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/ci.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/completions.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/errors.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/exec.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/filter.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/index.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/init.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/maintenance.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/query.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/registry.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/report.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/run.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/sql.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/status.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/sync.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/commands/watch.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-bird-migration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-commands.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-config-command.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-extensions.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-git-integration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-hooks-v2.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-live-inspection.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-parameterized-commands.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-run-args.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-sandbox-specs.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-sync.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/design-track-save.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/duck-hunt-v2-migration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/duck-hunt-v3-migration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/proposal-bird-v5.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/design/skill-inspect-enrichment.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/getting-started.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/index.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/integration.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/mcp.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/explore-bird-sqlite-backend.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/explore-nsjail-spack-package.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/patterns-integration-prompt.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plans/roadmap-to-1.0.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/plugin-guide.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/python-api.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/query-guide.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/requirements.txt +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/sandbox.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/schema-stability.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/schema.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/experiments/agent-build-test.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/mkdocs.yml +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/__main__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/bird.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/bird_schema.sql +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/cli.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/README.md +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/ci_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/clean_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/config_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/core.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/events.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/execution.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/hooks_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/hooks_gen.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/init_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/management.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/management_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/mcp_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/migrate.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/query_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/record_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/registry.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/report_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/sandbox_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/serve_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/sync_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/commands/watch_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/config_format.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/discovery.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/local_executor.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/ext/pipeline.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/git.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/github.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/locks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/output.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/query.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/runtime.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/schema.sql +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/execution.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/inspect.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/query.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/services/refs.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/storage.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/drone.yml.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/git_hook.sh.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/github_workflow.yml.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/templates/hook_script.sh.j2 +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq/user_config.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/engines.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/profile.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/source_annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/spec.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/strace_parser.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/tighten.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox/violations.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox_bwrap/args.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/src/blq_sandbox_systemd/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/__init__.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/conftest.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_attempts_outcomes.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_auto_init.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_autoprune.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_bird.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_bwrap_args.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_bwrap_engine.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ci.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ci_generators.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_command_args.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_command_lock_field.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_config_consumption.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_core.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_execution_locks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_exit_code_reason.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_discovery.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_integration.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_local_executor.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_pipeline.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_ext_types.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_git.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_hooks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_hooks_gen.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_import_capture_storage.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_inspect.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_json_null_filter.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_locks.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_log_content_persistence.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_mcp_merge.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_phase1_structured_output.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_phase2_command_registry.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_query_api.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_query_filter.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_record_invocation.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_report.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_runtime_config.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_cmd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_events.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_ext.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_profile.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_register.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_systemd.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_tighten.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_sandbox_violations.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_schema_repair.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_security_containment.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_execution.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_inspect.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_query.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_services_refs.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_source_annotator.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_storage.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_storage_prune.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_strace_parser.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_user_config.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/tests/test_watch.py +0 -0
- {blq_cli-1.1.0 → blq_cli-1.2.0}/uv.lock +0 -0
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.2.0
|
|
4
|
+
|
|
5
|
+
### Added — MCP tools publish the names of the keys they return (#47)
|
|
6
|
+
|
|
7
|
+
fastmcp derives `outputSchema` from a tool's return annotation, so
|
|
8
|
+
`dict[str, Any]` published `{"type": "object", "additionalProperties": true}`
|
|
9
|
+
— an object with no key names. A programmatic caller had to guess them, and a
|
|
10
|
+
wrong guess produces a program that validates, runs, and answers incorrectly.
|
|
11
|
+
Measured across four local models driven through lackpy: **0/24 correct while
|
|
12
|
+
17/24 called the right tool**, with `len()` on `events`' dict returning its key
|
|
13
|
+
count rather than its event count.
|
|
14
|
+
|
|
15
|
+
14 tools now return TypedDicts whose keys come from the implementations' own
|
|
16
|
+
return statements. Declaring the shapes immediately caught three places where
|
|
17
|
+
the code did not match a reasonable reading of it:
|
|
18
|
+
|
|
19
|
+
- `output.streams` is a list of stream names, not a mapping
|
|
20
|
+
- `query.rows` is positional value lists paired with `columns`, not per-row objects
|
|
21
|
+
- `ci_generate.available` is the registered command names, not a boolean
|
|
22
|
+
|
|
23
|
+
No data is dropped — keys a TypedDict does not declare still reach the client
|
|
24
|
+
in `structuredContent`.
|
|
25
|
+
|
|
26
|
+
**One behavioural note for Python callers:** with an `outputSchema` present,
|
|
27
|
+
fastmcp's own client deserializes `.data` into a generated model rather than a
|
|
28
|
+
dict, so `result.data["key"]` no longer works. Read `structured_content`, which
|
|
29
|
+
is the JSON a real MCP client receives over the wire. Consumers speaking
|
|
30
|
+
JSON-RPC (Claude Code, lackpy, any stdio client) are unaffected and strictly
|
|
31
|
+
better off.
|
|
32
|
+
|
|
33
|
+
|
|
3
34
|
## v1.1.0
|
|
4
35
|
|
|
5
36
|
Three fixes with one theme: blq reported success while losing the thing it was
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: blq-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Build Log Query - capture and query build/test logs with DuckDB
|
|
5
5
|
Project-URL: Homepage, https://blq-cli.readthedocs.io/
|
|
6
6
|
Project-URL: Repository, https://github.com/teaguesterling/blq-cli
|
|
@@ -34,7 +34,7 @@ import re as _re
|
|
|
34
34
|
import shlex
|
|
35
35
|
import subprocess
|
|
36
36
|
import sys
|
|
37
|
-
from typing import Any
|
|
37
|
+
from typing import Any, TypedDict
|
|
38
38
|
|
|
39
39
|
import pandas as pd # type: ignore[import-untyped]
|
|
40
40
|
from fastmcp import FastMCP
|
|
@@ -214,6 +214,129 @@ def _get_storage() -> BlqStorage:
|
|
|
214
214
|
return BlqStorage.open()
|
|
215
215
|
|
|
216
216
|
|
|
217
|
+
# ── Published result shapes ──────────────────────────────────────────
|
|
218
|
+
#
|
|
219
|
+
# fastmcp derives each tool's `outputSchema` from its return annotation. A bare
|
|
220
|
+
# `dict[str, Any]` becomes {"type": "object", "additionalProperties": true} —
|
|
221
|
+
# an object with no key names, which tells a programmatic caller nothing it can
|
|
222
|
+
# write code against. Measured cost, from four local models driven against this
|
|
223
|
+
# suite: 0/24 correct while 17/24 called the correct tool, because the models
|
|
224
|
+
# picked the right tool and guessed the return shape wrong. `len()` on `events`'
|
|
225
|
+
# dict returns its key count, not its event count.
|
|
226
|
+
#
|
|
227
|
+
# These TypedDicts exist to put the key names on the wire. Keys are taken from
|
|
228
|
+
# the implementations' own `return {...}` statements. `total=False` throughout:
|
|
229
|
+
# most of these tools return an `error` key on failure paths and omit it
|
|
230
|
+
# otherwise, and several have distinct success shapes (`events` returns batch
|
|
231
|
+
# keys under `run_ids`), so declaring everything required would publish a
|
|
232
|
+
# contract the code does not keep.
|
|
233
|
+
|
|
234
|
+
class EventsResult(TypedDict, total=False):
|
|
235
|
+
"""`events` — single-run keys, plus the batch keys used with `run_ids`."""
|
|
236
|
+
events: list[dict[str, Any]]
|
|
237
|
+
total_count: int
|
|
238
|
+
runs: list[dict[str, Any]]
|
|
239
|
+
run_count: int
|
|
240
|
+
total_events: int
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class StatusResult(TypedDict, total=False):
|
|
244
|
+
sources: list[dict[str, Any]]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class HistoryResult(TypedDict, total=False):
|
|
248
|
+
runs: list[dict[str, Any]]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class QueryResult(TypedDict, total=False):
|
|
252
|
+
# Positional value lists paired with `columns`, not per-row objects.
|
|
253
|
+
rows: list[list[Any]]
|
|
254
|
+
columns: list[str]
|
|
255
|
+
row_count: int
|
|
256
|
+
error: str
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class InspectResult(TypedDict, total=False):
|
|
260
|
+
events: list[dict[str, Any]]
|
|
261
|
+
found: int
|
|
262
|
+
total: int
|
|
263
|
+
error: str
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class OutputResult(TypedDict, total=False):
|
|
267
|
+
# A LIST of stream names, not a mapping — declaring it as an object made
|
|
268
|
+
# fastmcp reject every call with "[] is not of type 'object'". Worth noting
|
|
269
|
+
# that the schema caught the wrong assumption immediately, which is most of
|
|
270
|
+
# the argument for declaring these at all.
|
|
271
|
+
streams: list[str]
|
|
272
|
+
ref: str
|
|
273
|
+
run_id: int
|
|
274
|
+
error: str
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class DiffResult(TypedDict, total=False):
|
|
278
|
+
new: list[dict[str, Any]]
|
|
279
|
+
fixed: list[dict[str, Any]]
|
|
280
|
+
summary: dict[str, Any]
|
|
281
|
+
error: str
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class CommandsResult(TypedDict, total=False):
|
|
285
|
+
commands: list[dict[str, Any]]
|
|
286
|
+
error: str
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class RunResult(TypedDict, total=False):
|
|
290
|
+
# Nullable, not merely optional: the failure paths return these keys set to
|
|
291
|
+
# None rather than omitting them, and `total=False` only makes a key
|
|
292
|
+
# absent-able.
|
|
293
|
+
run_ref: str | None
|
|
294
|
+
status: str | None
|
|
295
|
+
exit_code: int | None
|
|
296
|
+
summary: dict[str, Any]
|
|
297
|
+
results: list[dict[str, Any]]
|
|
298
|
+
commands_requested: int
|
|
299
|
+
commands_run: int
|
|
300
|
+
error: str
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class ExecResult(TypedDict, total=False):
|
|
304
|
+
run_ref: str | None
|
|
305
|
+
status: str | None
|
|
306
|
+
exit_code: int | None
|
|
307
|
+
summary: dict[str, Any]
|
|
308
|
+
errors: list[dict[str, Any]]
|
|
309
|
+
error: str
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class CiCheckResult(TypedDict, total=False):
|
|
313
|
+
status: str | None
|
|
314
|
+
mode: str | None
|
|
315
|
+
has_errors: bool
|
|
316
|
+
current_errors: int
|
|
317
|
+
current_run_id: int
|
|
318
|
+
error: str
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class CiGenerateResult(TypedDict, total=False):
|
|
322
|
+
# The names of the registered commands, not a yes/no — read from the
|
|
323
|
+
# implementation rather than inferred from the key name, which is how the
|
|
324
|
+
# first three drafts of these annotations got it wrong.
|
|
325
|
+
available: list[str]
|
|
326
|
+
count: int
|
|
327
|
+
scripts: list[dict[str, Any]]
|
|
328
|
+
shell: str | None
|
|
329
|
+
error: str
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class CommandMutationResult(TypedDict, total=False):
|
|
333
|
+
"""Shared by `register_command` / `unregister_command`."""
|
|
334
|
+
success: bool
|
|
335
|
+
message: str
|
|
336
|
+
existing_name: str
|
|
337
|
+
error: str
|
|
338
|
+
|
|
339
|
+
|
|
217
340
|
def _get_suppressed_list(include_suppressed: bool = False) -> list[str] | None:
|
|
218
341
|
"""Get list of suppressed fingerprints, or None if no filtering."""
|
|
219
342
|
if include_suppressed:
|
|
@@ -2282,7 +2405,7 @@ def run(
|
|
|
2282
2405
|
# Batch mode parameters
|
|
2283
2406
|
commands: list[str] | None = None,
|
|
2284
2407
|
stop_on_failure: bool = True,
|
|
2285
|
-
) ->
|
|
2408
|
+
) -> RunResult:
|
|
2286
2409
|
"""Run a registered command and capture its output.
|
|
2287
2410
|
|
|
2288
2411
|
Can run a single command or multiple commands in sequence (batch mode).
|
|
@@ -2340,7 +2463,7 @@ def exec(
|
|
|
2340
2463
|
timeout: int | None = None,
|
|
2341
2464
|
shell: bool = False,
|
|
2342
2465
|
lines: str | None = None,
|
|
2343
|
-
) ->
|
|
2466
|
+
) -> ExecResult:
|
|
2344
2467
|
"""Execute an ad-hoc shell command and capture its output.
|
|
2345
2468
|
|
|
2346
2469
|
IMPORTANT: Do NOT use shell pipes, redirects, or command chains
|
|
@@ -2379,7 +2502,7 @@ def query(
|
|
|
2379
2502
|
sql: str | None = None,
|
|
2380
2503
|
filter: str | None = None,
|
|
2381
2504
|
limit: int = 100,
|
|
2382
|
-
) ->
|
|
2505
|
+
) -> QueryResult:
|
|
2383
2506
|
"""Query stored log events with SQL or simple filter expressions.
|
|
2384
2507
|
|
|
2385
2508
|
Use either `sql` for raw SQL queries or `filter` for simple expressions.
|
|
@@ -2421,7 +2544,7 @@ def events(
|
|
|
2421
2544
|
# Batch mode
|
|
2422
2545
|
run_ids: list[int] | None = None,
|
|
2423
2546
|
limit_per_run: int = 10,
|
|
2424
|
-
) ->
|
|
2547
|
+
) -> EventsResult:
|
|
2425
2548
|
"""Get events with optional severity filter.
|
|
2426
2549
|
|
|
2427
2550
|
For errors only: use severity="error"
|
|
@@ -2493,7 +2616,7 @@ def inspect(
|
|
|
2493
2616
|
include_fingerprint_history: bool = False,
|
|
2494
2617
|
# Batch mode
|
|
2495
2618
|
refs: list[str] | None = None,
|
|
2496
|
-
) ->
|
|
2619
|
+
) -> InspectResult:
|
|
2497
2620
|
"""Get comprehensive event details with context and enrichment.
|
|
2498
2621
|
|
|
2499
2622
|
Returns event details with optional context and enrichment:
|
|
@@ -2572,7 +2695,7 @@ def output(
|
|
|
2572
2695
|
context: int = 0,
|
|
2573
2696
|
lines: str | None = None,
|
|
2574
2697
|
debug_formats: bool = False,
|
|
2575
|
-
) ->
|
|
2698
|
+
) -> OutputResult:
|
|
2576
2699
|
"""Get raw output for a run with optional search and filtering.
|
|
2577
2700
|
|
|
2578
2701
|
Retrieves the captured stdout/stderr from a command execution.
|
|
@@ -2606,7 +2729,7 @@ def output(
|
|
|
2606
2729
|
|
|
2607
2730
|
|
|
2608
2731
|
@mcp.tool()
|
|
2609
|
-
def status() ->
|
|
2732
|
+
def status() -> StatusResult:
|
|
2610
2733
|
"""Get current status summary of all sources.
|
|
2611
2734
|
|
|
2612
2735
|
Returns:
|
|
@@ -2975,7 +3098,7 @@ def _last_impl(
|
|
|
2975
3098
|
@mcp.tool()
|
|
2976
3099
|
def history(
|
|
2977
3100
|
limit: int | None = None, source: str | None = None, status: str | None = None
|
|
2978
|
-
) ->
|
|
3101
|
+
) -> HistoryResult:
|
|
2979
3102
|
"""Get run history.
|
|
2980
3103
|
|
|
2981
3104
|
Args:
|
|
@@ -2995,7 +3118,7 @@ def history(
|
|
|
2995
3118
|
|
|
2996
3119
|
|
|
2997
3120
|
@mcp.tool()
|
|
2998
|
-
def diff(run1: int, run2: int) ->
|
|
3121
|
+
def diff(run1: int, run2: int) -> DiffResult:
|
|
2999
3122
|
"""Compare errors between two runs.
|
|
3000
3123
|
|
|
3001
3124
|
Args:
|
|
@@ -3023,7 +3146,7 @@ def register_command(
|
|
|
3023
3146
|
lines: str | None = None,
|
|
3024
3147
|
sandbox: str | dict[str, Any] | None = None,
|
|
3025
3148
|
lock: str | None = None,
|
|
3026
|
-
) ->
|
|
3149
|
+
) -> CommandMutationResult:
|
|
3027
3150
|
"""Register a new command.
|
|
3028
3151
|
|
|
3029
3152
|
If a command with the same name or same command string already exists,
|
|
@@ -3075,7 +3198,7 @@ def register_command(
|
|
|
3075
3198
|
|
|
3076
3199
|
|
|
3077
3200
|
@mcp.tool()
|
|
3078
|
-
def unregister_command(name: str) ->
|
|
3201
|
+
def unregister_command(name: str) -> CommandMutationResult:
|
|
3079
3202
|
"""Remove a registered command.
|
|
3080
3203
|
|
|
3081
3204
|
Note: This tool can be disabled via mcp.disabled_tools config.
|
|
@@ -3091,7 +3214,7 @@ def unregister_command(name: str) -> dict[str, Any]:
|
|
|
3091
3214
|
|
|
3092
3215
|
|
|
3093
3216
|
@mcp.tool()
|
|
3094
|
-
def commands() ->
|
|
3217
|
+
def commands() -> CommandsResult:
|
|
3095
3218
|
"""List all registered commands.
|
|
3096
3219
|
|
|
3097
3220
|
Returns:
|
|
@@ -3592,7 +3715,7 @@ def ci_check(
|
|
|
3592
3715
|
baseline: str | None = None,
|
|
3593
3716
|
fail_on_any: bool = False,
|
|
3594
3717
|
run_id: int | None = None,
|
|
3595
|
-
) ->
|
|
3718
|
+
) -> CiCheckResult:
|
|
3596
3719
|
"""Check for regressions compared to a baseline run.
|
|
3597
3720
|
|
|
3598
3721
|
Compares error fingerprints between the current run and a baseline to
|
|
@@ -3619,7 +3742,7 @@ def ci_check(
|
|
|
3619
3742
|
def ci_generate(
|
|
3620
3743
|
commands: list[str] | None = None,
|
|
3621
3744
|
shell: str = "bash",
|
|
3622
|
-
) ->
|
|
3745
|
+
) -> CiGenerateResult:
|
|
3623
3746
|
"""Generate standalone shell scripts from registered commands.
|
|
3624
3747
|
|
|
3625
3748
|
Creates shell script content for CI environments where blq may not be
|
|
@@ -16,7 +16,18 @@ if fastmcp:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def get_data(result):
|
|
19
|
-
"""Extract data from CallToolResult.
|
|
19
|
+
"""Extract data from CallToolResult as a plain dict.
|
|
20
|
+
|
|
21
|
+
Prefer `structured_content`: once a tool declares an outputSchema (which
|
|
22
|
+
they now do, so callers can see the key names — see #47), fastmcp
|
|
23
|
+
deserializes `.data` into a generated model rather than a dict, and a model
|
|
24
|
+
is not subscriptable. `structured_content` is the JSON payload a real MCP
|
|
25
|
+
client receives over the wire, including any keys the schema does not
|
|
26
|
+
declare, so asserting against it tests what a consumer actually gets.
|
|
27
|
+
"""
|
|
28
|
+
sc = getattr(result, "structured_content", None)
|
|
29
|
+
if sc is not None:
|
|
30
|
+
return sc
|
|
20
31
|
if hasattr(result, "data"):
|
|
21
32
|
return result.data
|
|
22
33
|
return result
|
|
@@ -16,7 +16,18 @@ if fastmcp:
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def get_data(result):
|
|
19
|
-
"""Extract data from CallToolResult.
|
|
19
|
+
"""Extract data from CallToolResult as a plain dict.
|
|
20
|
+
|
|
21
|
+
Prefer `structured_content`: once a tool declares an outputSchema (which
|
|
22
|
+
they now do, so callers can see the key names — see #47), fastmcp
|
|
23
|
+
deserializes `.data` into a generated model rather than a dict, and a model
|
|
24
|
+
is not subscriptable. `structured_content` is the JSON payload a real MCP
|
|
25
|
+
client receives over the wire, including any keys the schema does not
|
|
26
|
+
declare, so asserting against it tests what a consumer actually gets.
|
|
27
|
+
"""
|
|
28
|
+
sc = getattr(result, "structured_content", None)
|
|
29
|
+
if sc is not None:
|
|
30
|
+
return sc
|
|
20
31
|
if hasattr(result, "data"):
|
|
21
32
|
return result.data
|
|
22
33
|
return result
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""MCP tools must publish the NAMES of the keys they return.
|
|
2
|
+
|
|
3
|
+
fastmcp derives `outputSchema` from a tool's return annotation. A bare
|
|
4
|
+
`dict[str, Any]` becomes:
|
|
5
|
+
|
|
6
|
+
{"type": "object", "additionalProperties": true}
|
|
7
|
+
|
|
8
|
+
which tells a caller it will receive *an object* and nothing more. A
|
|
9
|
+
programmatic consumer then has to guess the keys, and a wrong guess produces
|
|
10
|
+
a program that validates, runs, and answers incorrectly.
|
|
11
|
+
|
|
12
|
+
Measured by a session driving four local models against this suite through
|
|
13
|
+
lackpy, which generates one restricted-Python program per intent:
|
|
14
|
+
|
|
15
|
+
0/24 correct while 17/24 called the correct tool
|
|
16
|
+
|
|
17
|
+
`len()` on `events`' returned dict gives 2 — its key count — where the answer
|
|
18
|
+
was 1. Annotating the return types is the whole fix: the wire format already
|
|
19
|
+
carries the property names, nothing else changes, and it helps every
|
|
20
|
+
programmatic caller rather than one harness.
|
|
21
|
+
|
|
22
|
+
See #47.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
fastmcp = pytest.importorskip("fastmcp")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def _schema_for(tool_name):
|
|
31
|
+
from fastmcp import Client
|
|
32
|
+
from blq.serve import mcp
|
|
33
|
+
|
|
34
|
+
async with Client(mcp) as client:
|
|
35
|
+
for tool in await client.list_tools():
|
|
36
|
+
if tool.name == tool_name:
|
|
37
|
+
return tool.outputSchema
|
|
38
|
+
raise AssertionError(f"tool {tool_name!r} not published")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _properties(schema):
|
|
42
|
+
"""The named properties a caller can rely on.
|
|
43
|
+
|
|
44
|
+
fastmcp wraps a non-object return in {"result": ...}; for a TypedDict it
|
|
45
|
+
emits the properties directly. Accept either, and look through the wrapper
|
|
46
|
+
so the assertion is about the tool's shape, not fastmcp's packaging.
|
|
47
|
+
"""
|
|
48
|
+
assert schema is not None, "tool publishes no outputSchema at all"
|
|
49
|
+
props = schema.get("properties") or {}
|
|
50
|
+
if set(props) == {"result"}:
|
|
51
|
+
inner = props["result"]
|
|
52
|
+
props = inner.get("properties") or {}
|
|
53
|
+
return props
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Keys taken from the impl's own `return {...}` statements, not from docs.
|
|
57
|
+
EXPECTED = {
|
|
58
|
+
"events": {"events", "total_count"},
|
|
59
|
+
"status": {"sources"},
|
|
60
|
+
"history": {"runs"},
|
|
61
|
+
"query": {"rows", "columns", "row_count"},
|
|
62
|
+
"inspect": {"events", "found", "total"},
|
|
63
|
+
"output": {"streams", "ref"},
|
|
64
|
+
"diff": {"new", "fixed", "summary"},
|
|
65
|
+
"commands": {"commands"},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
@pytest.mark.parametrize("tool_name,expected", sorted(EXPECTED.items()))
|
|
71
|
+
async def test_tool_publishes_named_properties(tool_name, expected):
|
|
72
|
+
schema = await _schema_for(tool_name)
|
|
73
|
+
props = _properties(schema)
|
|
74
|
+
missing = expected - set(props)
|
|
75
|
+
assert not missing, (
|
|
76
|
+
f"{tool_name} publishes no property names for {sorted(missing)} — "
|
|
77
|
+
f"got {sorted(props) or 'nothing'}. A caller cannot write code "
|
|
78
|
+
f"against this."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
@pytest.mark.parametrize("tool_name", sorted(EXPECTED))
|
|
84
|
+
async def test_schema_is_not_an_open_object(tool_name):
|
|
85
|
+
"""`additionalProperties: true` with no properties is the defect."""
|
|
86
|
+
schema = await _schema_for(tool_name)
|
|
87
|
+
props = _properties(schema)
|
|
88
|
+
assert props, (
|
|
89
|
+
f"{tool_name}'s outputSchema names no properties at all"
|
|
90
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-22-extension-system-design.md
RENAMED
|
File without changes
|
{blq_cli-1.1.0 → blq_cli-1.2.0}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|