blq-cli 1.1.0__tar.gz → 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. {blq_cli-1.1.0 → blq_cli-1.2.1}/CHANGELOG.md +54 -0
  2. {blq_cli-1.1.0 → blq_cli-1.2.1}/PKG-INFO +2 -1
  3. {blq_cli-1.1.0 → blq_cli-1.2.1}/pyproject.toml +4 -1
  4. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/serve.py +147 -14
  5. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_mcp_ci_tools.py +12 -1
  6. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_mcp_server.py +12 -1
  7. blq_cli-1.2.1/tests/test_output_schemas.py +90 -0
  8. {blq_cli-1.1.0 → blq_cli-1.2.1}/.claude/hooks/blq-suggest.sh +0 -0
  9. {blq_cli-1.1.0 → blq_cli-1.2.1}/.github/workflows/ci.yml +0 -0
  10. {blq_cli-1.1.0 → blq_cli-1.2.1}/.github/workflows/docs.yml +0 -0
  11. {blq_cli-1.1.0 → blq_cli-1.2.1}/.github/workflows/publish.yml +0 -0
  12. {blq_cli-1.1.0 → blq_cli-1.2.1}/.gitignore +0 -0
  13. {blq_cli-1.1.0 → blq_cli-1.2.1}/.mcp.json +0 -0
  14. {blq_cli-1.1.0 → blq_cli-1.2.1}/.readthedocs.yml +0 -0
  15. {blq_cli-1.1.0 → blq_cli-1.2.1}/AGENT.md +0 -0
  16. {blq_cli-1.1.0 → blq_cli-1.2.1}/AGENTS.md +0 -0
  17. {blq_cli-1.1.0 → blq_cli-1.2.1}/CLAUDE.md +0 -0
  18. {blq_cli-1.1.0 → blq_cli-1.2.1}/CONTRIBUTING.md +0 -0
  19. {blq_cli-1.1.0 → blq_cli-1.2.1}/README.md +0 -0
  20. {blq_cli-1.1.0 → blq_cli-1.2.1}/SKILL.md +0 -0
  21. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/api-reference.md +0 -0
  22. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/capture.md +0 -0
  23. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/ci.md +0 -0
  24. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/completions.md +0 -0
  25. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/errors.md +0 -0
  26. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/exec.md +0 -0
  27. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/filter.md +0 -0
  28. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/index.md +0 -0
  29. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/init.md +0 -0
  30. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/maintenance.md +0 -0
  31. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/query.md +0 -0
  32. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/registry.md +0 -0
  33. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/report.md +0 -0
  34. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/run.md +0 -0
  35. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/sql.md +0 -0
  36. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/status.md +0 -0
  37. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/sync.md +0 -0
  38. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/commands/watch.md +0 -0
  39. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-bird-migration.md +0 -0
  40. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-commands.md +0 -0
  41. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-config-command.md +0 -0
  42. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-extensions.md +0 -0
  43. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-git-integration.md +0 -0
  44. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-hooks-v2.md +0 -0
  45. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-live-inspection.md +0 -0
  46. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-parameterized-commands.md +0 -0
  47. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-run-args.md +0 -0
  48. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-sandbox-specs.md +0 -0
  49. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-sync.md +0 -0
  50. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/design-track-save.md +0 -0
  51. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/duck-hunt-v2-migration.md +0 -0
  52. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/duck-hunt-v3-migration.md +0 -0
  53. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/proposal-bird-v5.md +0 -0
  54. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/design/skill-inspect-enrichment.md +0 -0
  55. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/getting-started.md +0 -0
  56. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/index.md +0 -0
  57. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/integration.md +0 -0
  58. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/mcp.md +0 -0
  59. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/explore-bird-sqlite-backend.md +0 -0
  60. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/explore-nsjail-python-wrapper.md +0 -0
  61. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/explore-nsjail-spack-package.md +0 -0
  62. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/patterns-integration-prompt.md +0 -0
  63. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plans/roadmap-to-1.0.md +0 -0
  64. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/plugin-guide.md +0 -0
  65. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/python-api.md +0 -0
  66. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/query-guide.md +0 -0
  67. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/requirements.txt +0 -0
  68. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/sandbox.md +0 -0
  69. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/schema-stability.md +0 -0
  70. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/schema.md +0 -0
  71. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-22-extension-system.md +0 -0
  72. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-28-bwrap-engine.md +0 -0
  73. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-28-command-locks.md +0 -0
  74. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-29-annotators-and-tighten.md +0 -0
  75. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-29-service-layer-phase1.md +0 -0
  76. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/plans/2026-03-29-strace-profiling.md +0 -0
  77. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/specs/2026-03-22-extension-system-design.md +0 -0
  78. {blq_cli-1.1.0 → blq_cli-1.2.1}/docs/superpowers/specs/2026-03-29-unified-service-layer-design.md +0 -0
  79. {blq_cli-1.1.0 → blq_cli-1.2.1}/experiments/agent-build-test.md +0 -0
  80. {blq_cli-1.1.0 → blq_cli-1.2.1}/mkdocs.yml +0 -0
  81. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/__init__.py +0 -0
  82. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/__main__.py +0 -0
  83. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/bird.py +0 -0
  84. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/bird_schema.sql +0 -0
  85. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/cli.py +0 -0
  86. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/README.md +0 -0
  87. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/__init__.py +0 -0
  88. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/ci_cmd.py +0 -0
  89. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/clean_cmd.py +0 -0
  90. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/config_cmd.py +0 -0
  91. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/core.py +0 -0
  92. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/events.py +0 -0
  93. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/execution.py +0 -0
  94. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/hooks_cmd.py +0 -0
  95. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/hooks_gen.py +0 -0
  96. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/init_cmd.py +0 -0
  97. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/management.py +0 -0
  98. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/management_cmd.py +0 -0
  99. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/mcp_cmd.py +0 -0
  100. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/migrate.py +0 -0
  101. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/query_cmd.py +0 -0
  102. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/record_cmd.py +0 -0
  103. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/registry.py +0 -0
  104. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/report_cmd.py +0 -0
  105. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/sandbox_cmd.py +0 -0
  106. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/serve_cmd.py +0 -0
  107. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/sync_cmd.py +0 -0
  108. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/commands/watch_cmd.py +0 -0
  109. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/config_format.py +0 -0
  110. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/__init__.py +0 -0
  111. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/annotator.py +0 -0
  112. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/discovery.py +0 -0
  113. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/local_executor.py +0 -0
  114. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/ext/pipeline.py +0 -0
  115. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/git.py +0 -0
  116. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/github.py +0 -0
  117. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/locks.py +0 -0
  118. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/output.py +0 -0
  119. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/query.py +0 -0
  120. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/runtime.py +0 -0
  121. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/schema.sql +0 -0
  122. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/__init__.py +0 -0
  123. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/execution.py +0 -0
  124. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/inspect.py +0 -0
  125. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/query.py +0 -0
  126. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/services/refs.py +0 -0
  127. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/storage.py +0 -0
  128. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/drone.yml.j2 +0 -0
  129. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/git_hook.sh.j2 +0 -0
  130. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/github_workflow.yml.j2 +0 -0
  131. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/gitlab_ci.yml.j2 +0 -0
  132. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/templates/hook_script.sh.j2 +0 -0
  133. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq/user_config.py +0 -0
  134. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/__init__.py +0 -0
  135. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/engines.py +0 -0
  136. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/profile.py +0 -0
  137. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/source_annotator.py +0 -0
  138. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/spec.py +0 -0
  139. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/strace_parser.py +0 -0
  140. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/tighten.py +0 -0
  141. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox/violations.py +0 -0
  142. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox_bwrap/__init__.py +0 -0
  143. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox_bwrap/args.py +0 -0
  144. {blq_cli-1.1.0 → blq_cli-1.2.1}/src/blq_sandbox_systemd/__init__.py +0 -0
  145. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/__init__.py +0 -0
  146. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/conftest.py +0 -0
  147. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/sql/test_duck_hunt_v2_migration.sql +0 -0
  148. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_annotator.py +0 -0
  149. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_attempts_outcomes.py +0 -0
  150. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_auto_init.py +0 -0
  151. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_autoprune.py +0 -0
  152. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_bird.py +0 -0
  153. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_bwrap_args.py +0 -0
  154. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_bwrap_engine.py +0 -0
  155. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ci.py +0 -0
  156. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ci_generators.py +0 -0
  157. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_command_args.py +0 -0
  158. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_command_lock_field.py +0 -0
  159. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_config_consumption.py +0 -0
  160. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_core.py +0 -0
  161. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_execution_locks.py +0 -0
  162. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_exit_code_reason.py +0 -0
  163. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_discovery.py +0 -0
  164. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_integration.py +0 -0
  165. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_local_executor.py +0 -0
  166. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_pipeline.py +0 -0
  167. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_ext_types.py +0 -0
  168. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_git.py +0 -0
  169. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_hooks.py +0 -0
  170. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_hooks_gen.py +0 -0
  171. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_import_capture_storage.py +0 -0
  172. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_inspect.py +0 -0
  173. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_json_null_filter.py +0 -0
  174. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_locks.py +0 -0
  175. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_log_content_persistence.py +0 -0
  176. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_mcp_merge.py +0 -0
  177. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_phase1_structured_output.py +0 -0
  178. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_phase2_command_registry.py +0 -0
  179. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_query_api.py +0 -0
  180. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_query_filter.py +0 -0
  181. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_record_invocation.py +0 -0
  182. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_report.py +0 -0
  183. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_runtime_config.py +0 -0
  184. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox.py +0 -0
  185. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_cmd.py +0 -0
  186. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_events.py +0 -0
  187. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_ext.py +0 -0
  188. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_profile.py +0 -0
  189. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_register.py +0 -0
  190. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_systemd.py +0 -0
  191. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_tighten.py +0 -0
  192. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_sandbox_violations.py +0 -0
  193. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_schema_repair.py +0 -0
  194. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_security_containment.py +0 -0
  195. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_execution.py +0 -0
  196. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_inspect.py +0 -0
  197. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_query.py +0 -0
  198. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_services_refs.py +0 -0
  199. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_source_annotator.py +0 -0
  200. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_storage.py +0 -0
  201. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_storage_prune.py +0 -0
  202. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_strace_parser.py +0 -0
  203. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_user_config.py +0 -0
  204. {blq_cli-1.1.0 → blq_cli-1.2.1}/tests/test_watch.py +0 -0
  205. {blq_cli-1.1.0 → blq_cli-1.2.1}/uv.lock +0 -0
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.2.1
4
+
5
+ ### Fixed — 1.2.0 was broken on Python 3.10 and 3.11
6
+
7
+ The TypedDicts added in 1.2.0 were imported from `typing`. pydantic refuses to
8
+ build a schema from `typing.TypedDict` below 3.12 ("Please use
9
+ `typing_extensions.TypedDict` instead"), so importing `blq.serve` raised on
10
+ every supported Python under 3.12 — which is to say the MCP server did not
11
+ start at all there.
12
+
13
+ `requires-python` is `>=3.10`, so 1.2.0 should not have shipped. It passed
14
+ local checks because the development interpreter was 3.14; CI caught it on the
15
+ 3.10 matrix entry, but the publish workflow triggers on release rather than on
16
+ a green CI run, so the package reached PyPI anyway.
17
+
18
+ Now imported from `typing_extensions` below 3.12, with the backport declared as
19
+ a conditional dependency. Verified against a real 3.11 interpreter: 1382 tests
20
+ pass, where 1.2.0 could not import.
21
+
22
+ **If you installed 1.2.0 on Python 3.10 or 3.11, upgrade.** On 3.12+ it was
23
+ unaffected.
24
+
25
+
26
+ ## v1.2.0
27
+
28
+ ### Added — MCP tools publish the names of the keys they return (#47)
29
+
30
+ fastmcp derives `outputSchema` from a tool's return annotation, so
31
+ `dict[str, Any]` published `{"type": "object", "additionalProperties": true}`
32
+ — an object with no key names. A programmatic caller had to guess them, and a
33
+ wrong guess produces a program that validates, runs, and answers incorrectly.
34
+ Measured across four local models driven through lackpy: **0/24 correct while
35
+ 17/24 called the right tool**, with `len()` on `events`' dict returning its key
36
+ count rather than its event count.
37
+
38
+ 14 tools now return TypedDicts whose keys come from the implementations' own
39
+ return statements. Declaring the shapes immediately caught three places where
40
+ the code did not match a reasonable reading of it:
41
+
42
+ - `output.streams` is a list of stream names, not a mapping
43
+ - `query.rows` is positional value lists paired with `columns`, not per-row objects
44
+ - `ci_generate.available` is the registered command names, not a boolean
45
+
46
+ No data is dropped — keys a TypedDict does not declare still reach the client
47
+ in `structuredContent`.
48
+
49
+ **One behavioural note for Python callers:** with an `outputSchema` present,
50
+ fastmcp's own client deserializes `.data` into a generated model rather than a
51
+ dict, so `result.data["key"]` no longer works. Read `structured_content`, which
52
+ is the JSON a real MCP client receives over the wire. Consumers speaking
53
+ JSON-RPC (Claude Code, lackpy, any stdio client) are unaffected and strictly
54
+ better off.
55
+
56
+
3
57
  ## v1.1.0
4
58
 
5
59
  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.1.0
3
+ Version: 1.2.1
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
@@ -27,6 +27,7 @@ Requires-Dist: pyyaml>=6.0
27
27
  Requires-Dist: tabulate>=0.9.0
28
28
  Requires-Dist: tomli-w>=1.0.0
29
29
  Requires-Dist: tomli>=2.0.0; python_version < '3.11'
30
+ Requires-Dist: typing-extensions>=4.0; python_version < '3.12'
30
31
  Requires-Dist: watchdog>=3.0.0
31
32
  Provides-Extra: dev
32
33
  Requires-Dist: fastmcp>=2.0.0; extra == 'dev'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "blq-cli"
7
- version = "1.1.0"
7
+ version = "1.2.1"
8
8
  description = "Build Log Query - capture and query build/test logs with DuckDB"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -33,6 +33,9 @@ dependencies = [
33
33
  "pyyaml>=6.0",
34
34
  "tabulate>=0.9.0",
35
35
  "tomli>=2.0.0;python_version<'3.11'",
36
+ # pydantic will not build a schema from `typing.TypedDict` below 3.12, so
37
+ # the MCP tools' output schemas need the backport there.
38
+ "typing_extensions>=4.0;python_version<'3.12'",
36
39
  "tomli-w>=1.0.0",
37
40
  "watchdog>=3.0.0",
38
41
  ]
@@ -36,6 +36,16 @@ import subprocess
36
36
  import sys
37
37
  from typing import Any
38
38
 
39
+ # pydantic rejects `typing.TypedDict` below 3.12 and refuses to build a schema
40
+ # from it ("Please use typing_extensions.TypedDict instead"). This package
41
+ # supports 3.10+, so the stdlib import breaks every MCP tool on 3.10/3.11 —
42
+ # which is exactly how it shipped in 1.2.0, because the dev interpreter was
43
+ # 3.14 and only CI runs the older matrix.
44
+ if sys.version_info >= (3, 12):
45
+ from typing import TypedDict
46
+ else:
47
+ from typing_extensions import TypedDict
48
+
39
49
  import pandas as pd # type: ignore[import-untyped]
40
50
  from fastmcp import FastMCP
41
51
 
@@ -214,6 +224,129 @@ def _get_storage() -> BlqStorage:
214
224
  return BlqStorage.open()
215
225
 
216
226
 
227
+ # ── Published result shapes ──────────────────────────────────────────
228
+ #
229
+ # fastmcp derives each tool's `outputSchema` from its return annotation. A bare
230
+ # `dict[str, Any]` becomes {"type": "object", "additionalProperties": true} —
231
+ # an object with no key names, which tells a programmatic caller nothing it can
232
+ # write code against. Measured cost, from four local models driven against this
233
+ # suite: 0/24 correct while 17/24 called the correct tool, because the models
234
+ # picked the right tool and guessed the return shape wrong. `len()` on `events`'
235
+ # dict returns its key count, not its event count.
236
+ #
237
+ # These TypedDicts exist to put the key names on the wire. Keys are taken from
238
+ # the implementations' own `return {...}` statements. `total=False` throughout:
239
+ # most of these tools return an `error` key on failure paths and omit it
240
+ # otherwise, and several have distinct success shapes (`events` returns batch
241
+ # keys under `run_ids`), so declaring everything required would publish a
242
+ # contract the code does not keep.
243
+
244
+ class EventsResult(TypedDict, total=False):
245
+ """`events` — single-run keys, plus the batch keys used with `run_ids`."""
246
+ events: list[dict[str, Any]]
247
+ total_count: int
248
+ runs: list[dict[str, Any]]
249
+ run_count: int
250
+ total_events: int
251
+
252
+
253
+ class StatusResult(TypedDict, total=False):
254
+ sources: list[dict[str, Any]]
255
+
256
+
257
+ class HistoryResult(TypedDict, total=False):
258
+ runs: list[dict[str, Any]]
259
+
260
+
261
+ class QueryResult(TypedDict, total=False):
262
+ # Positional value lists paired with `columns`, not per-row objects.
263
+ rows: list[list[Any]]
264
+ columns: list[str]
265
+ row_count: int
266
+ error: str
267
+
268
+
269
+ class InspectResult(TypedDict, total=False):
270
+ events: list[dict[str, Any]]
271
+ found: int
272
+ total: int
273
+ error: str
274
+
275
+
276
+ class OutputResult(TypedDict, total=False):
277
+ # A LIST of stream names, not a mapping — declaring it as an object made
278
+ # fastmcp reject every call with "[] is not of type 'object'". Worth noting
279
+ # that the schema caught the wrong assumption immediately, which is most of
280
+ # the argument for declaring these at all.
281
+ streams: list[str]
282
+ ref: str
283
+ run_id: int
284
+ error: str
285
+
286
+
287
+ class DiffResult(TypedDict, total=False):
288
+ new: list[dict[str, Any]]
289
+ fixed: list[dict[str, Any]]
290
+ summary: dict[str, Any]
291
+ error: str
292
+
293
+
294
+ class CommandsResult(TypedDict, total=False):
295
+ commands: list[dict[str, Any]]
296
+ error: str
297
+
298
+
299
+ class RunResult(TypedDict, total=False):
300
+ # Nullable, not merely optional: the failure paths return these keys set to
301
+ # None rather than omitting them, and `total=False` only makes a key
302
+ # absent-able.
303
+ run_ref: str | None
304
+ status: str | None
305
+ exit_code: int | None
306
+ summary: dict[str, Any]
307
+ results: list[dict[str, Any]]
308
+ commands_requested: int
309
+ commands_run: int
310
+ error: str
311
+
312
+
313
+ class ExecResult(TypedDict, total=False):
314
+ run_ref: str | None
315
+ status: str | None
316
+ exit_code: int | None
317
+ summary: dict[str, Any]
318
+ errors: list[dict[str, Any]]
319
+ error: str
320
+
321
+
322
+ class CiCheckResult(TypedDict, total=False):
323
+ status: str | None
324
+ mode: str | None
325
+ has_errors: bool
326
+ current_errors: int
327
+ current_run_id: int
328
+ error: str
329
+
330
+
331
+ class CiGenerateResult(TypedDict, total=False):
332
+ # The names of the registered commands, not a yes/no — read from the
333
+ # implementation rather than inferred from the key name, which is how the
334
+ # first three drafts of these annotations got it wrong.
335
+ available: list[str]
336
+ count: int
337
+ scripts: list[dict[str, Any]]
338
+ shell: str | None
339
+ error: str
340
+
341
+
342
+ class CommandMutationResult(TypedDict, total=False):
343
+ """Shared by `register_command` / `unregister_command`."""
344
+ success: bool
345
+ message: str
346
+ existing_name: str
347
+ error: str
348
+
349
+
217
350
  def _get_suppressed_list(include_suppressed: bool = False) -> list[str] | None:
218
351
  """Get list of suppressed fingerprints, or None if no filtering."""
219
352
  if include_suppressed:
@@ -2282,7 +2415,7 @@ def run(
2282
2415
  # Batch mode parameters
2283
2416
  commands: list[str] | None = None,
2284
2417
  stop_on_failure: bool = True,
2285
- ) -> dict[str, Any]:
2418
+ ) -> RunResult:
2286
2419
  """Run a registered command and capture its output.
2287
2420
 
2288
2421
  Can run a single command or multiple commands in sequence (batch mode).
@@ -2340,7 +2473,7 @@ def exec(
2340
2473
  timeout: int | None = None,
2341
2474
  shell: bool = False,
2342
2475
  lines: str | None = None,
2343
- ) -> dict[str, Any]:
2476
+ ) -> ExecResult:
2344
2477
  """Execute an ad-hoc shell command and capture its output.
