bantamkit 0.28.0__tar.gz → 0.29.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.
Files changed (194) hide show
  1. {bantamkit-0.28.0 → bantamkit-0.29.0}/PKG-INFO +26 -2
  2. {bantamkit-0.28.0 → bantamkit-0.29.0}/README.md +25 -1
  3. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/__init__.py +1 -1
  4. bantamkit-0.29.0/src/bantamkit/hostinstall.py +294 -0
  5. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/mcpserver.py +51 -1
  6. bantamkit-0.29.0/tests/test_hostinstall.py +308 -0
  7. {bantamkit-0.28.0 → bantamkit-0.29.0}/.gitignore +0 -0
  8. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/contracts/default.yaml +0 -0
  9. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/manifest.yaml +0 -0
  10. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/HISTORY.md +0 -0
  11. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/README.md +0 -0
  12. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
  13. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
  14. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
  15. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
  16. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
  17. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
  18. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
  19. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
  20. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
  21. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
  22. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
  23. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
  24. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
  25. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
  26. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
  27. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
  28. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
  29. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
  30. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
  31. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
  32. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
  33. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
  34. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
  35. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
  36. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
  37. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
  38. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
  39. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
  40. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
  41. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
  42. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
  43. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
  44. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/fixtures/.gitkeep +0 -0
  45. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/fixtures/catalog.json +0 -0
  46. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/perturbations/task-completion.yaml +0 -0
  47. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/.gitkeep +0 -0
  48. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-contact.yaml +0 -0
  49. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-invoice.yaml +0 -0
  50. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-order.yaml +0 -0
  51. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-schedule.yaml +0 -0
  52. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-versions.yaml +0 -0
  53. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
  54. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
  55. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
  56. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
  57. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-db-port.yaml +0 -0
  58. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-deploy.yaml +0 -0
  59. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
  60. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
  61. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-oncall.yaml +0 -0
  62. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
  63. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-owner.yaml +0 -0
  64. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
  65. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
  66. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-compare.yaml +0 -0
  67. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
  68. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
  69. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-total.yaml +0 -0
  70. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/profiles/default.yaml +0 -0
  71. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/profiles/patient.yaml +0 -0
  72. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/.gitkeep +0 -0
  73. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/code-quality.yaml +0 -0
  74. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/grounded-completion.yaml +0 -0
  75. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/task-completion.yaml +0 -0
  76. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/schemas/shiftwork-checkpoint.json +0 -0
  77. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/skills/.gitkeep +0 -0
  78. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/skills/file-graph.md +0 -0
  79. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/skills/memory.md +0 -0
  80. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/.gitkeep +0 -0
  81. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/bantamkit_read.json +0 -0
  82. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/bantamkit_status.json +0 -0
  83. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/build_identity.json +0 -0
  84. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/document_list.json +0 -0
  85. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/document_read.json +0 -0
  86. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/file_graph.json +0 -0
  87. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/memory_compact.json +0 -0
  88. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/memory_recall.json +0 -0
  89. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/memory_save.json +0 -0
  90. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/shiftwork_clock_in.json +0 -0
  91. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/shiftwork_clock_out.json +0 -0
  92. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/shiftwork_status.json +0 -0
  93. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/skill_audit.json +0 -0
  94. {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/validate_json.json +0 -0
  95. {bantamkit-0.28.0 → bantamkit-0.29.0}/hatch_build.py +0 -0
  96. {bantamkit-0.28.0 → bantamkit-0.29.0}/pyproject.toml +0 -0
  97. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/agent.py +0 -0
  98. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/assets.py +0 -0
  99. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/budget.py +0 -0
  100. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/client.py +0 -0
  101. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/contract.py +0 -0
  102. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/criticreplay.py +0 -0
  103. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/critique.py +0 -0
  104. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/docread.py +0 -0
  105. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/evalrun.py +0 -0
  106. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/eventlog.py +0 -0
  107. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/filegraph.py +0 -0
  108. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/loopguard.py +0 -0
  109. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/mcpreport.py +0 -0
  110. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/__init__.py +0 -0
  111. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/__main__.py +0 -0
  112. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/component.py +0 -0
  113. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/divergence.py +0 -0
  114. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/layers.py +0 -0
  115. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/store.py +0 -0
  116. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/pdfread.py +0 -0
  117. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/profile.py +0 -0
  118. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/shiftwork.py +0 -0
  119. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/skillaudit.py +0 -0
  120. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/statusline.py +0 -0
  121. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/structured.py +0 -0
  122. {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/textutil.py +0 -0
  123. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/cli_exit_status_probe.py +0 -0
  124. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/conftest.py +0 -0
  125. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/bad-crc.docx +0 -0
  126. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/charref-4301-digits.html +0 -0
  127. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/charset-table.json +0 -0
  128. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/compression-method-9.docx +0 -0
  129. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/corrupt-deflate.docx +0 -0
  130. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/encrypted-member.docx +0 -0
  131. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/encrypted-mimetype.odt +0 -0
  132. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
  133. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/internal-dtd-entity.docx +0 -0
  134. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/rfc2231-charset.eml +0 -0
  135. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/rfc822-nested-twice.eml +0 -0
  136. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
  137. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/x-uuencode.eml +0 -0
  138. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/f8404ab-perturbation-baseline.json +0 -0
  139. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/served-tool-surface.json +0 -0
  140. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/docread_fixtures.py +0 -0
  141. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/perturbation_baseline_harness.py +0 -0
  142. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/rbp16_effect_probe.py +0 -0
  143. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/rbp18_payload_probe.py +0 -0
  144. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_adapter.py +0 -0
  145. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_agent.py +0 -0
  146. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_amendguard.py +0 -0
  147. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_bantamkit_read_tool.py +0 -0
  148. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_budget.py +0 -0
  149. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_build_identity.py +0 -0
  150. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_client.py +0 -0
  151. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_compaction_corpus_survey.py +0 -0
  152. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_conformance.py +0 -0
  153. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_contract_fanout.py +0 -0
  154. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_criticreplay.py +0 -0
  155. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_critique.py +0 -0
  156. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_doc_commands_gate.py +0 -0
  157. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_docread.py +0 -0
  158. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_document_setup.py +0 -0
  159. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_document_tasks.py +0 -0
  160. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_document_tools.py +0 -0
  161. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_encoding_gate.py +0 -0
  162. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_evalrun.py +0 -0
  163. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_eventlog.py +0 -0
  164. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_field_program_gates.py +0 -0
  165. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_field_programs.py +0 -0
  166. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_filegraph.py +0 -0
  167. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_ladder_statistics.py +0 -0
  168. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_launcher_which.py +0 -0
  169. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_layers.py +0 -0
  170. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_loopguard.py +0 -0
  171. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcp_endpoint.py +0 -0
  172. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcpdrift.py +0 -0
  173. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcpreport.py +0 -0
  174. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcpserver.py +0 -0
  175. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory.py +0 -0
  176. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_compact_tool.py +0 -0
  177. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_component.py +0 -0
  178. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_divergence.py +0 -0
  179. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_layers.py +0 -0
  180. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_store_tripwire.py +0 -0
  181. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mutmatrix.py +0 -0
  182. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_newline_gate.py +0 -0
  183. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_packaging.py +0 -0
  184. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_pdfread.py +0 -0
  185. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_pinharness_ledger.py +0 -0
  186. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_served_tool_count_records.py +0 -0
  187. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_shiftwork.py +0 -0
  188. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_skillaudit.py +0 -0
  189. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_status_surface.py +0 -0
  190. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_statusline.py +0 -0
  191. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_structured.py +0 -0
  192. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_thread_exception_gate.py +0 -0
  193. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_tool_manifest.py +0 -0
  194. {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_version_agreement.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bantamkit
3
- Version: 0.28.0
3
+ Version: 0.29.0
4
4
  Summary: bantamweight tooling — harness primitives that lift small-model agents
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -52,7 +52,31 @@ command here that one is the documented form rather than a measured one.
52
52
 
53
53
  ### Connect it to a host
54
54
 
55
- Every host below runs the same command; only the file and the key around it change. If
55
+ **One command, and it writes the entry for you:**
56
+
57
+ ```bash
58
+ bantamkit-mcp --install claude # Claude Code
59
+ bantamkit-mcp --install claude-desktop # Claude Desktop
60
+ bantamkit-mcp --install copilot # GitHub Copilot in VS Code
61
+ bantamkit-mcp --install cursor # Cursor
62
+ ```
63
+
64
+ It records the absolute path of the console script you just ran, so the entry points at the
65
+ environment you installed into rather than at whatever is on a host's PATH.
66
+
67
+ `--install claude` runs `claude mcp add` rather than editing `~/.claude.json` directly:
68
+ that file is the host's, and it carries state that is not MCP configuration. What happens on
69
+ a second run there is Claude Code's decision, not this command's, and `--force` does not
70
+ reach it.
71
+
72
+ **For the other three**, which are edited directly: it never prompts. A second run that
73
+ finds its own entry says so and changes nothing; an entry that differs is refused, printed
74
+ beside the one it would write, and replaced only with `--force`. Every write backs the file
75
+ up first as `<name>.backup-<date>`, preserves the file's permissions, and a file that does
76
+ not parse is refused rather than replaced.
77
+
78
+ The rest of this section is what those commands write, for anyone who would rather do it by
79
+ hand. Every host runs the same command; only the file and the key around it change. If
56
80
  you installed with `pip` into an environment you keep, replace the `command`/`args` pair
57
81
  with the absolute path to the `bantamkit-mcp` console script in that environment.
58
82
 
@@ -35,7 +35,31 @@ command here that one is the documented form rather than a measured one.
35
35
 
36
36
  ### Connect it to a host
37
37
 
38
- Every host below runs the same command; only the file and the key around it change. If
38
+ **One command, and it writes the entry for you:**
39
+
40
+ ```bash
41
+ bantamkit-mcp --install claude # Claude Code
42
+ bantamkit-mcp --install claude-desktop # Claude Desktop
43
+ bantamkit-mcp --install copilot # GitHub Copilot in VS Code
44
+ bantamkit-mcp --install cursor # Cursor
45
+ ```
46
+
47
+ It records the absolute path of the console script you just ran, so the entry points at the
48
+ environment you installed into rather than at whatever is on a host's PATH.
49
+
50
+ `--install claude` runs `claude mcp add` rather than editing `~/.claude.json` directly:
51
+ that file is the host's, and it carries state that is not MCP configuration. What happens on
52
+ a second run there is Claude Code's decision, not this command's, and `--force` does not
53
+ reach it.
54
+
55
+ **For the other three**, which are edited directly: it never prompts. A second run that
56
+ finds its own entry says so and changes nothing; an entry that differs is refused, printed
57
+ beside the one it would write, and replaced only with `--force`. Every write backs the file
58
+ up first as `<name>.backup-<date>`, preserves the file's permissions, and a file that does
59
+ not parse is refused rather than replaced.
60
+
61
+ The rest of this section is what those commands write, for anyone who would rather do it by
62
+ hand. Every host runs the same command; only the file and the key around it change. If
39
63
  you installed with `pip` into an environment you keep, replace the `command`/`args` pair
40
64
  with the absolute path to the `bantamkit-mcp` console script in that environment.
41
65
 
@@ -29,4 +29,4 @@ from bantamkit.structured import StructuredOutputError, extract_json, structured
29
29
  # file as its dynamic version source, so the wheel's metadata and the string the MCP
30
30
  # server advertises are the same committed bytes, and neither is a function of when
31
31
  # someone last ran `pip`.
32
- __version__ = "0.28.0"
32
+ __version__ = "0.29.0"
@@ -0,0 +1,294 @@
1
+ """Wire this server into an MCP host's configuration — `--install <host>`.
2
+
3
+ WHAT THIS IS FOR. Publishing to npm and PyPI made bantamkit installable; it did not make
4
+ it REACHABLE. Every host needs a JSON entry in a different file under a different key, and
5
+ the key is not even the same word everywhere: VS Code says `servers`, the other three say
6
+ `mcpServers`. That difference is the one that costs people an afternoon, so this writes the
7
+ entry instead of documenting it.
8
+
9
+ FOUR HOSTS, AND ONE OF THEM IS NOT WRITTEN BY US. Claude Code's configuration is
10
+ `~/.claude.json`, which is not an MCP file: it carries project history and other state
11
+ belonging to the host. `claude mcp add` does the same job safely and was measured writing
12
+ exactly the entry documented here, so `--install claude` RUNS it. The other three files
13
+ exist to hold server entries and nothing else, so they are edited directly. That is not a
14
+ convenience — a file the host owns and rewrites is a file we should not be merging into by
15
+ hand.
16
+
17
+ WHAT THIS WRITES IS WHAT IS RUNNING. The command recorded is this interpreter's own
18
+ `bantamkit-mcp` console script, by absolute path. The Node distribution records
19
+ `npx -y bantamkit-mcp` for the same reason: the thing installed should be the thing that
20
+ answers, and neither side should send a host looking for the other's runtime. The two
21
+ outputs therefore DIFFER by construction and that difference is ruled in `docs/porting.md`.
22
+
23
+ WHY IT NEVER PROMPTS. An overwrite is exactly the moment a program wants to ask, and asking
24
+ requires a terminal. Measured repeatedly on this machine: Claude Code's `!` channel has no
25
+ TTY, and neither does CI, so a prompt there is not a question — it is an `EOFError` or a
26
+ silent hang. So the refusal is the answer: an entry that differs is reported with both
27
+ values and a `--force` to re-run with, and the behaviour is identical whether or not anyone
28
+ is watching.
29
+
30
+ Layer 5 (Composition): this module reads and writes host files and shells out. Nothing in
31
+ the runtime imports it; `mcpserver.py` calls it from the flag and returns before a store or
32
+ a transport exists, the same shape `--assets-root` uses.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import json
38
+ import os
39
+ import platform
40
+ import shutil
41
+ import subprocess
42
+ import sys
43
+ from datetime import date
44
+ from pathlib import Path
45
+
46
+ # The four hosts, in the order they print in `--help`. `claude` first because it is the one
47
+ # with its own installer and therefore the one whose arm is different.
48
+ HOSTS = ("claude", "claude-desktop", "copilot", "cursor")
49
+
50
+ # The entry name written into every host. One word, so a person reading their own config
51
+ # can see where it came from, and so a second run can find what the first one wrote.
52
+ ENTRY = "bantamkit"
53
+
54
+
55
+ class InstallError(Exception):
56
+ """A refusal, carrying the sentence the operator should read. Never a traceback."""
57
+
58
+
59
+ def _home() -> Path:
60
+ return Path.home()
61
+
62
+
63
+ def host_config_path(host: str) -> Path:
64
+ """The file `--install <host>` would write, on THIS platform.
65
+
66
+ Resolved from `Path.home()` and nothing else, which is what makes the tests possible:
67
+ point `HOME` (and `USERPROFILE` on Windows) at a temporary directory and every path
68
+ below moves with it, so no test can reach a real configuration file.
69
+
70
+ `claude` has no entry here on purpose — its arm shells out to `claude mcp add` and never
71
+ names a path, because naming one would be this module claiming to know a layout the host
72
+ is free to change.
73
+ """
74
+ system = platform.system()
75
+ if host == "claude-desktop":
76
+ if system == "Darwin":
77
+ base = _home() / "Library" / "Application Support" / "Claude"
78
+ return base / "claude_desktop_config.json"
79
+ if system == "Windows":
80
+ appdata = os.environ.get("APPDATA")
81
+ base = Path(appdata) if appdata else _home() / "AppData" / "Roaming"
82
+ return base / "Claude" / "claude_desktop_config.json"
83
+ return _home() / ".config" / "Claude" / "claude_desktop_config.json"
84
+ if host == "copilot":
85
+ if system == "Darwin":
86
+ return _home() / "Library" / "Application Support" / "Code" / "User" / "mcp.json"
87
+ if system == "Windows":
88
+ appdata = os.environ.get("APPDATA")
89
+ base = Path(appdata) if appdata else _home() / "AppData" / "Roaming"
90
+ return base / "Code" / "User" / "mcp.json"
91
+ return _home() / ".config" / "Code" / "User" / "mcp.json"
92
+ if host == "cursor":
93
+ return _home() / ".cursor" / "mcp.json"
94
+ raise InstallError(f"no configuration file is written for {host}")
95
+
96
+
97
+ def config_key(host: str) -> str:
98
+ """`servers` in VS Code, `mcpServers` everywhere else.
99
+
100
+ This single word is the reason this command exists. It is not a stylistic difference:
101
+ a correct entry under the wrong key is silently ignored, and the reader has no error to
102
+ search for.
103
+ """
104
+ return "servers" if host == "copilot" else "mcpServers"
105
+
106
+
107
+ def entry_for(host: str, command: str, args: list[str]) -> dict:
108
+ """The server entry, in the shape the host expects.
109
+
110
+ VS Code's documented example carries `type: "stdio"`; the other three do not use it. The
111
+ field is written only where the host's own documentation shows it, because a key a host
112
+ does not read is a key a reader has to wonder about.
113
+ """
114
+ entry: dict = {"command": command, "args": list(args)}
115
+ if host == "copilot":
116
+ entry = {"type": "stdio", **entry}
117
+ return entry
118
+
119
+
120
+ def _read_config(path: Path) -> dict:
121
+ """The file as an object, or a refusal that says why — never a partially-read file.
122
+
123
+ A file that does not parse is NOT overwritten. It is somebody's configuration, the
124
+ parse error names the byte, and replacing it would destroy the only copy of whatever
125
+ they were in the middle of writing.
126
+ """
127
+ if not path.exists():
128
+ return {}
129
+ try:
130
+ text = path.read_text(encoding="utf-8")
131
+ except OSError as exc:
132
+ raise InstallError(f"cannot read {path}: {exc}") from None
133
+ if not text.strip():
134
+ return {}
135
+ try:
136
+ loaded = json.loads(text)
137
+ except json.JSONDecodeError as exc:
138
+ raise InstallError(
139
+ f"{path} is not valid JSON, so this refuses to touch it: {exc.msg} "
140
+ f"(line {exc.lineno}, column {exc.colno})"
141
+ ) from None
142
+ if not isinstance(loaded, dict):
143
+ raise InstallError(
144
+ f"{path} holds {type(loaded).__name__}, not an object; refusing to touch it"
145
+ )
146
+ return loaded
147
+
148
+
149
+ def _write_config(path: Path, data: dict) -> None:
150
+ """Write the whole object, atomically, leaving no half-file behind on a crash."""
151
+ path.parent.mkdir(parents=True, exist_ok=True)
152
+ tmp = path.with_name(path.name + ".bantamkit-tmp")
153
+ # The mode of the file being replaced, carried onto its replacement. A host config holds
154
+ # API keys in per-server `env` blocks, and a user who chmod'ed theirs to 0600 had it come
155
+ # back 0644 because a fresh temp file gets the process umask. Measured on both runtimes.
156
+ mode = path.stat().st_mode & 0o7777 if path.exists() else None
157
+ # `ensure_ascii` is left at its DEFAULT, which escapes non-ASCII as `\uXXXX`. That is not
158
+ # a style choice: `runtime-ts`'s `dumpJson` reproduces `json.dumps` including this, and a
159
+ # home directory with a non-ASCII name — a Thai or Japanese Windows username, say — would
160
+ # otherwise make the two runtimes write different bytes for the same install. Escaped
161
+ # JSON is still JSON and every host parses it; two runtimes disagreeing is the thing that
162
+ # costs something.
163
+ payload = json.dumps(data, indent=2) + "\n"
164
+ try:
165
+ tmp.write_text(payload, encoding="utf-8")
166
+ if mode is not None:
167
+ os.chmod(tmp, mode)
168
+ os.replace(tmp, path)
169
+ except OSError as exc:
170
+ tmp.unlink(missing_ok=True)
171
+ raise InstallError(f"cannot write {path}: {exc}") from None
172
+
173
+
174
+ def _backup(path: Path) -> Path | None:
175
+ """Copy the file aside before changing it. Returns the copy, or None if there was none.
176
+
177
+ Dated rather than numbered: a person looking at their own config directory a week later
178
+ can tell when it happened, and a second run on the same day overwrites the same name
179
+ rather than growing a pile nobody prunes.
180
+ """
181
+ if not path.exists():
182
+ return None
183
+ backup = path.with_name(f"{path.name}.backup-{date.today().isoformat()}")
184
+ try:
185
+ shutil.copy2(path, backup)
186
+ except OSError as exc:
187
+ raise InstallError(f"cannot back up {path}: {exc}") from None
188
+ return backup
189
+
190
+
191
+ def _install_via_claude_cli(command: str, args: list[str]) -> list[str]:
192
+ """`claude mcp add`, because `~/.claude.json` is the host's file and not an MCP file."""
193
+ binary = shutil.which("claude")
194
+ if binary is None:
195
+ raise InstallError(
196
+ "the `claude` command is not on PATH, so this cannot register with Claude Code. "
197
+ "Install Claude Code, or add the entry by hand — `docs/install.md` gives the shape."
198
+ )
199
+ argv = [binary, "mcp", "add", ENTRY, "-s", "user", "--", command, *args]
200
+ try:
201
+ # `encoding` named rather than `text=True` alone: `text=True` decodes with the
202
+ # LOCALE encoding, so on a Windows console at cp874 or cp932 this would mangle
203
+ # whatever `claude mcp add` wrote — and the only time its output is read is when
204
+ # something already went wrong. The encoding gate names this class; it caught this
205
+ # line rather than a reviewer.
206
+ done = subprocess.run(
207
+ argv, capture_output=True, text=True, encoding="utf-8", errors="replace"
208
+ )
209
+ except OSError as exc:
210
+ raise InstallError(f"could not run {binary}: {exc}") from None
211
+ if done.returncode != 0:
212
+ detail = (done.stderr or done.stdout or "").strip()
213
+ raise InstallError(
214
+ f"`claude mcp add` failed (exit {done.returncode})"
215
+ + (f"\n{detail}" if detail else "")
216
+ )
217
+ return argv
218
+
219
+
220
+ def install(host: str, command: str, args: list[str], force: bool = False) -> str:
221
+ """Register this server with `host`. Returns the report to print; raises `InstallError`.
222
+
223
+ The three file-writing hosts share one path: read, compare, back up, merge, write. The
224
+ comparison is against the entry this run WOULD write, so a second run with the same
225
+ command is a no-op that says so rather than a rewrite that looks like work.
226
+ """
227
+ if host not in HOSTS:
228
+ raise InstallError(f"unknown host {host!r}; choose one of: {', '.join(HOSTS)}")
229
+
230
+ if host == "claude":
231
+ argv = _install_via_claude_cli(command, args)
232
+ # `claude`, not the resolved binary: `shutil.which` gives an absolute path and the
233
+ # port has no such path to print, so printing it would be a divergence with nothing
234
+ # behind it. And `argv[1:]` ALONE dropped the verb — the line read
235
+ # `mcp add bantamkit ...`, which is not a command anyone can copy. Found by review.
236
+ return f"installed bantamkit into claude\n ran : claude {' '.join(argv[1:])}"
237
+
238
+ path = host_config_path(host)
239
+ key = config_key(host)
240
+ wanted = entry_for(host, command, args)
241
+
242
+ data = _read_config(path)
243
+ servers = data.get(key)
244
+ if servers is None:
245
+ servers = {}
246
+ elif not isinstance(servers, dict):
247
+ raise InstallError(f"{path} has a {key!r} that is not an object; refusing to touch it")
248
+
249
+ # MEMBERSHIP, not `is not None`: a config holding `"bantamkit": null` has an entry, and
250
+ # `.get()` cannot tell that from having none. Reviewed after the two runtimes were
251
+ # measured disagreeing on exactly that file — this side rewrote it without `--force`
252
+ # while the port refused, so the guarantee "an entry that differs is never replaced
253
+ # silently" was false here and nowhere else.
254
+ present = ENTRY in servers
255
+ existing = servers.get(ENTRY)
256
+ if present and existing == wanted:
257
+ return f"bantamkit is already installed in {host} and matches\n file : {path}"
258
+ if present and not force:
259
+ raise InstallError(
260
+ f"{host} already has a bantamkit entry with different settings\n"
261
+ f" file : {path}\n"
262
+ f" current : {json.dumps(existing, sort_keys=True)}\n"
263
+ f" proposed: {json.dumps(wanted, sort_keys=True)}\n"
264
+ " re-run with --force to replace it"
265
+ )
266
+
267
+ backup = _backup(path)
268
+ servers[ENTRY] = wanted
269
+ data[key] = servers
270
+ _write_config(path, data)
271
+
272
+ lines = [
273
+ f"installed bantamkit into {host}",
274
+ f" file : {path}",
275
+ f" key : {key}",
276
+ f" command: {command} {' '.join(args)}".rstrip(),
277
+ ]
278
+ if backup is not None:
279
+ lines.append(f" backup : {backup}")
280
+ return "\n".join(lines)
281
+
282
+
283
+ def this_command() -> tuple[str, list[str]]:
284
+ """What a host should run to get THIS server.
285
+
286
+ `sys.argv[0]` is the console script the operator actually invoked, resolved absolute so
287
+ the entry does not depend on the host's PATH or working directory. Falling back to
288
+ `python -m bantamkit.mcpserver` matters for `python -m` invocations, where `argv[0]` is
289
+ the module path and naming it would write an entry that only works from one directory.
290
+ """
291
+ argv0 = Path(sys.argv[0]) if sys.argv and sys.argv[0] else None
292
+ if argv0 is not None and argv0.name.startswith("bantamkit-mcp") and argv0.exists():
293
+ return str(argv0.resolve()), []
294
+ return str(Path(sys.executable).resolve()), ["-m", "bantamkit.mcpserver"]
@@ -17,7 +17,7 @@ from pathlib import Path
17
17
  from typing import Annotated, Any
18
18
 
19
19
  import bantamkit
20
- from bantamkit import __version__, docread, shiftwork, skillaudit
20
+ from bantamkit import __version__, docread, hostinstall, shiftwork, skillaudit
21
21
  from bantamkit.assets import AssetNotFound, assets_root, load_skill, load_tool_asset
22
22
  from bantamkit.client import BantamError
23
23
  from bantamkit.contract import (
@@ -1262,6 +1262,27 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
1262
1262
  action="store_true",
1263
1263
  help="print one status line for a host status bar, then exit",
1264
1264
  )
1265
+ # THE SAME PLACE AND THE SAME REASON AS THE TWO ABOVE. It prints and returns before a
1266
+ # transport exists, so it belongs with the flags that need no server; and it is placed
1267
+ # after `--statusline` rather than beside `-h` so that the FIRST line of the 80-column
1268
+ # usage — pinned by
1269
+ # `test_assets_root_appears_in_the_generated_help_in_the_documented_position` and by the
1270
+ # `cli` conformance suite — stays byte-identical. The choices render long enough to take
1271
+ # a line of their own; that line is below the pinned one.
1272
+ parser.add_argument(
1273
+ "--install",
1274
+ choices=hostinstall.HOSTS,
1275
+ help="wire this server into a host's MCP configuration, then exit",
1276
+ )
1277
+ # Paired with `--install` and useless without it, which the parser checks rather than
1278
+ # the help text claiming it. It exists because this command NEVER prompts: an overwrite
1279
+ # is exactly when a program wants to ask, and asking needs a TTY that neither Claude
1280
+ # Code's `!` channel nor CI has.
1281
+ parser.add_argument(
1282
+ "--force",
1283
+ action="store_true",
1284
+ help="with --install, replace an existing bantamkit entry",
1285
+ )
1265
1286
  stores = parser.add_mutually_exclusive_group()
1266
1287
  stores.add_argument("--store", help="single memory store path (disables layering)")
1267
1288
  stores.add_argument(
@@ -1343,6 +1364,27 @@ def _print_assets_root() -> None:
1343
1364
  sys.stdout.buffer.flush()
1344
1365
 
1345
1366
 
1367
+ def _run_install(args: argparse.Namespace) -> None:
1368
+ """`--install <host>`: register this server, print what happened, return.
1369
+
1370
+ Written through `sys.stdout.buffer` for the reason `_print_assets_root` gives: on
1371
+ Windows `print` emits CRLF where Node's `process.stdout.write` emits LF, and a
1372
+ byte-comparing conformance runner would read that as a divergence.
1373
+
1374
+ A refusal goes to stderr and exits 1. It is not an exception the operator has to read a
1375
+ traceback for: every `InstallError` carries the sentence that says what to do next.
1376
+ """
1377
+ command, extra = hostinstall.this_command()
1378
+ try:
1379
+ report = hostinstall.install(args.install, command, extra, force=args.force)
1380
+ except hostinstall.InstallError as exc:
1381
+ sys.stderr.buffer.write(f"error: {exc}\n".encode())
1382
+ sys.stderr.buffer.flush()
1383
+ raise SystemExit(1) from None
1384
+ sys.stdout.buffer.write(f"{report}\n".encode())
1385
+ sys.stdout.buffer.flush()
1386
+
1387
+
1346
1388
  def main() -> None:
1347
1389
  if MCPServer is None:
1348
1390
  raise SystemExit(_INSTALL_HINT)
@@ -1358,6 +1400,14 @@ def main() -> None:
1358
1400
  if args.statusline:
1359
1401
  _print_status_line(args)
1360
1402
  return
1403
+ if args.install:
1404
+ _run_install(args)
1405
+ return
1406
+ # `--force` alone is a typo with a plausible reading -- somebody meant to install and
1407
+ # dropped the flag that says where. Refusing names the missing half instead of starting
1408
+ # a server that ignores it.
1409
+ if args.force:
1410
+ raise SystemExit("--force is only meaningful with --install")
1361
1411
  server = build_server(_build_memory(args))
1362
1412
  asyncio.run(server.run_stdio_async())
1363
1413