bantamkit 0.35.8__tar.gz → 0.35.9__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.35.8 → bantamkit-0.35.9}/PKG-INFO +3 -3
- {bantamkit-0.35.8 → bantamkit-0.35.9}/README.md +2 -2
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/__init__.py +1 -1
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/assets.py +29 -3
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/hostinstall.py +43 -9
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/mcpserver.py +101 -24
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/mcpstdio.py +106 -4
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/tokenledger.py +22 -1
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/updatecheck.py +33 -4
- bantamkit-0.35.9/tests/test_assets.py +48 -0
- bantamkit-0.35.9/tests/test_cli_oneshot.py +113 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_hookadapter.py +40 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_hostinstall.py +75 -8
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_hostinstall_hooks.py +27 -2
- bantamkit-0.35.9/tests/test_mcp_request_guard.py +103 -0
- bantamkit-0.35.9/tests/test_mcpstdio_frames.py +68 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_selfupdate.py +1 -1
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_status_surface.py +4 -1
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_tokenledger.py +89 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_updatecheck.py +32 -5
- {bantamkit-0.35.8 → bantamkit-0.35.9}/.gitignore +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/contracts/default.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/manifest.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/HISTORY.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/README.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/fixtures/.gitkeep +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/fixtures/catalog.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/perturbations/task-completion.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/.gitkeep +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/extract-contact.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/extract-invoice.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/extract-order.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/extract-schedule.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/extract-versions.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-db-port.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-deploy.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-oncall.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/recall-owner.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/shop-compare.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/evals/tasks/shop-total.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/pricing/default.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/profiles/default.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/profiles/patient.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/rubrics/.gitkeep +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/rubrics/code-quality.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/rubrics/grounded-completion.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/rubrics/task-completion.yaml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/schemas/shiftwork-checkpoint.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/skills/.gitkeep +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/skills/file-graph.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/skills/memory.md +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/.gitkeep +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/bantamkit_read.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/bantamkit_status.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/build_identity.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/document_list.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/document_read.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/file_graph.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/memory_compact.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/memory_dream.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/memory_recall.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/memory_save.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/repo_map.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/shiftwork_clock_in.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/shiftwork_clock_out.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/shiftwork_plan.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/shiftwork_status.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/skill_audit.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/token_ledger.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/validate_json.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/_assets/tools/work_plan.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/hatch_build.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/pyproject.toml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/agent.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/budget.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/client.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/contract.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/criticreplay.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/critique.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/docmanifest.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/docread.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/evalrun.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/eventlog.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/filegraph.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/hookadapter.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/loopguard.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/mcpreport.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/__init__.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/__main__.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/component.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/divergence.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/dream.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/layers.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/memory/store.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/pdfread.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/pricing.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/profile.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/repomap.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/selfupdate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/shiftwork.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/skillaudit.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/statusline.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/structured.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/textutil.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/src/bantamkit/workplan.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/cli_exit_status_probe.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/conftest.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/bad-crc.docx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/charref-4301-digits.html +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/charset-table.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/compression-method-9.docx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/corrupt-deflate.docx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/encrypted-member.docx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/encrypted-mimetype.odt +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/internal-dtd-entity.docx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/rfc2231-charset.eml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/rfc822-nested-twice.eml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/docread/x-uuencode.eml +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/f8404ab-perturbation-baseline.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/platform-assumption-baseline.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/data/served-tool-surface.json +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/docread_fixtures.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/perturbation_baseline_harness.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/rbp16_effect_probe.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/rbp18_payload_probe.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_adapter.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_agent.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_amendguard.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_bantamkit_gitignore.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_bantamkit_read_tool.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_budget.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_build_identity.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_client.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_compaction_corpus_survey.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_conformance.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_conformance_harness_resilience.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_conformance_suite_table_gate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_contract_fanout.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_contract_patterns.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_contract_refs.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_criticreplay.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_critique.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_doc_commands_gate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_docread.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_docread_ceilings.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_document_manifest_parity.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_document_setup.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_document_tasks.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_document_tools.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_encoding_gate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_evalrun.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_eventlog.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_field_program_gates.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_field_programs.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_filegraph.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_install_shape.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_ladder_statistics.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_launcher_which.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_layers.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_loopguard.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_mcp_endpoint.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_mcpdrift.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_mcpreport.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_mcpserver.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_compact_tool.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_component.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_divergence.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_dream.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_layers.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_store_tripwire.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_memory_strings.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_mutmatrix.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_nativeexport.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_newline_gate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_packaging.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_pdfread.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_pinharness_ledger.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_platform_assumption_gate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_pricing.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_readme_separation.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_repo_map_tool.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_repomap.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_served_tool_count_records.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_shiftwork.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_skillaudit.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_statusline.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_structured.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_thread_exception_gate.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_tool_manifest.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_version_agreement.py +0 -0
- {bantamkit-0.35.8 → bantamkit-0.35.9}/tests/test_workplan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bantamkit
|
|
3
|
-
Version: 0.35.
|
|
3
|
+
Version: 0.35.9
|
|
4
4
|
Summary: Memory MCP server for Claude Code, Cursor, VS Code Copilot and Claude Desktop, plus a Python library that lifts small-model agents. Install once, run offline.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Ink01101011/bantamkit
|
|
6
6
|
Project-URL: Repository, https://github.com/Ink01101011/bantamkit
|
|
@@ -103,9 +103,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
|
|
|
103
103
|
one platform only), copy `wheels/` across, and install from it:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
python -m pip download "bantamkit[mcp]==0.35.
|
|
106
|
+
python -m pip download "bantamkit[mcp]==0.35.9" -d wheels
|
|
107
107
|
python -m venv <env>
|
|
108
|
-
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.
|
|
108
|
+
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.9"
|
|
109
109
|
<env>/bin/bantamkit-mcp --install cursor
|
|
110
110
|
```
|
|
111
111
|
|
|
@@ -74,9 +74,9 @@ CPU architecture and Python minor version** (some wheels, such as `pydantic_core
|
|
|
74
74
|
one platform only), copy `wheels/` across, and install from it:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
python -m pip download "bantamkit[mcp]==0.35.
|
|
77
|
+
python -m pip download "bantamkit[mcp]==0.35.9" -d wheels
|
|
78
78
|
python -m venv <env>
|
|
79
|
-
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.
|
|
79
|
+
<env>/bin/pip install --no-index --find-links wheels "bantamkit[mcp]==0.35.9"
|
|
80
80
|
<env>/bin/bantamkit-mcp --install cursor
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -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.35.
|
|
32
|
+
__version__ = "0.35.9"
|
|
@@ -26,6 +26,32 @@ def assets_root() -> Path:
|
|
|
26
26
|
raise AssetNotFound("no assets directory found; set BANTAMKIT_ASSETS")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _asset_path(kind: str, name: str, ext: str) -> Path:
|
|
30
|
+
"""job66/#145: the one path every loader below builds, and the containment check on it.
|
|
31
|
+
|
|
32
|
+
`name` is a BARE asset name (`memory`, `shiftwork-checkpoint`, `memory_recall`) and
|
|
33
|
+
nothing else. Before this, each loader joined `name` under its directory verbatim, so a
|
|
34
|
+
caller passing user input could read any `.json`/`.md` on disk through `..`. A name
|
|
35
|
+
holding a path separator (`/` or `\\`, either platform's), `..`, a drive colon or a NUL
|
|
36
|
+
is refused BEFORE any filesystem access, and the joined path is then checked LEXICALLY
|
|
37
|
+
(`os.path.abspath`, not `resolve()`: a pack may be a symlink farm, and the check is about
|
|
38
|
+
the name) to sit directly inside `<root>/<kind>s/`. On Windows `Path("tools") / "C:x"`
|
|
39
|
+
is drive-relative and leaves the pack, which is what the colon refusal and the lexical
|
|
40
|
+
check close. The refusal is an `AssetNotFound`, so the MCP resource handler's existing
|
|
41
|
+
`unknown skill asset: <name>` answer is unchanged. `runtime-ts/src/assets.ts::assetPath`
|
|
42
|
+
raises the same sentence.
|
|
43
|
+
"""
|
|
44
|
+
directory = assets_root() / f"{kind}s"
|
|
45
|
+
path = directory / f"{name}{ext}"
|
|
46
|
+
if (
|
|
47
|
+
any(c in name for c in "/\\:\0")
|
|
48
|
+
or ".." in name
|
|
49
|
+
or os.path.dirname(os.path.abspath(path)) != os.path.abspath(directory)
|
|
50
|
+
):
|
|
51
|
+
raise AssetNotFound(f"{kind} asset name is not a bare name: {name}")
|
|
52
|
+
return path
|
|
53
|
+
|
|
54
|
+
|
|
29
55
|
def load_tool_asset(name: str) -> dict:
|
|
30
56
|
"""Return one tool asset VERBATIM — every key, including the ones no surface uses.
|
|
31
57
|
|
|
@@ -34,7 +60,7 @@ def load_tool_asset(name: str) -> dict:
|
|
|
34
60
|
register this at all?) and `output_schema` (what do I advertise as the return shape?)
|
|
35
61
|
and neither belongs on the agent-facing `Tool`.
|
|
36
62
|
"""
|
|
37
|
-
path =
|
|
63
|
+
path = _asset_path("tool", name, ".json")
|
|
38
64
|
if not path.exists():
|
|
39
65
|
raise AssetNotFound(f"tool asset not found: {path}")
|
|
40
66
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -54,14 +80,14 @@ def load_tool(name: str) -> Tool:
|
|
|
54
80
|
|
|
55
81
|
def load_schema(name: str) -> dict:
|
|
56
82
|
"""Return a JSON Schema asset (parsed) — e.g. the shift-work checkpoint contract."""
|
|
57
|
-
path =
|
|
83
|
+
path = _asset_path("schema", name, ".json")
|
|
58
84
|
if not path.exists():
|
|
59
85
|
raise AssetNotFound(f"schema asset not found: {path}")
|
|
60
86
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
61
87
|
|
|
62
88
|
|
|
63
89
|
def load_skill(name: str) -> str:
|
|
64
|
-
path =
|
|
90
|
+
path = _asset_path("skill", name, ".md")
|
|
65
91
|
if not path.exists():
|
|
66
92
|
raise AssetNotFound(f"skill asset not found: {path}")
|
|
67
93
|
return path.read_text(encoding="utf-8")
|
|
@@ -40,6 +40,7 @@ import json
|
|
|
40
40
|
import math
|
|
41
41
|
import os
|
|
42
42
|
import platform
|
|
43
|
+
import re
|
|
43
44
|
import shlex
|
|
44
45
|
import shutil
|
|
45
46
|
import subprocess
|
|
@@ -219,13 +220,7 @@ def _write_config(path: Path, data: dict) -> None:
|
|
|
219
220
|
# API keys in per-server `env` blocks, and a user who chmod'ed theirs to 0600 had it come
|
|
220
221
|
# back 0644 because a fresh temp file gets the process umask. Measured on both runtimes.
|
|
221
222
|
mode = target.stat().st_mode & 0o7777 if target.exists() else None
|
|
222
|
-
|
|
223
|
-
# a style choice: `runtime-ts`'s `dumpJson` reproduces `json.dumps` including this, and a
|
|
224
|
-
# home directory with a non-ASCII name — a Thai or Japanese Windows username, say — would
|
|
225
|
-
# otherwise make the two runtimes write different bytes for the same install. Escaped
|
|
226
|
-
# JSON is still JSON and every host parses it; two runtimes disagreeing is the thing that
|
|
227
|
-
# costs something.
|
|
228
|
-
payload = json.dumps(data, indent=2) + "\n"
|
|
223
|
+
payload = _dump_host_json(data, _detected_indent(target)) + "\n"
|
|
229
224
|
try:
|
|
230
225
|
tmp.write_text(payload, encoding="utf-8")
|
|
231
226
|
if mode is not None:
|
|
@@ -236,6 +231,45 @@ def _write_config(path: Path, data: dict) -> None:
|
|
|
236
231
|
raise InstallError(f"cannot write {path}: {exc}") from None
|
|
237
232
|
|
|
238
233
|
|
|
234
|
+
def _detected_indent(path: Path) -> str:
|
|
235
|
+
"""The indent unit the file already uses: a tab, or N spaces; two for a new file (#118).
|
|
236
|
+
|
|
237
|
+
The unit is read off the FIRST line that starts with whitespace and has something after
|
|
238
|
+
it. A JSON string cannot hold a raw newline, so every line start is structural and that
|
|
239
|
+
line is a depth-one member. A file with no such line (absent, empty, all on one line)
|
|
240
|
+
gets two spaces, which is what this writer always used. `runtime-ts`'s `detectedIndent`
|
|
241
|
+
applies the same rule to the same text.
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
text = path.read_text(encoding="utf-8")
|
|
245
|
+
except (OSError, UnicodeDecodeError):
|
|
246
|
+
return " "
|
|
247
|
+
for line in text.split("\n"):
|
|
248
|
+
if line[:1] not in (" ", "\t") or not line.strip(" \t\r"):
|
|
249
|
+
continue
|
|
250
|
+
if line[0] == "\t":
|
|
251
|
+
return "\t"
|
|
252
|
+
return " " * (len(line) - len(line.lstrip(" ")))
|
|
253
|
+
return " "
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# A lone surrogate is all `ensure_ascii=False` leaves unencodable: `"\ud800"` in the host file
|
|
257
|
+
# decodes to one, and UTF-8 cannot carry it. It goes back out as the escape it came in as.
|
|
258
|
+
_LONE_SURROGATE = re.compile("[\ud800-\udfff]")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _dump_host_json(data: dict, indent: str) -> str:
|
|
262
|
+
"""The host file's text: the user's indent unit, non-ASCII as literal UTF-8 (#118).
|
|
263
|
+
|
|
264
|
+
`ensure_ascii=False` escapes only `"`, `\\` and C0 controls; everything else, DEL and
|
|
265
|
+
U+2028 included, is written as itself, so `"ไทย 🎉"` stays `"ไทย 🎉"` rather than coming
|
|
266
|
+
back as `\\u0e44...`. `runtime-ts` reaches the same bytes from `dumpJson`'s ASCII output
|
|
267
|
+
(`hostJsonText`), and `tools/conformance/suites/install.mjs` compares the two.
|
|
268
|
+
"""
|
|
269
|
+
text = json.dumps(data, indent=indent, ensure_ascii=False)
|
|
270
|
+
return _LONE_SURROGATE.sub(lambda m: f"\\u{ord(m.group()):04x}", text)
|
|
271
|
+
|
|
272
|
+
|
|
239
273
|
def _backup_target(path: Path) -> Path:
|
|
240
274
|
"""The name the next backup of `path` gets: the first one that is not taken.
|
|
241
275
|
|
|
@@ -743,8 +777,8 @@ def install_hooks(
|
|
|
743
777
|
5. dated backup, then one atomic write.
|
|
744
778
|
|
|
745
779
|
The write itself is `_write_config` — the same temp-file replace, the same carried mode,
|
|
746
|
-
the same
|
|
747
|
-
`--install` leaves are produced by one function (RULING Q3.5).
|
|
780
|
+
the same detected indent and literal non-ASCII through `_dump_host_json` (#118) — so the
|
|
781
|
+
bytes this leaves and the bytes `--install` leaves are produced by one function (RULING Q3.5).
|
|
748
782
|
"""
|
|
749
783
|
command, extra = this_command()
|
|
750
784
|
rendered = hook_command(command, extra)
|
|
@@ -1405,6 +1405,23 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
|
1405
1405
|
notice = _notice()
|
|
1406
1406
|
return {**answer, "bantamkit_degraded": notice} if notice else answer
|
|
1407
1407
|
|
|
1408
|
+
def _noted_json(document: str) -> str:
|
|
1409
|
+
"""A reply that is ONE JSON DOCUMENT rendered as text (`skill_audit`, `token_ledger`),
|
|
1410
|
+
plus the footer if there is one, as a `bantamkit_degraded` key LAST in it (#124).
|
|
1411
|
+
|
|
1412
|
+
`_noted` would append a sentence after the closing brace and the reply would stop
|
|
1413
|
+
parsing. The key is spliced into the text rather than the document being re-parsed,
|
|
1414
|
+
so every byte of a degraded reply before the key is the healthy reply's: `as_json`
|
|
1415
|
+
is `json.dumps(dict, indent=2, ensure_ascii=False)` of a non-empty dict, so it always
|
|
1416
|
+
ends `\\n}`, and the key goes in at that two-space indent, where `json.dumps` would
|
|
1417
|
+
have put it.
|
|
1418
|
+
"""
|
|
1419
|
+
notice = _notice()
|
|
1420
|
+
if not notice:
|
|
1421
|
+
return document
|
|
1422
|
+
key = json.dumps(notice, ensure_ascii=False)
|
|
1423
|
+
return f'{document[:-2]},\n "bantamkit_degraded": {key}\n}}'
|
|
1424
|
+
|
|
1408
1425
|
def memory_save(
|
|
1409
1426
|
type: str, name: str, description: str, body: str, links: list[str] | None = None
|
|
1410
1427
|
) -> str:
|
|
@@ -1772,7 +1789,7 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
|
1772
1789
|
"omissions": len(result.omissions),
|
|
1773
1790
|
},
|
|
1774
1791
|
)
|
|
1775
|
-
return
|
|
1792
|
+
return _noted_json(result.as_json())
|
|
1776
1793
|
|
|
1777
1794
|
def repo_map(
|
|
1778
1795
|
root: str,
|
|
@@ -1903,7 +1920,7 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
|
|
|
1903
1920
|
"sessions": len(result.sessions),
|
|
1904
1921
|
},
|
|
1905
1922
|
)
|
|
1906
|
-
return
|
|
1923
|
+
return _noted_json(result.as_json())
|
|
1907
1924
|
|
|
1908
1925
|
# The served surface, in one place, read out of the asset pack. Adding a tool here
|
|
1909
1926
|
# without an asset raises AssetNotFound at startup — the manifest cannot drift behind
|
|
@@ -2214,8 +2231,50 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2214
2231
|
return parser
|
|
2215
2232
|
|
|
2216
2233
|
|
|
2234
|
+
#: job66/#139: the flags that each ACT and then exit, in registration order. At most one of
|
|
2235
|
+
#: them may be given. WAS: `_dispatch` ran the first in registration order and dropped the
|
|
2236
|
+
#: rest at exit 0, so `--install cursor --assets-root` printed the pack root, wrote no host
|
|
2237
|
+
#: config, and told the script that called it nothing.
|
|
2238
|
+
_ONE_SHOT_DESTS = (
|
|
2239
|
+
"assets_root",
|
|
2240
|
+
"hook",
|
|
2241
|
+
"mcp_report",
|
|
2242
|
+
"statusline",
|
|
2243
|
+
"update",
|
|
2244
|
+
"install",
|
|
2245
|
+
"install_hooks",
|
|
2246
|
+
"remove_hooks",
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
|
|
2250
|
+
def _refuse_stacked_one_shots(parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:
|
|
2251
|
+
"""Two one-shot flags together are a USAGE error: the usage block, one sentence, exit 2.
|
|
2252
|
+
|
|
2253
|
+
The sentence is argparse's own mutually-exclusive wording, but this is NOT
|
|
2254
|
+
`add_mutually_exclusive_group()`, and the reason is measured: the eight flags are not
|
|
2255
|
+
contiguous in the parser (`--k`, `--index-budget` and `--force` sit between them), and
|
|
2256
|
+
CPython 3.12/3.13 leave a non-contiguous group out of the usage line while 3.14 brackets
|
|
2257
|
+
it as `[--assets-root | --hook | ...]`. A real group would make `-h` depend on the
|
|
2258
|
+
interpreter's minor version, and both `-h` outputs are compared byte for byte with the
|
|
2259
|
+
port's. So the check runs after the parse, over the same actions, and names the two
|
|
2260
|
+
EARLIEST-REGISTERED flags given -- deterministic, and the same order
|
|
2261
|
+
`runtime-ts/src/cli.ts` names them in.
|
|
2262
|
+
"""
|
|
2263
|
+
given = [
|
|
2264
|
+
action
|
|
2265
|
+
for action in parser._actions
|
|
2266
|
+
if action.dest in _ONE_SHOT_DESTS and getattr(args, action.dest) not in (None, False)
|
|
2267
|
+
]
|
|
2268
|
+
if len(given) > 1:
|
|
2269
|
+
first, second = ("/".join(a.option_strings) for a in given[:2])
|
|
2270
|
+
parser.error(f"argument {second}: not allowed with argument {first}")
|
|
2271
|
+
|
|
2272
|
+
|
|
2217
2273
|
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
2218
|
-
|
|
2274
|
+
parser = _build_parser()
|
|
2275
|
+
args = parser.parse_args(argv)
|
|
2276
|
+
_refuse_stacked_one_shots(parser, args)
|
|
2277
|
+
return args
|
|
2219
2278
|
|
|
2220
2279
|
|
|
2221
2280
|
def _typed_bare_at_a_terminal(
|
|
@@ -2381,10 +2440,11 @@ def _check_store_flag(raw: str) -> None:
|
|
|
2381
2440
|
def _run_hook(args: argparse.Namespace) -> None:
|
|
2382
2441
|
"""`--hook`: one JSON event on stdin, at most one JSON object on stdout, EXIT 0 ALWAYS.
|
|
2383
2442
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2443
|
+
ALONE ON ITS COMMAND LINE (job66/#139). `--hook` beside another one-shot flag is a usage
|
|
2444
|
+
error at exit 2, and `--store`/`--start`/`--yes`/`--force` beside it are refused at exit
|
|
2445
|
+
1 before this runs. WAS: dispatch order was registration order, so `--mcp-report
|
|
2446
|
+
--install cursor` printed a report and silently wrote nothing, and `--hook --store X`
|
|
2447
|
+
ignored X.
|
|
2388
2448
|
|
|
2389
2449
|
EXIT 0 ALWAYS, AND THAT IS THE CONTRACT, not a convenience. A hook that exits non-zero
|
|
2390
2450
|
or lets a traceback reach stderr is rendered by the host as an error on the USER'S
|
|
@@ -2719,9 +2779,16 @@ def main() -> None:
|
|
|
2719
2779
|
Written through `sys.stderr.buffer`, like every other refusal in this file, so the line
|
|
2720
2780
|
ends LF on Windows too and cannot drift from the port's `process.stderr.write`.
|
|
2721
2781
|
"""
|
|
2722
|
-
if MCPServer is None:
|
|
2723
|
-
raise SystemExit(_INSTALL_HINT)
|
|
2724
2782
|
args = _parse_args()
|
|
2783
|
+
# job66/#173: the SDK check sits AFTER the parse and is skipped for the arms that never
|
|
2784
|
+
# build a server. `--hook`, `--statusline` and `--mcp-report` are registered with hosts
|
|
2785
|
+
# and run on EVERY event; they import nothing from `mcp`, so a plain `pip install
|
|
2786
|
+
# bantamkit` must run them (docs/hooks.md: the hook "exits 0 always"). WAS: the check ran
|
|
2787
|
+
# before `_parse_args()`, so all three exited 1 with the install hint on every event.
|
|
2788
|
+
if MCPServer is None and not (
|
|
2789
|
+
args.assets_root or args.hook or args.mcp_report or args.statusline
|
|
2790
|
+
):
|
|
2791
|
+
raise SystemExit(_INSTALL_HINT)
|
|
2725
2792
|
try:
|
|
2726
2793
|
_dispatch(args)
|
|
2727
2794
|
except BantamError as e:
|
|
@@ -2731,6 +2798,29 @@ def main() -> None:
|
|
|
2731
2798
|
|
|
2732
2799
|
|
|
2733
2800
|
def _dispatch(args: argparse.Namespace) -> None:
|
|
2801
|
+
# job66/#139: THE MODIFIERS ARE CHECKED BEFORE ANY FLAG ACTS. `_parse_args` already
|
|
2802
|
+
# refuses two one-shot flags together; what is left is a modifier given to a flag that
|
|
2803
|
+
# does not read it. WAS: these two refusals sat after every acting arm, so `--hook --yes`,
|
|
2804
|
+
# `--hook --force` and `--statusline --force` ran the first flag and dropped the modifier
|
|
2805
|
+
# at exit 0 while `--force` alone was refused. Same sentences, same exit 1, earlier.
|
|
2806
|
+
#
|
|
2807
|
+
# `--force` alone is a typo with a plausible reading -- somebody meant to install and
|
|
2808
|
+
# dropped the flag that says where. Refusing names the missing half instead of running
|
|
2809
|
+
# something that ignores it.
|
|
2810
|
+
if args.force and not args.install:
|
|
2811
|
+
raise SystemExit("--force is only meaningful with --install")
|
|
2812
|
+
# The same reading, and the same refusal, for the consent flag. The sentence names BOTH
|
|
2813
|
+
# flags since the ruling of 2026-09-20: naming only one would send an operator who
|
|
2814
|
+
# mistyped `--remove-hooks` looking for a flag they already had.
|
|
2815
|
+
if args.yes and not (args.install_hooks or args.remove_hooks):
|
|
2816
|
+
raise SystemExit("--yes is only meaningful with --install-hooks or --remove-hooks")
|
|
2817
|
+
# REFUSED, NOT HONOURED (job66/#139). The hook binds the store the winning MCP
|
|
2818
|
+
# registration pins, resolved from the cwd the HOST put in the event -- `_run_hook` never
|
|
2819
|
+
# reads `args` -- so `--hook --store X` used to inject nothing from X and say nothing.
|
|
2820
|
+
# Honouring it would give a hook registration a second, competing way to name a store.
|
|
2821
|
+
for flag in ("store", "start"):
|
|
2822
|
+
if args.hook and getattr(args, flag) is not None:
|
|
2823
|
+
raise SystemExit(f"--{flag} is not meaningful with --hook")
|
|
2734
2824
|
# Before `_build_memory`, which touches the filesystem, and before the server exists at
|
|
2735
2825
|
# all -- the Node arm returns from `main` here too, ahead of `new RawStdioTransport()`.
|
|
2736
2826
|
if args.assets_root:
|
|
@@ -2751,27 +2841,14 @@ def _dispatch(args: argparse.Namespace) -> None:
|
|
|
2751
2841
|
if args.install:
|
|
2752
2842
|
_run_install(args)
|
|
2753
2843
|
return
|
|
2754
|
-
#
|
|
2755
|
-
#
|
|
2844
|
+
# At most one of these arms can be reached (job66/#139: `_parse_args` refuses a second
|
|
2845
|
+
# one-shot flag), so the order below no longer decides what a stacked argv does.
|
|
2756
2846
|
if args.install_hooks:
|
|
2757
2847
|
_run_install_hooks(args)
|
|
2758
2848
|
return
|
|
2759
2849
|
if args.remove_hooks:
|
|
2760
2850
|
_run_remove_hooks(args)
|
|
2761
2851
|
return
|
|
2762
|
-
# `--force` alone is a typo with a plausible reading -- somebody meant to install and
|
|
2763
|
-
# dropped the flag that says where. Refusing names the missing half instead of starting
|
|
2764
|
-
# a server that ignores it.
|
|
2765
|
-
if args.force:
|
|
2766
|
-
raise SystemExit("--force is only meaningful with --install")
|
|
2767
|
-
# The same reading, and the same refusal, for the consent flag: `--yes` on its own is
|
|
2768
|
-
# somebody who meant to install or remove hooks and dropped the flag that says which. It
|
|
2769
|
-
# is checked AFTER `--force` because that is registration order, and both are checked
|
|
2770
|
-
# after every flag that acts, so `--install-hooks --yes` never reaches either of them. The
|
|
2771
|
-
# sentence names BOTH flags since the ruling of 2026-09-20: naming only one would send an
|
|
2772
|
-
# operator who mistyped `--remove-hooks` looking for a flag they already had.
|
|
2773
|
-
if args.yes:
|
|
2774
|
-
raise SystemExit("--yes is only meaningful with --install-hooks or --remove-hooks")
|
|
2775
2852
|
# A PERSON TYPED IT. `_typed_bare_at_a_terminal` carries the whole argument; what
|
|
2776
2853
|
# belongs here is only that this sits BEFORE `_build_memory`, which is what creates a
|
|
2777
2854
|
# store. Somebody who typed a command to see what it does has not asked for a
|
|
@@ -17,6 +17,23 @@ for a tool this server serves, whose arguments hold one. Every other line goes t
|
|
|
17
17
|
byte for byte, as before -- including a lone surrogate anywhere else in a frame, which the SDK
|
|
18
18
|
still drops (the malformed-frame class, not this one).
|
|
19
19
|
|
|
20
|
+
TWO MORE FRAMES THE SDK DROPPED ARE ANSWERED (job66/#148). A line that is not JSON, or JSON
|
|
21
|
+
that is not a JSON-RPC message (a batch array, a bare number, a request with no `jsonrpc`), went
|
|
22
|
+
the same way: the reader's parse failed, the exception went to the session, nothing was sent.
|
|
23
|
+
JSON-RPC 2.0 section 5.1 prescribes an answer with `id: null`, so the wrapper now asks the SDK's
|
|
24
|
+
own adapter first and, where the SDK would drop the line, answers `-32700 Parse error` (the
|
|
25
|
+
standard library's `json` cannot read it either) or `-32600 Invalid Request` (it can). The
|
|
26
|
+
port writes the same two frames from `runtime-ts/src/mcp/transport.ts`; the `wire` suite
|
|
27
|
+
compares them. A blank line still goes to the SDK and is still dropped, as the port skips it.
|
|
28
|
+
|
|
29
|
+
A HOST THAT CLOSED ITS READ END ENDS THE SESSION QUIETLY (job66/#181). Measured before this
|
|
30
|
+
change: a 3 MB answer written to a closed stdout raised `BrokenPipeError` out of the SDK's writer,
|
|
31
|
+
and the process then HUNG until stdin closed -- the reader is a blocking `readline` on an anyio
|
|
32
|
+
worker thread, which a cancelled task group must wait for -- and only then died, exit 1, with an
|
|
33
|
+
exception-group traceback. Stdin is now read on a DAEMON thread, so a cancelled reader is
|
|
34
|
+
abandoned rather than awaited, and the broken pipe is caught: exit 0, nothing on stderr, the
|
|
35
|
+
same as the port and the same as stdin EOF.
|
|
36
|
+
|
|
20
37
|
PRIVATE SDK NAMES, ON PURPOSE. `_claim_fd`, `_open_stdin_diversion`, `_UnownedTextWrapper`
|
|
21
38
|
and `MCPServer._lowlevel_server` are not public API. They are what `stdio_server()` and
|
|
22
39
|
`run_stdio_async()` themselves use, the SDK is upper-capped in `runtime-py/pyproject.toml`
|
|
@@ -26,13 +43,14 @@ child a handler spawns. A rename inside the cap fails at import or at first use,
|
|
|
26
43
|
|
|
27
44
|
from __future__ import annotations
|
|
28
45
|
|
|
46
|
+
import asyncio
|
|
29
47
|
import json
|
|
30
48
|
import re
|
|
31
49
|
import sys
|
|
50
|
+
import threading
|
|
32
51
|
from collections.abc import AsyncIterator
|
|
33
52
|
from typing import Any
|
|
34
53
|
|
|
35
|
-
import anyio
|
|
36
54
|
import mcp_types as types
|
|
37
55
|
from mcp.server import stdio as _sdk_stdio
|
|
38
56
|
from mcp.shared.message import SessionMessage
|
|
@@ -110,8 +128,87 @@ def refusal_for(line: str, tools: frozenset[str]) -> types.JSONRPCResponse | Non
|
|
|
110
128
|
)
|
|
111
129
|
|
|
112
130
|
|
|
131
|
+
def _no_constant(name: str) -> Any:
|
|
132
|
+
raise ValueError(f"not JSON: {name}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def malformed_frame_error(line: str) -> types.JSONRPCError | None:
|
|
136
|
+
"""The `id: null` error for a line the SDK's reader would drop, or `None` (#148).
|
|
137
|
+
|
|
138
|
+
`-32700` when the line is not JSON at all -- `NaN` and `Infinity` included, which
|
|
139
|
+
`json.loads` would otherwise accept and JSON does not -- and `-32600` when it is JSON
|
|
140
|
+
but not a message the SDK's own adapter accepts. A blank line is `None`: it is skipped,
|
|
141
|
+
not answered, on both runtimes.
|
|
142
|
+
"""
|
|
143
|
+
if not line.strip():
|
|
144
|
+
return None
|
|
145
|
+
# pydantic's parser reads `NaN` and `Infinity` where JSON has none; the port's
|
|
146
|
+
# `JSON.parse` does not. Only a line spelling one can hold one, so only that line pays.
|
|
147
|
+
if ("NaN" in line or "Infinity" in line) and not _is_strict_json(line):
|
|
148
|
+
return _error(-32700, "Parse error")
|
|
149
|
+
try:
|
|
150
|
+
types.jsonrpc_message_adapter.validate_json(line, by_name=False)
|
|
151
|
+
except Exception: # noqa: BLE001 - the SDK's reader catches exactly this broadly
|
|
152
|
+
pass
|
|
153
|
+
else:
|
|
154
|
+
return None
|
|
155
|
+
# A lone surrogate lands here too: `json` reads it, pydantic's parser does not.
|
|
156
|
+
if _is_strict_json(line):
|
|
157
|
+
return _error(-32600, "Invalid Request")
|
|
158
|
+
return _error(-32700, "Parse error")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_strict_json(line: str) -> bool:
|
|
162
|
+
try:
|
|
163
|
+
json.loads(line, parse_constant=_no_constant)
|
|
164
|
+
except ValueError:
|
|
165
|
+
return False
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _error(code: int, message: str) -> types.JSONRPCError:
|
|
170
|
+
error = types.ErrorData(code=code, message=message)
|
|
171
|
+
return types.JSONRPCError(jsonrpc="2.0", id=None, error=error)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _DaemonLines:
|
|
175
|
+
"""The lines of a text stream, read on a DAEMON thread (#181).
|
|
176
|
+
|
|
177
|
+
`anyio.wrap_file` reads on a worker thread the task group has to wait for, so a session
|
|
178
|
+
whose stdout broke could not end until stdin did. A daemon thread is abandoned instead.
|
|
179
|
+
One line is read ahead at most: the thread blocks until the loop has taken the last one.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, text: Any) -> None:
|
|
183
|
+
self._text = text
|
|
184
|
+
|
|
185
|
+
def __aiter__(self) -> AsyncIterator[str]:
|
|
186
|
+
return self._lines()
|
|
187
|
+
|
|
188
|
+
async def _lines(self) -> AsyncIterator[str]:
|
|
189
|
+
loop = asyncio.get_running_loop()
|
|
190
|
+
queue: asyncio.Queue[str | None] = asyncio.Queue(maxsize=1)
|
|
191
|
+
|
|
192
|
+
def pump() -> None:
|
|
193
|
+
try:
|
|
194
|
+
for line in self._text:
|
|
195
|
+
asyncio.run_coroutine_threadsafe(queue.put(line), loop).result()
|
|
196
|
+
except (OSError, ValueError, RuntimeError):
|
|
197
|
+
pass # the fd was released or the loop is gone: nobody is reading any more
|
|
198
|
+
finally:
|
|
199
|
+
try:
|
|
200
|
+
asyncio.run_coroutine_threadsafe(queue.put(None), loop)
|
|
201
|
+
except RuntimeError:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
threading.Thread(target=pump, name="bantamkit-stdin", daemon=True).start()
|
|
205
|
+
while (line := await queue.get()) is not None:
|
|
206
|
+
yield line
|
|
207
|
+
|
|
208
|
+
|
|
113
209
|
class _Lines:
|
|
114
|
-
"""The SDK's stdin, with the lone-surrogate `tools/call` frames answered and withheld
|
|
210
|
+
"""The SDK's stdin, with the lone-surrogate `tools/call` frames answered and withheld,
|
|
211
|
+
and the frames the SDK would drop answered `-32700`/`-32600` (#148)."""
|
|
115
212
|
|
|
116
213
|
def __init__(self, inner: Any, tools: frozenset[str]) -> None:
|
|
117
214
|
self._inner = inner
|
|
@@ -123,7 +220,9 @@ class _Lines:
|
|
|
123
220
|
|
|
124
221
|
async def _lines(self) -> AsyncIterator[str]:
|
|
125
222
|
async for line in self._inner:
|
|
126
|
-
response
|
|
223
|
+
response: types.JSONRPCResponse | types.JSONRPCError | None = None
|
|
224
|
+
if self.write_stream is not None:
|
|
225
|
+
response = refusal_for(line, self._tools) or malformed_frame_error(line)
|
|
127
226
|
if response is None:
|
|
128
227
|
yield line
|
|
129
228
|
else:
|
|
@@ -136,11 +235,14 @@ async def serve_stdio(server: Any) -> None:
|
|
|
136
235
|
buffer, release = _sdk_stdio._claim_fd(0, sys.stdin, "rb", _sdk_stdio._open_stdin_diversion)
|
|
137
236
|
try:
|
|
138
237
|
text = _sdk_stdio._UnownedTextWrapper(buffer, encoding="utf-8", errors="replace")
|
|
139
|
-
lines = _Lines(
|
|
238
|
+
lines = _Lines(_DaemonLines(text), tools)
|
|
140
239
|
async with _sdk_stdio.stdio_server(stdin=lines) as (read_stream, write_stream): # type: ignore[arg-type]
|
|
141
240
|
lines.write_stream = write_stream
|
|
142
241
|
lowlevel = server._lowlevel_server
|
|
143
242
|
await lowlevel.run(read_stream, write_stream, lowlevel.create_initialization_options())
|
|
243
|
+
except* (BrokenPipeError, ConnectionResetError):
|
|
244
|
+
# The host closed its read end (#181): the session is over, exactly as on stdin EOF.
|
|
245
|
+
pass
|
|
144
246
|
finally:
|
|
145
247
|
if release is not None:
|
|
146
248
|
release()
|
|
@@ -69,6 +69,16 @@ THE PARITY HAZARDS, EACH PINNED
|
|
|
69
69
|
same measured hole.
|
|
70
70
|
- **A bare `Infinity`.** `json.loads` accepts the token and `JSON.parse` throws. The line
|
|
71
71
|
decoder closes it, so a transcript carrying one is `unparsed-line` on both sides.
|
|
72
|
+
- **A leading UTF-8 BOM** (#150). `read_text(encoding="utf-8")` keeps U+FEFF and `json.loads`
|
|
73
|
+
refuses it, while a default `TextDecoder` strips it from the FIRST line only. The port decodes
|
|
74
|
+
with `ignoreBOM: true`, so a BOM is `unparsed-line` on both sides, on any line. Tolerating it
|
|
75
|
+
instead was the other choice and was not taken: the host never writes one, RFC 8259 forbids
|
|
76
|
+
adding one, and a line that is not what the host writes is exactly what an omission names.
|
|
77
|
+
- **Directory symlinks** (#151). `Path.is_dir()` and `statSync` both FOLLOW a link, so a link
|
|
78
|
+
to an ancestor was walked until the OS refused it (ELOOP, ~32 levels on macOS) and the one
|
|
79
|
+
transcript beneath it was counted once per level. A symlinked directory is now neither
|
|
80
|
+
descended into nor read, on both sides -- `skill_audit`'s rule (`os.walk`'s
|
|
81
|
+
`followlinks=False`). A symlink to a FILE is still read: it cannot loop.
|
|
72
82
|
|
|
73
83
|
THE COST IS THE REFUSAL, AND THAT IS THE NORMAL ANSWER
|
|
74
84
|
------------------------------------------------------
|
|
@@ -261,6 +271,12 @@ class _Accumulator:
|
|
|
261
271
|
self.tokens = {cls: 0 for cls in TOKEN_CLASSES}
|
|
262
272
|
|
|
263
273
|
|
|
274
|
+
def _is_junction(path: Path) -> bool:
|
|
275
|
+
"""`Path.is_junction()` where it exists (3.12+); before it, no junction is named."""
|
|
276
|
+
probe = getattr(path, "is_junction", None)
|
|
277
|
+
return bool(probe()) if probe is not None else False
|
|
278
|
+
|
|
279
|
+
|
|
264
280
|
def _walk(base: Path) -> list[tuple[Path, str]]:
|
|
265
281
|
"""Every `*.jsonl` under `base`, in one deterministic order, each with the name it is called by.
|
|
266
282
|
|
|
@@ -285,7 +301,12 @@ def _walk(base: Path) -> list[tuple[Path, str]]:
|
|
|
285
301
|
child = directory / name
|
|
286
302
|
rel = f"{prefix}{name}"
|
|
287
303
|
if child.is_dir():
|
|
288
|
-
|
|
304
|
+
# #151: a symlinked directory is not a transcript tree. `is_dir()` follows the
|
|
305
|
+
# link, so a link to a directory lands in this arm and is skipped here: neither
|
|
306
|
+
# walked nor read. Node's `lstatSync` calls a Windows junction a symbolic link,
|
|
307
|
+
# so a junction is skipped too where this Python can name one.
|
|
308
|
+
if not (child.is_symlink() or _is_junction(child)):
|
|
309
|
+
visit(child, f"{rel}/")
|
|
289
310
|
elif name.endswith(TRANSCRIPT_SUFFIX):
|
|
290
311
|
found.append((child, rel))
|
|
291
312
|
|
|
@@ -119,6 +119,7 @@ from __future__ import annotations
|
|
|
119
119
|
|
|
120
120
|
import json
|
|
121
121
|
import re
|
|
122
|
+
import sys
|
|
122
123
|
from dataclasses import dataclass
|
|
123
124
|
from pathlib import Path
|
|
124
125
|
from typing import Any
|
|
@@ -151,6 +152,7 @@ __all__ = [
|
|
|
151
152
|
"decide",
|
|
152
153
|
"update_status",
|
|
153
154
|
"update_line",
|
|
155
|
+
"update_command",
|
|
154
156
|
]
|
|
155
157
|
|
|
156
158
|
#: The key of the record THIS runtime reads. Divergent by construction: `runtime-ts` reads
|
|
@@ -173,9 +175,16 @@ UPDATE_NEVER = "update: never checked."
|
|
|
173
175
|
|
|
174
176
|
#: The whole point of the feature. `{latest}` and `{installed}` are BOTH here so the reader
|
|
175
177
|
#: can falsify the claim, the same reason `selfupdate.COMPARISON` carries both.
|
|
178
|
+
#:
|
|
179
|
+
#: `{command}` is `update_command()` (job66/#171). WAS: `` `{program} --update` ``, the bare
|
|
180
|
+
#: command, which a pip venv or an npm kept install puts on nobody's PATH -- so the sentence
|
|
181
|
+
#: answered `command not found` from a GUI-launched host's shell. The TEMPLATE is still
|
|
182
|
+
#: byte-identical across runtimes; what each runtime fills into `{command}` is its own
|
|
183
|
+
#: interpreter and entry point (the `docs/porting.md` row *the update command names each
|
|
184
|
+
#: runtime's own interpreter*).
|
|
176
185
|
UPDATE_AVAILABLE = (
|
|
177
186
|
"update: {program} {installed} is running; the package index has {latest} — run "
|
|
178
|
-
"`{
|
|
187
|
+
"`{command}`, then reconnect the host."
|
|
179
188
|
)
|
|
180
189
|
|
|
181
190
|
#: Names the date because the record may be months old.
|
|
@@ -347,11 +356,24 @@ def _checked_date(record: dict[str, Any], key: str) -> str | None:
|
|
|
347
356
|
return found.group(1) if found else None
|
|
348
357
|
|
|
349
358
|
|
|
359
|
+
def update_command() -> str:
|
|
360
|
+
"""The command `UPDATE_AVAILABLE` tells the operator to run: THIS interpreter, absolute.
|
|
361
|
+
|
|
362
|
+
job66/#171. `sys.executable`, double-quoted, then `-m bantamkit.mcpserver --update` --
|
|
363
|
+
the interpreter that is running this server, so the command needs no PATH (a host
|
|
364
|
+
launched from a GUI gets `/usr/bin:/bin`) and runs the same install that answered. The
|
|
365
|
+
quotes hold a home directory with a space in it. The port fills in `process.execPath`
|
|
366
|
+
and its own `cli.js` instead.
|
|
367
|
+
"""
|
|
368
|
+
return f'"{sys.executable}" -m bantamkit.mcpserver --update'
|
|
369
|
+
|
|
370
|
+
|
|
350
371
|
def decide(
|
|
351
372
|
installed: str,
|
|
352
373
|
source: str,
|
|
353
374
|
record: dict[str, Any] | None,
|
|
354
375
|
key: str = KEY,
|
|
376
|
+
command: str | None = None,
|
|
355
377
|
) -> UpdateStatus:
|
|
356
378
|
"""The five-state decision, pure: no filesystem, no clock, no network, no TTL.
|
|
357
379
|
|
|
@@ -370,7 +392,12 @@ def decide(
|
|
|
370
392
|
if order < 0:
|
|
371
393
|
return UpdateStatus(
|
|
372
394
|
STATE_AVAILABLE,
|
|
373
|
-
UPDATE_AVAILABLE.format(
|
|
395
|
+
UPDATE_AVAILABLE.format(
|
|
396
|
+
program=PROGRAM,
|
|
397
|
+
installed=installed,
|
|
398
|
+
latest=latest,
|
|
399
|
+
command=update_command() if command is None else command,
|
|
400
|
+
),
|
|
374
401
|
)
|
|
375
402
|
if order == 0:
|
|
376
403
|
return UpdateStatus(
|
|
@@ -387,16 +414,18 @@ def update_status(
|
|
|
387
414
|
installed: str,
|
|
388
415
|
key: str = KEY,
|
|
389
416
|
path: Path | None = None,
|
|
417
|
+
command: str | None = None,
|
|
390
418
|
) -> UpdateStatus:
|
|
391
419
|
"""Read the record and decide. The whole reader, in one call, for `bantamkit_status`."""
|
|
392
420
|
source, record = load_record(path)
|
|
393
|
-
return decide(installed, source, record, key)
|
|
421
|
+
return decide(installed, source, record, key, command)
|
|
394
422
|
|
|
395
423
|
|
|
396
424
|
def update_line(
|
|
397
425
|
installed: str,
|
|
398
426
|
key: str = KEY,
|
|
399
427
|
path: Path | None = None,
|
|
428
|
+
command: str | None = None,
|
|
400
429
|
) -> str:
|
|
401
430
|
"""Exactly one of the five sentences. Always a line: there is no silent state here."""
|
|
402
|
-
return update_status(installed, key, path).line
|
|
431
|
+
return update_status(installed, key, path, command).line
|