2345
2478
 
2346
2479
  IMPORTANT: Do NOT use shell pipes, redirects, or command chains
@@ -2379,7 +2512,7 @@ def query(
2379
2512
  sql: str | None = None,
2380
2513
  filter: str | None = None,
2381
2514
  limit: int = 100,
2382
- ) -> dict[str, Any]:
2515
+ ) -> QueryResult:
2383
2516
  """Query stored log events with SQL or simple filter expressions.
2384
2517
 
2385
2518
  Use either `sql` for raw SQL queries or `filter` for simple expressions.
@@ -2421,7 +2554,7 @@ def events(
2421
2554
  # Batch mode
2422
2555
  run_ids: list[int] | None = None,
2423
2556
  limit_per_run: int = 10,
2424
- ) -> dict[str, Any]:
2557
+ ) -> EventsResult:
2425
2558
  """Get events with optional severity filter.
2426
2559
 
2427
2560
  For errors only: use severity="error"
@@ -2493,7 +2626,7 @@ def inspect(
2493
2626
  include_fingerprint_history: bool = False,
2494
2627
  # Batch mode
2495
2628
  refs: list[str] | None = None,
2496
- ) -> dict[str, Any]:
2629
+ ) -> InspectResult:
2497
2630
  """Get comprehensive event details with context and enrichment.
2498
2631
 
2499
2632
  Returns event details with optional context and enrichment:
@@ -2572,7 +2705,7 @@ def output(
2572
2705
  context: int = 0,
2573
2706
  lines: str | None = None,
2574
2707
  debug_formats: bool = False,
2575
- ) -> dict[str, Any]:
2708
+ ) -> OutputResult:
2576
2709
  """Get raw output for a run with optional search and filtering.
