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.
- {bantamkit-0.28.0 → bantamkit-0.29.0}/PKG-INFO +26 -2
- {bantamkit-0.28.0 → bantamkit-0.29.0}/README.md +25 -1
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/__init__.py +1 -1
- bantamkit-0.29.0/src/bantamkit/hostinstall.py +294 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/mcpserver.py +51 -1
- bantamkit-0.29.0/tests/test_hostinstall.py +308 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/.gitignore +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/contracts/default.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/manifest.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/HISTORY.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/README.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/fixtures/.gitkeep +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/fixtures/catalog.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/perturbations/task-completion.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/.gitkeep +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-contact.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-invoice.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-order.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-schedule.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/extract-versions.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-db-port.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-deploy.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-oncall.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/recall-owner.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-compare.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/evals/tasks/shop-total.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/profiles/default.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/profiles/patient.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/.gitkeep +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/code-quality.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/grounded-completion.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/rubrics/task-completion.yaml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/schemas/shiftwork-checkpoint.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/skills/.gitkeep +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/skills/file-graph.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/skills/memory.md +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/.gitkeep +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/bantamkit_read.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/bantamkit_status.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/build_identity.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/document_list.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/document_read.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/file_graph.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/memory_compact.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/memory_recall.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/memory_save.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/shiftwork_clock_in.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/shiftwork_clock_out.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/shiftwork_status.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/skill_audit.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/_assets/tools/validate_json.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/hatch_build.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/pyproject.toml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/agent.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/assets.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/budget.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/client.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/contract.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/criticreplay.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/critique.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/docread.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/evalrun.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/eventlog.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/filegraph.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/loopguard.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/mcpreport.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/__init__.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/__main__.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/component.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/divergence.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/layers.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/memory/store.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/pdfread.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/profile.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/shiftwork.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/skillaudit.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/statusline.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/structured.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/src/bantamkit/textutil.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/cli_exit_status_probe.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/conftest.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/bad-crc.docx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/charref-4301-digits.html +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/charset-table.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/compression-method-9.docx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/corrupt-deflate.docx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/encrypted-member.docx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/encrypted-mimetype.odt +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/internal-dtd-entity.docx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/rfc2231-charset.eml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/rfc822-nested-twice.eml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/docread/x-uuencode.eml +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/f8404ab-perturbation-baseline.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/data/served-tool-surface.json +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/docread_fixtures.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/perturbation_baseline_harness.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/rbp16_effect_probe.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/rbp18_payload_probe.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_adapter.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_agent.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_amendguard.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_bantamkit_read_tool.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_budget.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_build_identity.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_client.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_compaction_corpus_survey.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_conformance.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_contract_fanout.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_criticreplay.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_critique.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_doc_commands_gate.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_docread.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_document_setup.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_document_tasks.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_document_tools.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_encoding_gate.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_evalrun.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_eventlog.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_field_program_gates.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_field_programs.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_filegraph.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_ladder_statistics.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_launcher_which.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_layers.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_loopguard.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcp_endpoint.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcpdrift.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcpreport.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mcpserver.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_compact_tool.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_component.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_divergence.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_layers.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_memory_store_tripwire.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_mutmatrix.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_newline_gate.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_packaging.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_pdfread.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_pinharness_ledger.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_served_tool_count_records.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_shiftwork.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_skillaudit.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_status_surface.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_statusline.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_structured.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_thread_exception_gate.py +0 -0
- {bantamkit-0.28.0 → bantamkit-0.29.0}/tests/test_tool_manifest.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|