boost-skill-cli 1.0.93__tar.gz → 1.0.95__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.
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/PKG-INFO +1 -1
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/_version.py +2 -2
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/configuration.py +10 -186
- boost_skill_cli-1.0.95/boost_cli/core/mcp.py +165 -0
- boost_skill_cli-1.0.95/boost_cli/core/serve.py +151 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/PKG-INFO +1 -1
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/SOURCES.txt +2 -0
- boost_skill_cli-1.0.95/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +26 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap.html +23 -12
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_configuration.py +3 -3
- boost_skill_cli-1.0.95/tests/unit/test_mcp.py +241 -0
- boost_skill_cli-1.0.95/tests/unit/test_serve.py +137 -0
- boost_skill_cli-1.0.93/boost_cli/core/mcp.py +0 -70
- boost_skill_cli-1.0.93/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +0 -16
- boost_skill_cli-1.0.93/tests/unit/test_mcp.py +0 -83
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/.gitattributes +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/.gitignore +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/CLAUDE.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/CONTRIBUTING.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/LICENSE +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/MANIFEST.in +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/Makefile +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/README.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/SECURITY.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/__init__.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/__main__.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/cli.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/cliparse.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/__init__.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/bmad.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/discovery.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/hooks.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/info.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/intelligence.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/pkg.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/quality.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/taps.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/commands/team.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/__init__.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/agents.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/ai.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/catalog.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/claude_settings.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/config.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/dense.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/embed.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/frontmatter.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/gitutil.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/journal.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/lockfile.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/logs.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/output.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/paths.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/policy.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/rag.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/registry.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/staleness.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/store.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/typosquat.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/core/util.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/data/registries.json +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/errors.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_cli/spin.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/entry_points.txt +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/requires.txt +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/boost_skill_cli.egg-info/top_level.txt +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/.claude/settings.local.json +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/DEBUGGING.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/demo.gif +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/demo.tape +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/design-roadmap.html +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/index.html +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/mcp-hub.html +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/rag-architecture.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D01.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D02.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D03.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D04.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D05.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D06.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D07.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D08.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D09.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D10.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D11.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D12.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D13.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D14.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D15.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D16.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D17.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D18.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D19.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D20.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D21.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D22.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D23.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D24.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D25.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/BOOST-D26.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/accessibility-audit-pa11y-ci-axe-core.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/atomic-corruption-safe-lock-file-writes.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/atomic-skill-install-temp-dir-swap.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/autonomous-ship-workflow-and-isolated-worktree.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/bring-commands-under-mutation-testing.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/broken-link-and-anchor-checking-lychee.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/build-provenance-slsa-attestations.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/cache-the-catalog-entry-set-across-rag-queries.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/capability-manifest-and-least-privilege-policy.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/clean-env-install-smoke-pip-and-pipx.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/command-reference-docs-site.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/complexity-and-dead-code-radar-xenon-vulture.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/coverage-dashboard-codecov-free-for-oss.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/coverage-guided-fuzzing-atheris-oss-fuzz.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/crash-recorder-error-paths-core-logs-py.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/dependency-cve-gate-pip-audit.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/docstring-coverage-interrogate.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/end-of-options-guard-on-git-commands.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/extension-free-tests-core-dense-py.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/finish-mutation-hardening-across-core.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/fix-self-update-version-detection-dead-branch.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/fork-safe-network-proxy-handler.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/frontmatter-scalar-over-coercion.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/github-pages-deploy-is-broken-every-push.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/hash-pinned-reproducible-toolchain-uv-lock.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/html-validation-html-validate.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/integrity-verification-boost-verify.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/invocation-pid-logging-for-crash-correlation.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/layering-guard-import-linter.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/lighthouse-ci-on-the-pages-site.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/localize-the-stored-bm25-snippet.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/lockfile-enforcement-and-commit-pinning.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/log-timestamps-mislabeled-utc.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/lowest-version-resolution-uv-resolution-lowest-d.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/make-the-roadmaps-discoverable.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/markdown-consistency-markdownlint-cli2.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mcp-launch-objc-fork-safety.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/memoize-config-load-in-process.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/modernization-smells-refurb-pyupgrade.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mutation-hardening-core-frontmatter-py.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mutation-hardening-core-gitutil-py.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/mutation-hardening-core-store-py.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/one-command-every-env-nox.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/one-shared-atomic-write-helper.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/package-metadata-validation-twine-check-friends.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/patch-coverage-gate-diff-cover.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/performance-regression-gate-pytest-benchmark.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/post-deploy-smoke-headless-load-check.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/pre-release-python-canary-3-14t-free-threaded.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/promote-nav-footer-into-the-shared-style-system.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/prompt-injection-scanning-of-skill-markdown.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/property-based-tests-hypothesis-on-the-parsers.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/prose-and-terminology-linting-vale.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/prune-ignored-dirs-during-scan-dir-walk.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/quality-dashboard-sonarcloud-free-for-oss.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/reconcile-the-theme-drift.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/refresh-the-marketing-surface.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/reuse-helpers-kill-minor-dead-work.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/robust-tag-argument-parsing.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/rule-install-native-materialization.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/runner-egress-monitoring-stepsecurity-harden-run.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/sbom-aware-scanning-osv-scanner.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/sbom-on-every-release-cyclonedx-syft.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/second-type-checker-pyright.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/secret-and-pii-scanning-of-installed-skills.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/secret-scanning-gitleaks-push-protection.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/security-linting-bandit-via-ruff-s-rules.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/shift-left-gate-pre-commit-pre-commit-ci.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/single-imperative-rule-extractor.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/single-tech-stack-prober.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/split-oversized-command-modules.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/startup-and-import-time-budget-x-importtime.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/stop-re-serializing-entry-meta-on-every-search.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/supply-chain-posture-openssf-scorecard.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/tap-signing-and-provenance-sigstore-minisign.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/theme-asset-linting-stylelint-eslint.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/typo-detection-codespell.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/typosquat-and-name-confusion-detection.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/unify-tilde-two-copies-have-a-boundary-bug.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/untrack-generated-build-noise.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/update-diff-before-apply.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/visual-regression-pass-on-the-guide.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/widen-the-ruff-rule-surface-b-sim-c4-perf-ruf.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/windows-in-the-ci-matrix.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/workflow-linting-actionlint.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/workflow-sast-zizmor.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/docs/roadmap/items/workspace-scope-install.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/index.html +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/pyproject.toml +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/scripts/build_registries.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/scripts/build_roadmap.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/scripts/mutation_gate.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/setup.cfg +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/README.md +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/boost.css +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/boost.js +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/style/demo.html +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/conftest.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_bmad.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_discovery.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_hooks.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_info.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_intelligence.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_pkg.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_quality.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_taps.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_cli_team.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/functional/test_everyday_loop.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/make_fixture.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/smoke.sh +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_agents.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_ai.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_catalog.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_claude_settings.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_cliparse.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_config.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_dense.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_dense_fallback.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_embed.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_errors_and_cli_table.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_frontmatter.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_gitutil.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_journal.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_lockfile.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_logs.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_mutation_hardening.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_output.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_paths.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_pkg_confusions.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_policy.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_rag.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_registries_fresh.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_registry.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_roadmap_fresh.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_spin.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_staleness.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_store.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_token_parity.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_typosquat.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_util.py +0 -0
- {boost_skill_cli-1.0.93 → boost_skill_cli-1.0.95}/tests/unit/test_version.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.95'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 95)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
completions, schedule, serve, mcp, self-update."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import errno
|
|
6
5
|
import getpass
|
|
7
|
-
import html
|
|
8
6
|
import json
|
|
9
7
|
import os
|
|
10
8
|
import re
|
|
@@ -12,15 +10,13 @@ import shutil
|
|
|
12
10
|
import subprocess
|
|
13
11
|
import sys
|
|
14
12
|
import time
|
|
15
|
-
import urllib.parse
|
|
16
13
|
from datetime import datetime, timedelta
|
|
17
|
-
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
14
|
from pathlib import Path
|
|
19
15
|
|
|
20
16
|
from .. import cliparse
|
|
21
17
|
from .. import __version__
|
|
22
18
|
from ..core import agents, catalog, config, frontmatter, gitutil, journal
|
|
23
|
-
from ..core import lockfile, mcp, paths, policy, rag, registry, store, util
|
|
19
|
+
from ..core import lockfile, mcp, paths, policy, rag, registry, serve, store, util
|
|
24
20
|
from ..core import output as out
|
|
25
21
|
from ..errors import BoostError
|
|
26
22
|
|
|
@@ -730,106 +726,8 @@ def cmd_schedule(argv) -> int:
|
|
|
730
726
|
|
|
731
727
|
# ---------------------------------------------------------------- serve
|
|
732
728
|
|
|
733
|
-
_PAGE_CSS = ("body{background:#111;color:#ddd;font-family:ui-monospace,SFMono-Regular,"
|
|
734
|
-
"Menlo,monospace;max-width:720px;margin:2rem auto;padding:0 1rem}"
|
|
735
|
-
"h1{color:#fff;font-size:1.3rem}a{color:#7dc4ff;text-decoration:none}"
|
|
736
|
-
"a:hover{text-decoration:underline}table{border-collapse:collapse;width:100%}"
|
|
737
|
-
"th,td{text-align:left;padding:.3rem .8rem .3rem 0;"
|
|
738
|
-
"border-bottom:1px solid #2a2a2a}.dim{color:#777}")
|
|
739
|
-
|
|
740
|
-
_SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
def _serve_page() -> str:
|
|
744
|
-
installed = lockfile.installed()
|
|
745
|
-
entries = catalog.all_entries()
|
|
746
|
-
taps = registry.list_taps()
|
|
747
|
-
rows = "".join(
|
|
748
|
-
'<tr><td><a href="/skill/%s">%s</a></td><td>%s</td><td>%s</td></tr>'
|
|
749
|
-
% (urllib.parse.quote(n), html.escape(n),
|
|
750
|
-
html.escape(str(e.get("version", "?"))),
|
|
751
|
-
html.escape(str(e.get("tap", "?"))))
|
|
752
|
-
for n, e in sorted(installed.items()))
|
|
753
|
-
return ("<!doctype html><html><head><meta charset='utf-8'>"
|
|
754
|
-
"<title>boost — skill catalog</title><style>%s</style></head><body>"
|
|
755
|
-
"<h1>⚡ boost <span class='dim'>v%s</span></h1>"
|
|
756
|
-
"<p class='dim'>%d installed · %d available across %d taps</p>"
|
|
757
|
-
"<table><tr><th>skill</th><th>version</th><th>tap</th></tr>%s</table>"
|
|
758
|
-
"<p><a href='/installed.json'>installed.json</a> · "
|
|
759
|
-
"<a href='/catalog.json'>catalog.json</a></p></body></html>"
|
|
760
|
-
% (_PAGE_CSS, __version__, len(installed), len(entries), len(taps),
|
|
761
|
-
rows or "<tr><td colspan='3' class='dim'>nothing installed</td></tr>"))
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def _skill_text(name: str):
|
|
765
|
-
"""SKILL.md text for an installed skill, else from a tap. None if unknown."""
|
|
766
|
-
if not _SKILL_NAME_RE.fullmatch(name):
|
|
767
|
-
return None
|
|
768
|
-
fp = store.skill_store_dir(name) / "SKILL.md"
|
|
769
|
-
if fp.is_file():
|
|
770
|
-
return fp.read_text(encoding="utf-8", errors="replace")
|
|
771
|
-
for e in catalog.find(name):
|
|
772
|
-
try:
|
|
773
|
-
fp = registry.get(e["tap"]).path / e["skill_md"]
|
|
774
|
-
except BoostError:
|
|
775
|
-
continue
|
|
776
|
-
if fp.is_file():
|
|
777
|
-
return fp.read_text(encoding="utf-8", errors="replace")
|
|
778
|
-
return None
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
class _CatalogHandler(BaseHTTPRequestHandler):
|
|
782
|
-
server_version = "boost/" + __version__
|
|
783
|
-
|
|
784
|
-
def log_message(self, fmt, *args): # logged in _send instead
|
|
785
|
-
pass
|
|
786
|
-
|
|
787
|
-
def _send(self, status: int, ctype: str, body: bytes) -> None:
|
|
788
|
-
self.send_response(status)
|
|
789
|
-
self.send_header("Content-Type", ctype)
|
|
790
|
-
self.send_header("Content-Length", str(len(body)))
|
|
791
|
-
self.end_headers()
|
|
792
|
-
self.wfile.write(body)
|
|
793
|
-
out.dim(" %s %s → %d" % (self.command, self.path, status))
|
|
794
|
-
|
|
795
|
-
def do_GET(self):
|
|
796
|
-
path = urllib.parse.unquote(self.path.split("?", 1)[0])
|
|
797
|
-
try:
|
|
798
|
-
if path in ("/", "/index.html"):
|
|
799
|
-
self._send(200, "text/html; charset=utf-8", _serve_page().encode())
|
|
800
|
-
elif path == "/catalog.json":
|
|
801
|
-
self._send(200, "application/json",
|
|
802
|
-
json.dumps(catalog.all_entries(), indent=2).encode())
|
|
803
|
-
elif path == "/installed.json":
|
|
804
|
-
self._send(200, "application/json",
|
|
805
|
-
json.dumps(lockfile.read(), indent=2).encode())
|
|
806
|
-
elif path.startswith("/skill/"):
|
|
807
|
-
name = path[len("/skill/"):].strip("/")
|
|
808
|
-
if not _SKILL_NAME_RE.fullmatch(name):
|
|
809
|
-
self._send(404, "application/json",
|
|
810
|
-
json.dumps({"error": "no skill named %r" % name}).encode())
|
|
811
|
-
return
|
|
812
|
-
text = _skill_text(name)
|
|
813
|
-
if text is None:
|
|
814
|
-
self._send(404, "application/json",
|
|
815
|
-
json.dumps({"error": "no skill named %r" % name}).encode())
|
|
816
|
-
else:
|
|
817
|
-
self._send(200, "text/plain; charset=utf-8", text.encode())
|
|
818
|
-
else:
|
|
819
|
-
self._send(404, "application/json",
|
|
820
|
-
json.dumps({"error": "not found"}).encode())
|
|
821
|
-
except BrokenPipeError:
|
|
822
|
-
pass
|
|
823
|
-
except Exception as e: # noqa: BLE001 — keep the server alive
|
|
824
|
-
try:
|
|
825
|
-
self._send(500, "application/json",
|
|
826
|
-
json.dumps({"error": str(e)}).encode())
|
|
827
|
-
except Exception:
|
|
828
|
-
pass
|
|
829
|
-
|
|
830
|
-
|
|
831
729
|
def cmd_serve(argv) -> int:
|
|
832
|
-
"""boost serve [--port N] [--host H]"""
|
|
730
|
+
"""boost serve [--port N] [--host H] — thin wrapper over core.serve."""
|
|
833
731
|
p = cliparse.parser(
|
|
834
732
|
prog="boost serve",
|
|
835
733
|
description="Serve the skill catalog over HTTP (port 8787)")
|
|
@@ -839,24 +737,7 @@ def cmd_serve(argv) -> int:
|
|
|
839
737
|
p.add_argument("--host", default="127.0.0.1",
|
|
840
738
|
help="address to bind (default: 127.0.0.1)")
|
|
841
739
|
args = p.parse_args(argv)
|
|
842
|
-
|
|
843
|
-
try:
|
|
844
|
-
httpd = ThreadingHTTPServer((args.host, args.port), _CatalogHandler)
|
|
845
|
-
except OSError as e:
|
|
846
|
-
if e.errno == errno.EADDRINUSE:
|
|
847
|
-
raise BoostError("port %d is already in use" % args.port,
|
|
848
|
-
hint="pick another with --port")
|
|
849
|
-
raise BoostError("cannot bind %s:%d — %s" % (args.host, args.port, e),
|
|
850
|
-
hint="check --host and --port")
|
|
851
|
-
out.info("⚡ serving skill catalog on http://%s:%d %s"
|
|
852
|
-
% (args.host, args.port, out.c("(ctrl-c to stop)", out.DIM)))
|
|
853
|
-
try:
|
|
854
|
-
httpd.serve_forever()
|
|
855
|
-
except KeyboardInterrupt:
|
|
856
|
-
print()
|
|
857
|
-
out.ok("server stopped")
|
|
858
|
-
finally:
|
|
859
|
-
httpd.server_close()
|
|
740
|
+
serve.serve_http(args.host, args.port)
|
|
860
741
|
return 0
|
|
861
742
|
|
|
862
743
|
|
|
@@ -1021,70 +902,13 @@ _MCP_TOOLS = REGISTRY.specs()
|
|
|
1021
902
|
|
|
1022
903
|
|
|
1023
904
|
def _mcp_tool(tool: str, args: dict):
|
|
1024
|
-
"""Run one MCP tool -> (text, is_error). (None, _) for unknown tools.
|
|
1025
|
-
return REGISTRY.call(tool, args)
|
|
905
|
+
"""Run one MCP tool -> (text, is_error). (None, _) for unknown tools.
|
|
1026
906
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
return True
|
|
1033
|
-
except (BrokenPipeError, OSError):
|
|
1034
|
-
return False
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
def _mcp_serve_stdio() -> int:
|
|
1038
|
-
"""Newline-delimited JSON-RPC 2.0 MCP server on stdin/stdout."""
|
|
1039
|
-
while True:
|
|
1040
|
-
try:
|
|
1041
|
-
line = sys.stdin.readline()
|
|
1042
|
-
except (KeyboardInterrupt, OSError):
|
|
1043
|
-
return 0
|
|
1044
|
-
if not line: # EOF
|
|
1045
|
-
return 0
|
|
1046
|
-
line = line.strip()
|
|
1047
|
-
if not line:
|
|
1048
|
-
continue
|
|
1049
|
-
try:
|
|
1050
|
-
req = json.loads(line)
|
|
1051
|
-
except json.JSONDecodeError:
|
|
1052
|
-
if not _mcp_send({"jsonrpc": "2.0", "id": None,
|
|
1053
|
-
"error": {"code": -32700, "message": "parse error"}}):
|
|
1054
|
-
return 0
|
|
1055
|
-
continue
|
|
1056
|
-
method = str(req.get("method", ""))
|
|
1057
|
-
if "id" not in req: # notification (e.g. notifications/initialized)
|
|
1058
|
-
continue
|
|
1059
|
-
resp = {"jsonrpc": "2.0", "id": req.get("id")}
|
|
1060
|
-
if method == "initialize":
|
|
1061
|
-
resp["result"] = {"protocolVersion": "2024-11-05",
|
|
1062
|
-
"capabilities": {"tools": {}},
|
|
1063
|
-
"serverInfo": {"name": "boost", "version": __version__}}
|
|
1064
|
-
elif method == "ping":
|
|
1065
|
-
resp["result"] = {}
|
|
1066
|
-
elif method == "tools/list":
|
|
1067
|
-
resp["result"] = {"tools": _MCP_TOOLS}
|
|
1068
|
-
elif method == "tools/call":
|
|
1069
|
-
params = req.get("params") or {}
|
|
1070
|
-
tool = str(params.get("name", ""))
|
|
1071
|
-
try:
|
|
1072
|
-
text, is_err = _mcp_tool(tool, params.get("arguments") or {})
|
|
1073
|
-
except BoostError as e:
|
|
1074
|
-
text = "Error: %s" % e.message + ("\nhint: %s" % e.hint if e.hint else "")
|
|
1075
|
-
is_err = True
|
|
1076
|
-
except Exception as e: # noqa: BLE001 — server must not die
|
|
1077
|
-
text, is_err = "Error: %s" % e, True
|
|
1078
|
-
if text is None:
|
|
1079
|
-
resp["error"] = {"code": -32602, "message": "unknown tool %r" % tool}
|
|
1080
|
-
else:
|
|
1081
|
-
resp["result"] = {"content": [{"type": "text", "text": text}]}
|
|
1082
|
-
if is_err:
|
|
1083
|
-
resp["result"]["isError"] = True
|
|
1084
|
-
else:
|
|
1085
|
-
resp["error"] = {"code": -32601, "message": "method not found: %s" % method}
|
|
1086
|
-
if not _mcp_send(resp):
|
|
1087
|
-
return 0
|
|
907
|
+
Thin shim over ``REGISTRY.call`` kept for the functional tests; the JSON-RPC
|
|
908
|
+
protocol itself now lives in ``core/mcp.py`` (``handle_request`` /
|
|
909
|
+
``serve_stdio``).
|
|
910
|
+
"""
|
|
911
|
+
return REGISTRY.call(tool, args)
|
|
1088
912
|
|
|
1089
913
|
|
|
1090
914
|
def cmd_mcp(argv) -> int:
|
|
@@ -1100,7 +924,7 @@ def cmd_mcp(argv) -> int:
|
|
|
1100
924
|
args = p.parse_args(argv)
|
|
1101
925
|
|
|
1102
926
|
if args.stdio:
|
|
1103
|
-
return
|
|
927
|
+
return mcp.serve_stdio(REGISTRY, version=__version__)
|
|
1104
928
|
|
|
1105
929
|
shim = paths.launcher()
|
|
1106
930
|
if args.action == "register":
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Extensible MCP tool registry — the Phase-3 "MCP as a hub" seam.
|
|
2
|
+
|
|
3
|
+
An MCP tool is a *handler* — ``fn(args: dict) -> (text, is_error)`` — paired with
|
|
4
|
+
its JSON spec (``name`` / ``description`` / ``inputSchema``). Instead of a flat
|
|
5
|
+
``if/elif`` dispatcher, tools self-register on a :class:`Registry`; the JSON-RPC
|
|
6
|
+
server iterates it for both ``tools/list`` and ``tools/call``. Adding a
|
|
7
|
+
capability (a GitHub reach-out, a database query, some future dependency) is one
|
|
8
|
+
``register()`` call — no dispatcher edits, no server changes.
|
|
9
|
+
|
|
10
|
+
Handlers return ``(text, is_error)``. A handler that returns ``text is None``
|
|
11
|
+
signals "I did not produce a result"; :meth:`Registry.call` also returns
|
|
12
|
+
``(None, False)`` for an unknown tool, so the server can answer with a JSON-RPC
|
|
13
|
+
``unknown tool`` error in exactly one place. Reach-out handlers degrade the way
|
|
14
|
+
``core/ai.py`` does: probe for the dependency, and when it is absent return a
|
|
15
|
+
short helpful message rather than raising.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from ..errors import BoostError
|
|
24
|
+
|
|
25
|
+
# A handler maps parsed arguments to (text, is_error). ``text is None`` means the
|
|
26
|
+
# handler produced no result (treated by the server as an unknown/aborted tool).
|
|
27
|
+
Handler = Callable[[dict], Tuple[Optional[str], bool]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Registry:
|
|
31
|
+
"""An ordered name -> (spec, handler) map for MCP tools."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._order: List[str] = []
|
|
35
|
+
self._specs: Dict[str, dict] = {}
|
|
36
|
+
self._handlers: Dict[str, Handler] = {}
|
|
37
|
+
|
|
38
|
+
def register(self, name: str, description: str, input_schema: dict,
|
|
39
|
+
handler: Handler) -> None:
|
|
40
|
+
"""Register one tool. Raises on an empty or duplicate name."""
|
|
41
|
+
if not name:
|
|
42
|
+
raise ValueError("MCP tool name must be non-empty")
|
|
43
|
+
if name in self._handlers:
|
|
44
|
+
raise ValueError("duplicate MCP tool %r" % name)
|
|
45
|
+
self._order.append(name)
|
|
46
|
+
self._specs[name] = {"name": name, "description": description,
|
|
47
|
+
"inputSchema": input_schema}
|
|
48
|
+
self._handlers[name] = handler
|
|
49
|
+
|
|
50
|
+
def tool(self, name: str, description: str,
|
|
51
|
+
input_schema: dict) -> Callable[[Handler], Handler]:
|
|
52
|
+
"""Decorator form of :meth:`register`; returns the handler unchanged."""
|
|
53
|
+
def deco(fn: Handler) -> Handler:
|
|
54
|
+
self.register(name, description, input_schema, fn)
|
|
55
|
+
return fn
|
|
56
|
+
return deco
|
|
57
|
+
|
|
58
|
+
def specs(self) -> List[dict]:
|
|
59
|
+
"""The ``tools/list`` payload — specs in registration order."""
|
|
60
|
+
return [self._specs[name] for name in self._order]
|
|
61
|
+
|
|
62
|
+
def names(self) -> List[str]:
|
|
63
|
+
"""Registered tool names, in registration order."""
|
|
64
|
+
return list(self._order)
|
|
65
|
+
|
|
66
|
+
def has(self, name: str) -> bool:
|
|
67
|
+
return name in self._handlers
|
|
68
|
+
|
|
69
|
+
def call(self, name: str, args: dict) -> Tuple[Optional[str], bool]:
|
|
70
|
+
"""Dispatch to a handler. ``(None, False)`` for an unknown tool."""
|
|
71
|
+
handler = self._handlers.get(name)
|
|
72
|
+
if handler is None:
|
|
73
|
+
return None, False
|
|
74
|
+
return handler(args)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── JSON-RPC 2.0 protocol ─────────────────────────────────────────────────
|
|
78
|
+
# The wire protocol Claude Code speaks to `boost mcp --stdio`. handle_request is
|
|
79
|
+
# a *pure* request→response mapping (no I/O), so the whole protocol is unit
|
|
80
|
+
# testable; serve_stdio only adds the newline-delimited stdin/stdout loop.
|
|
81
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def handle_request(req: dict, *, version: str,
|
|
85
|
+
registry: "Registry") -> Optional[dict]:
|
|
86
|
+
"""Map one parsed JSON-RPC request to its response dict.
|
|
87
|
+
|
|
88
|
+
Returns ``None`` for a notification (a request with no ``id``) — the caller
|
|
89
|
+
sends nothing back. Handlers that raise during ``tools/call`` are turned into
|
|
90
|
+
an error *result* (``isError``) rather than a protocol error, so a failing
|
|
91
|
+
tool never kills the session.
|
|
92
|
+
"""
|
|
93
|
+
method = str(req.get("method", ""))
|
|
94
|
+
if "id" not in req: # notification (e.g. notifications/initialized)
|
|
95
|
+
return None
|
|
96
|
+
resp: dict = {"jsonrpc": "2.0", "id": req.get("id")}
|
|
97
|
+
if method == "initialize":
|
|
98
|
+
resp["result"] = {"protocolVersion": PROTOCOL_VERSION,
|
|
99
|
+
"capabilities": {"tools": {}},
|
|
100
|
+
"serverInfo": {"name": "boost", "version": version}}
|
|
101
|
+
elif method == "ping":
|
|
102
|
+
resp["result"] = {}
|
|
103
|
+
elif method == "tools/list":
|
|
104
|
+
resp["result"] = {"tools": registry.specs()}
|
|
105
|
+
elif method == "tools/call":
|
|
106
|
+
params = req.get("params") or {}
|
|
107
|
+
tool = str(params.get("name", ""))
|
|
108
|
+
try:
|
|
109
|
+
text, is_err = registry.call(tool, params.get("arguments") or {})
|
|
110
|
+
except BoostError as e:
|
|
111
|
+
text = "Error: %s" % e.message + ("\nhint: %s" % e.hint if e.hint else "")
|
|
112
|
+
is_err = True
|
|
113
|
+
except Exception as e: # noqa: BLE001 — server must not die
|
|
114
|
+
text, is_err = "Error: %s" % e, True
|
|
115
|
+
if text is None:
|
|
116
|
+
resp["error"] = {"code": -32602, "message": "unknown tool %r" % tool}
|
|
117
|
+
else:
|
|
118
|
+
resp["result"] = {"content": [{"type": "text", "text": text}]}
|
|
119
|
+
if is_err:
|
|
120
|
+
resp["result"]["isError"] = True
|
|
121
|
+
else:
|
|
122
|
+
resp["error"] = {"code": -32601, "message": "method not found: %s" % method}
|
|
123
|
+
return resp
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def serve_stdio(registry: "Registry", *, version: str,
|
|
127
|
+
stdin=None, stdout=None) -> int:
|
|
128
|
+
"""Newline-delimited JSON-RPC 2.0 MCP server on stdin/stdout.
|
|
129
|
+
|
|
130
|
+
``stdin``/``stdout`` default to the process streams but can be injected
|
|
131
|
+
(e.g. ``io.StringIO``) so the loop is testable end to end.
|
|
132
|
+
"""
|
|
133
|
+
stdin = stdin if stdin is not None else sys.stdin
|
|
134
|
+
stdout = stdout if stdout is not None else sys.stdout
|
|
135
|
+
|
|
136
|
+
def send(msg: dict) -> bool:
|
|
137
|
+
try:
|
|
138
|
+
stdout.write(json.dumps(msg) + "\n")
|
|
139
|
+
stdout.flush()
|
|
140
|
+
return True
|
|
141
|
+
except (BrokenPipeError, OSError):
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
while True:
|
|
145
|
+
try:
|
|
146
|
+
line = stdin.readline()
|
|
147
|
+
except (KeyboardInterrupt, OSError):
|
|
148
|
+
return 0
|
|
149
|
+
if not line: # EOF
|
|
150
|
+
return 0
|
|
151
|
+
line = line.strip()
|
|
152
|
+
if not line:
|
|
153
|
+
continue
|
|
154
|
+
try:
|
|
155
|
+
req = json.loads(line)
|
|
156
|
+
except json.JSONDecodeError:
|
|
157
|
+
if not send({"jsonrpc": "2.0", "id": None,
|
|
158
|
+
"error": {"code": -32700, "message": "parse error"}}):
|
|
159
|
+
return 0
|
|
160
|
+
continue
|
|
161
|
+
resp = handle_request(req, version=version, registry=registry)
|
|
162
|
+
if resp is None: # notification — nothing to send
|
|
163
|
+
continue
|
|
164
|
+
if not send(resp):
|
|
165
|
+
return 0
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""The `boost serve` HTTP catalog server — a small read-only view of the
|
|
2
|
+
installed skills plus JSON endpoints, extracted from the command layer.
|
|
3
|
+
|
|
4
|
+
The routing is a pure function (:func:`route`) that maps a request path to a
|
|
5
|
+
``(status, content_type, body)`` triple with no socket bound, so the whole
|
|
6
|
+
endpoint surface is unit-testable. :func:`serve_http` wires it into a threaded
|
|
7
|
+
stdlib HTTP server for the CLI.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import errno
|
|
12
|
+
import html
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import urllib.parse
|
|
16
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
17
|
+
from typing import Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from .. import __version__
|
|
20
|
+
from ..errors import BoostError
|
|
21
|
+
from . import catalog, lockfile, registry, store
|
|
22
|
+
from . import output as out
|
|
23
|
+
|
|
24
|
+
SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9._-]+$")
|
|
25
|
+
|
|
26
|
+
_PAGE_CSS = ("body{background:#111;color:#ddd;font-family:ui-monospace,SFMono-Regular,"
|
|
27
|
+
"Menlo,monospace;max-width:720px;margin:2rem auto;padding:0 1rem}"
|
|
28
|
+
"h1{color:#fff;font-size:1.3rem}a{color:#7dc4ff;text-decoration:none}"
|
|
29
|
+
"a:hover{text-decoration:underline}table{border-collapse:collapse;width:100%}"
|
|
30
|
+
"th,td{text-align:left;padding:.3rem .8rem .3rem 0;"
|
|
31
|
+
"border-bottom:1px solid #2a2a2a}.dim{color:#777}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def serve_page() -> str:
|
|
35
|
+
"""The HTML index: a table of installed skills with JSON-endpoint links."""
|
|
36
|
+
installed = lockfile.installed()
|
|
37
|
+
entries = catalog.all_entries()
|
|
38
|
+
taps = registry.list_taps()
|
|
39
|
+
rows = "".join(
|
|
40
|
+
'<tr><td><a href="/skill/%s">%s</a></td><td>%s</td><td>%s</td></tr>'
|
|
41
|
+
% (urllib.parse.quote(n), html.escape(n),
|
|
42
|
+
html.escape(str(e.get("version", "?"))),
|
|
43
|
+
html.escape(str(e.get("tap", "?"))))
|
|
44
|
+
for n, e in sorted(installed.items()))
|
|
45
|
+
return ("<!doctype html><html><head><meta charset='utf-8'>"
|
|
46
|
+
"<title>boost — skill catalog</title><style>%s</style></head><body>"
|
|
47
|
+
"<h1>⚡ boost <span class='dim'>v%s</span></h1>"
|
|
48
|
+
"<p class='dim'>%d installed · %d available across %d taps</p>"
|
|
49
|
+
"<table><tr><th>skill</th><th>version</th><th>tap</th></tr>%s</table>"
|
|
50
|
+
"<p><a href='/installed.json'>installed.json</a> · "
|
|
51
|
+
"<a href='/catalog.json'>catalog.json</a></p></body></html>"
|
|
52
|
+
% (_PAGE_CSS, __version__, len(installed), len(entries), len(taps),
|
|
53
|
+
rows or "<tr><td colspan='3' class='dim'>nothing installed</td></tr>"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def skill_text(name: str) -> Optional[str]:
|
|
57
|
+
"""SKILL.md text for an installed skill, else from a tap. None if unknown."""
|
|
58
|
+
if not SKILL_NAME_RE.fullmatch(name):
|
|
59
|
+
return None
|
|
60
|
+
fp = store.skill_store_dir(name) / "SKILL.md"
|
|
61
|
+
if fp.is_file():
|
|
62
|
+
return fp.read_text(encoding="utf-8", errors="replace")
|
|
63
|
+
for e in catalog.find(name):
|
|
64
|
+
try:
|
|
65
|
+
fp = registry.get(e["tap"]).path / e["skill_md"]
|
|
66
|
+
except BoostError:
|
|
67
|
+
continue
|
|
68
|
+
if fp.is_file():
|
|
69
|
+
return fp.read_text(encoding="utf-8", errors="replace")
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _json_body(obj) -> bytes:
|
|
74
|
+
return json.dumps(obj, indent=2).encode()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def route(path: str) -> Tuple[int, str, bytes]:
|
|
78
|
+
"""Map a GET path to ``(status, content_type, body)``. Pure — no socket.
|
|
79
|
+
|
|
80
|
+
Mirrors the historical dispatch exactly: ``/`` and ``/index.html`` serve the
|
|
81
|
+
HTML page; ``/catalog.json`` and ``/installed.json`` the JSON views; a
|
|
82
|
+
``/skill/<name>`` path the raw SKILL.md (404 for an invalid or unknown name);
|
|
83
|
+
anything else a JSON ``not found``.
|
|
84
|
+
"""
|
|
85
|
+
path = urllib.parse.unquote(path.split("?", 1)[0])
|
|
86
|
+
if path in ("/", "/index.html"):
|
|
87
|
+
return 200, "text/html; charset=utf-8", serve_page().encode()
|
|
88
|
+
if path == "/catalog.json":
|
|
89
|
+
return 200, "application/json", _json_body(catalog.all_entries())
|
|
90
|
+
if path == "/installed.json":
|
|
91
|
+
return 200, "application/json", _json_body(lockfile.read())
|
|
92
|
+
if path.startswith("/skill/"):
|
|
93
|
+
name = path[len("/skill/"):].strip("/")
|
|
94
|
+
if not SKILL_NAME_RE.fullmatch(name):
|
|
95
|
+
return (404, "application/json",
|
|
96
|
+
json.dumps({"error": "no skill named %r" % name}).encode())
|
|
97
|
+
text = skill_text(name)
|
|
98
|
+
if text is None:
|
|
99
|
+
return (404, "application/json",
|
|
100
|
+
json.dumps({"error": "no skill named %r" % name}).encode())
|
|
101
|
+
return 200, "text/plain; charset=utf-8", text.encode()
|
|
102
|
+
return 404, "application/json", json.dumps({"error": "not found"}).encode()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _CatalogHandler(BaseHTTPRequestHandler):
|
|
106
|
+
server_version = "boost/" + __version__
|
|
107
|
+
|
|
108
|
+
def log_message(self, fmt, *args): # logged in _send instead
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def _send(self, status: int, ctype: str, body: bytes) -> None:
|
|
112
|
+
self.send_response(status)
|
|
113
|
+
self.send_header("Content-Type", ctype)
|
|
114
|
+
self.send_header("Content-Length", str(len(body)))
|
|
115
|
+
self.end_headers()
|
|
116
|
+
self.wfile.write(body)
|
|
117
|
+
out.dim(" %s %s → %d" % (self.command, self.path, status))
|
|
118
|
+
|
|
119
|
+
def do_GET(self):
|
|
120
|
+
try:
|
|
121
|
+
status, ctype, body = route(self.path)
|
|
122
|
+
self._send(status, ctype, body)
|
|
123
|
+
except BrokenPipeError:
|
|
124
|
+
pass
|
|
125
|
+
except Exception as e: # noqa: BLE001 — keep the server alive
|
|
126
|
+
try:
|
|
127
|
+
self._send(500, "application/json",
|
|
128
|
+
json.dumps({"error": str(e)}).encode())
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def serve_http(host: str, port: int) -> None:
|
|
134
|
+
"""Bind and run the catalog server until interrupted (blocks)."""
|
|
135
|
+
try:
|
|
136
|
+
httpd = ThreadingHTTPServer((host, port), _CatalogHandler)
|
|
137
|
+
except OSError as e:
|
|
138
|
+
if e.errno == errno.EADDRINUSE:
|
|
139
|
+
raise BoostError("port %d is already in use" % port,
|
|
140
|
+
hint="pick another with --port")
|
|
141
|
+
raise BoostError("cannot bind %s:%d — %s" % (host, port, e),
|
|
142
|
+
hint="check --host and --port")
|
|
143
|
+
out.info("⚡ serving skill catalog on http://%s:%d %s"
|
|
144
|
+
% (host, port, out.c("(ctrl-c to stop)", out.DIM)))
|
|
145
|
+
try:
|
|
146
|
+
httpd.serve_forever()
|
|
147
|
+
except KeyboardInterrupt:
|
|
148
|
+
print()
|
|
149
|
+
out.ok("server stopped")
|
|
150
|
+
finally:
|
|
151
|
+
httpd.server_close()
|
|
@@ -48,6 +48,7 @@ boost_cli/core/paths.py
|
|
|
48
48
|
boost_cli/core/policy.py
|
|
49
49
|
boost_cli/core/rag.py
|
|
50
50
|
boost_cli/core/registry.py
|
|
51
|
+
boost_cli/core/serve.py
|
|
51
52
|
boost_cli/core/staleness.py
|
|
52
53
|
boost_cli/core/store.py
|
|
53
54
|
boost_cli/core/typosquat.py
|
|
@@ -229,6 +230,7 @@ tests/unit/test_rag.py
|
|
|
229
230
|
tests/unit/test_registries_fresh.py
|
|
230
231
|
tests/unit/test_registry.py
|
|
231
232
|
tests/unit/test_roadmap_fresh.py
|
|
233
|
+
tests/unit/test_serve.py
|
|
232
234
|
tests/unit/test_spin.py
|
|
233
235
|
tests/unit/test_staleness.py
|
|
234
236
|
tests/unit/test_store.py
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: extract-mcp-http-servers-out-of-configuration-py
|
|
3
|
+
board: code
|
|
4
|
+
section: shipped
|
|
5
|
+
status: shipped
|
|
6
|
+
category: Tech-debt
|
|
7
|
+
complexity: L
|
|
8
|
+
impact: High
|
|
9
|
+
wow: 4
|
|
10
|
+
note: two servers now testable in core
|
|
11
|
+
order: 5
|
|
12
|
+
owner: loop/extract-serve-mcp
|
|
13
|
+
pr: 106
|
|
14
|
+
title: Extract MCP + HTTP servers out of <code>configuration.py</code>
|
|
15
|
+
---
|
|
16
|
+
~200 lines implementing a full HTTP catalog server <b>and</b> a JSON-RPC 2.0 MCP
|
|
17
|
+
server lived inside the "configuration" command module — nearly untestable in
|
|
18
|
+
place. The JSON-RPC protocol now lives in <code>core/mcp.py</code> as a <em>pure</em>
|
|
19
|
+
<code>handle_request(req, *, version, registry)</code> plus a
|
|
20
|
+
<code>serve_stdio()</code> loop with injectable stdin/stdout; the HTTP server
|
|
21
|
+
moved to a new <code>core/serve.py</code> with a pure <code>route()</code>
|
|
22
|
+
function, <code>serve_page()</code>/<code>skill_text()</code>, and
|
|
23
|
+
<code>serve_http()</code>. <code>cmd_serve</code> and <code>cmd_mcp --stdio</code>
|
|
24
|
+
became thin wrappers. Behavior is preserved exactly (the full-server and
|
|
25
|
+
stdio-protocol functional tests pass unchanged) and the extracted core is pinned
|
|
26
|
+
by new unit tests covering every branch.
|