2577
2710
 
2578
2711
  Retrieves the captured stdout/stderr from a command execution.
@@ -2606,7 +2739,7 @@ def output(
2606
2739
 
2607
2740
 
2608
2741
  @mcp.tool()
2609
- def status() -> dict[str, Any]:
2742
+ def status() -> StatusResult:
2610
2743
  """Get current status summary of all sources.
2611
2744
 
2612
2745
  Returns:
@@ -2975,7 +3108,7 @@ def _last_impl(
2975
3108
  @mcp.tool()
2976
3109
  def history(
2977
3110
  limit: int | None = None, source: str | None = None, status: str | None = None
2978
- ) -> dict[str, Any]:
3111
+ ) -> HistoryResult:
2979
3112
  """Get run history.
2980
3113
 
2981
3114
  Args:
@@ -2995,7 +3128,7 @@ def history(
2995
3128
 
2996
3129
 
2997
3130
  @mcp.tool()
2998
- def diff(run1: int, run2: int) -> dict[str, Any]:
3131
+ def diff(run1: int, run2: int) -> DiffResult:
2999
3132
  """Compare errors between two runs.
3000
3133
 
3001
3134
  Args:
@@ -3023,7 +3156,7 @@ def register_command(
3023
3156
  lines: str | None = None,
3024
3157
  sandbox: str | dict[str, Any] | None = None,
3025
3158
  lock: str | None = None,
3026
- ) -> dict[str, Any]:
3159
+ ) -> CommandMutationResult:
3027
3160
  """Register a new command.
3028
3161
 
3029
3162
  If a command with the same name or same command string already exists,
@@ -3075,7 +3208,7 @@ def register_command(
3075
3208
 
3076
3209
 
3077
3210
  @mcp.tool()
3078
- def unregister_command(name: str) -> dict[str, Any]:
3211
+ def unregister_command(name: str) -> CommandMutationResult:
3079
3212
  """Remove a registered command.
3080
3213
 
3081
3214
  Note: This tool can be disabled via mcp.disabled_tools config.
@@ -3091,7 +3224,7 @@ def unregister_command(name: str) -> dict[str, Any]:
3091
3224
 
3092
3225
 
3093
3226
  @mcp.tool()
3094
- def commands() -> dict[str, Any]:
3227
+ def commands() -> CommandsResult:
3095
3228
  """List all registered commands.
3096
3229
 
3097
3230
  Returns:
@@ -3592,7 +3725,7 @@ def ci_check(
3592
3725
  baseline: str | None = None,
3593
3726
  fail_on_any: bool = False,
3594
3727
  run_id: int | None = None,
3595
- ) -> dict[str, Any]:
3728
+ ) -> CiCheckResult:
3596
3729
  """Check for regressions compared to a baseline run.
3597
3730
 
3598
3731
  Compares error fingerprints between the current run and a baseline to
@@ -3619,7 +3752,7 @@ def ci_check(
3619
3752
  def ci_generate(
3620
3753
  commands: list[str] | None = None,
3621
3754
  shell: str = "bash",
3622
- ) -> dict[str, Any]:
3755
+ ) -> CiGenerateResult:
3623
3756
  """Generate standalone shell scripts from registered commands.
3624
3757
 
3625
3758
  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