Prepare CmdForge 0.2.0 for PyPI
This commit is contained in:
parent
ca6135ac72
commit
1e698996b2
|
|
@ -4,6 +4,14 @@ All notable changes to CmdForge will be documented in this file.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2026-07-21
|
||||
|
||||
First public Python package release. This release brings the current CmdForge
|
||||
application to PyPI, including provider routing and provenance, MCP client and
|
||||
server support, provider-attached skills, delegated tools, contracts and
|
||||
preflight analysis, regression evidence, quality scoring, registry integrity,
|
||||
project-owned tools, agent discovery commands, and local usage suggestions.
|
||||
|
||||
### Added
|
||||
|
||||
#### System Dependencies
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Rob
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# CmdForge
|
||||
|
||||
**Turn useful AI workflows into commands you own.**
|
||||
|
||||
CmdForge is a personal tool-building environment for the command line. A tool
|
||||
can combine prompts, Python, other CmdForge tools, and Model Context Protocol
|
||||
(MCP) calls, then behave like an ordinary Unix command:
|
||||
|
||||
```bash
|
||||
cat meeting.txt | meeting-decisions
|
||||
git diff --staged | review-change
|
||||
cmdforge run classify-private --no-fallback --require-local
|
||||
```
|
||||
|
||||
Tools are readable YAML, stored either in `~/.cmdforge/` for personal use or
|
||||
`./.cmdforge/` when they belong to a project. You choose the provider at build
|
||||
time or runtime: a local Ollama model, an installed coding CLI, or an
|
||||
OpenAI-compatible API.
|
||||
|
||||
## Install
|
||||
|
||||
CmdForge requires Python 3.10 or newer. `pipx` is recommended for a personal
|
||||
command-line application:
|
||||
|
||||
```bash
|
||||
pipx install "cmdforge[mcp,pty]"
|
||||
```
|
||||
|
||||
Or install it in a virtual environment:
|
||||
|
||||
```bash
|
||||
python -m pip install "cmdforge[mcp,pty]"
|
||||
```
|
||||
|
||||
The base `cmdforge` package includes the CLI and desktop tool builder. The
|
||||
`mcp` extra adds MCP client/server support, while `pty` adds interactive CLI
|
||||
providers.
|
||||
|
||||
Verify the installation and discover providers already present on the machine:
|
||||
|
||||
```bash
|
||||
cmdforge --version
|
||||
cmdforge providers discover
|
||||
cmdforge providers discover --add
|
||||
```
|
||||
|
||||
## Begin with One Useful Command
|
||||
|
||||
```bash
|
||||
cmdforge create explain --prompt "Explain this clearly for a beginner: {input}"
|
||||
echo "def area(r): return 3.14159 * r * r" | cmdforge run explain
|
||||
```
|
||||
|
||||
Use the deterministic mock provider while developing:
|
||||
|
||||
```bash
|
||||
echo "sample input" | cmdforge run explain --provider mock
|
||||
cmdforge inspect explain
|
||||
```
|
||||
|
||||
For AI-assisted creation, install CmdForge's official `forge-tool`:
|
||||
|
||||
```bash
|
||||
cmdforge registry install official/forge-tool
|
||||
|
||||
echo "Create a tool that extracts decisions and owners from meeting notes" \
|
||||
| forge-tool --name meeting-decisions --project
|
||||
```
|
||||
|
||||
## Four Kinds of Step
|
||||
|
||||
- **Prompt steps** call a selected AI provider.
|
||||
- **Code steps** perform deterministic Python transformations.
|
||||
- **Tool steps** compose existing CmdForge tools and delegated contexts.
|
||||
- **MCP steps** call tools exposed by external MCP servers.
|
||||
|
||||
Contracts, deterministic preflight, regression baselines, schema compatibility,
|
||||
and evidence-based quality scores help distinguish “worked once” from a tool
|
||||
you can maintain.
|
||||
|
||||
## Use CmdForge from Coding Agents
|
||||
|
||||
CmdForge can act as an MCP server for Codex and Claude Code. It is closed by
|
||||
default: configuring a host does not expose any tools until you add an explicit
|
||||
allowlist to `~/.cmdforge/mcp.yaml`.
|
||||
|
||||
```bash
|
||||
cmdforge mcp configure codex --dry-run
|
||||
cmdforge mcp configure codex
|
||||
|
||||
cmdforge mcp configure claude-code --scope project --dry-run
|
||||
```
|
||||
|
||||
Agents can also discover the local and remote catalogs without reading prompt
|
||||
or code bodies:
|
||||
|
||||
```bash
|
||||
cmdforge list --json --filter "commit message" --limit 10
|
||||
cmdforge registry search "release notes" --json --limit 5
|
||||
git diff | cmdforge run-once "Summarize this change: {input}"
|
||||
```
|
||||
|
||||
## Privacy and Provenance
|
||||
|
||||
Fallback is a data-movement decision. Sensitive callers can require a local
|
||||
provider, particular capabilities, and verified runtime identity:
|
||||
|
||||
```bash
|
||||
cmdforge run incident-summary \
|
||||
--provider local-reasoner \
|
||||
--no-fallback \
|
||||
--require-local \
|
||||
--require-capability structured-json \
|
||||
--data-classification private \
|
||||
--require-model-identity \
|
||||
--result-envelope json
|
||||
```
|
||||
|
||||
The result envelope reports the requested provider, actual provider, attempted
|
||||
fallback chain, model, digest, and locality. Those fields are produced by
|
||||
CmdForge rather than invented by the model.
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Documentation](https://cmdforge.brrd.tech/docs)
|
||||
- [MCP and coding agents](https://cmdforge.brrd.tech/docs/mcp-overview)
|
||||
- [Providers and privacy policy](https://cmdforge.brrd.tech/docs/providers)
|
||||
- [Contracts and quality evidence](https://cmdforge.brrd.tech/docs/contracts-quality)
|
||||
- [Registry](https://cmdforge.brrd.tech/tools)
|
||||
- [Source and issues](https://gitea.brrd.tech/rob/CmdForge)
|
||||
|
||||
CmdForge is beta software, distributed under the MIT License. Review generated
|
||||
tools and third-party registry tools as you would any other executable code.
|
||||
30
README.md
30
README.md
|
|
@ -55,16 +55,14 @@ That's it - you just used AI to explain itself. The `eli5` tool uses a free mode
|
|||
For regular use, install natively:
|
||||
|
||||
```bash
|
||||
# Clone and install
|
||||
git clone https://gitea.brrd.tech/rob/CmdForge.git
|
||||
cd CmdForge
|
||||
pip install -e .
|
||||
# Install the application with MCP and interactive-provider support
|
||||
pipx install "cmdforge[mcp,pty]"
|
||||
|
||||
# Ensure ~/.local/bin is in PATH
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Install an AI provider (interactive guide)
|
||||
cmdforge providers install
|
||||
# Discover AI CLIs, API keys, and Ollama models already on this machine
|
||||
cmdforge providers discover --add
|
||||
|
||||
# Launch the GUI
|
||||
cmdforge
|
||||
|
|
@ -75,12 +73,24 @@ cmdforge create summarize
|
|||
|
||||
## Installation
|
||||
|
||||
### Native Install
|
||||
### From PyPI
|
||||
|
||||
```bash
|
||||
pipx install "cmdforge[mcp,pty]"
|
||||
```
|
||||
|
||||
Or install into an active virtual environment:
|
||||
|
||||
```bash
|
||||
python -m pip install "cmdforge[mcp,pty]"
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://gitea.brrd.tech/rob/CmdForge.git
|
||||
cd CmdForge
|
||||
pip install -e .
|
||||
python -m pip install -e ".[mcp,pty]"
|
||||
```
|
||||
|
||||
### With Development Dependencies
|
||||
|
|
@ -94,7 +104,7 @@ pip install -e ".[dev]"
|
|||
### Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- At least one AI CLI tool installed (see [Provider Setup](docs/reference/providers.md))
|
||||
- A configured provider for live AI calls; the built-in mock works without one
|
||||
- PySide6 (included automatically - requires display server on Linux)
|
||||
|
||||
### Post-Install
|
||||
|
|
@ -195,7 +205,7 @@ cmdforge usage enable # Begin recording tool names in shell pipe
|
|||
cmdforge usage suggestions # Show frequent pipelines
|
||||
cmdforge usage clear # Delete all local usage history
|
||||
|
||||
# Model Context Protocol (optional: pip install -e ".[mcp]")
|
||||
# Model Context Protocol (optional: python -m pip install "cmdforge[mcp]")
|
||||
cmdforge mcp add local --command npx --arg=-y --arg @scope/server
|
||||
cmdforge mcp add remote --transport streamable-http --url https://example.com/mcp
|
||||
cmdforge mcp connect remote
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
# Publishing CmdForge to PyPI
|
||||
|
||||
PyPI releases are public and immutable. A release version must never be reused
|
||||
for different bytes, even if an upload contains a mistake.
|
||||
|
||||
## One-time account setup
|
||||
|
||||
1. Create or sign in to an account at <https://pypi.org/>.
|
||||
2. Verify the account email address.
|
||||
3. Enable two-factor authentication and store the recovery codes in the
|
||||
password manager and an offline recovery location.
|
||||
4. For the first upload only, create an account-scoped API token. PyPI cannot
|
||||
create a project-scoped token until the project exists.
|
||||
5. Do not put a PyPI token in Git, `.pypirc`, chat, command arguments, or shell
|
||||
history. Let Twine prompt for it interactively.
|
||||
|
||||
After the first successful upload, immediately revoke the account-scoped token
|
||||
and create a new token restricted to the `cmdforge` project.
|
||||
|
||||
## Prepare and validate a release
|
||||
|
||||
Update the version in both `pyproject.toml` and `src/cmdforge/__init__.py`, then
|
||||
add the release to `CHANGELOG.md`. From a clean checkout:
|
||||
|
||||
```bash
|
||||
python -m pip install -e '.[release]'
|
||||
pytest tests/ -m "not integration"
|
||||
python -m build
|
||||
python -m twine check dist/*
|
||||
```
|
||||
|
||||
Inspect the wheel and source archive, then install the wheel into a clean
|
||||
temporary virtual environment and exercise both entry points. Do not upload an
|
||||
artifact that was built before the release commit.
|
||||
|
||||
## Upload
|
||||
|
||||
Run Twine interactively so the token is not recorded in shell history:
|
||||
|
||||
```bash
|
||||
python -m twine upload dist/*
|
||||
```
|
||||
|
||||
When prompted, use `__token__` as the username and paste the API token as the
|
||||
password. Once uploaded, verify the public project and install from PyPI in a
|
||||
new environment:
|
||||
|
||||
```bash
|
||||
python -m venv /tmp/cmdforge-pypi-check
|
||||
/tmp/cmdforge-pypi-check/bin/pip install 'cmdforge[mcp,pty]'
|
||||
/tmp/cmdforge-pypi-check/bin/cmdforge --version
|
||||
/tmp/cmdforge-pypi-check/bin/cmdforge --help
|
||||
```
|
||||
|
||||
Tag and push only the commit whose artifacts were published:
|
||||
|
||||
```bash
|
||||
git tag -a v0.2.0 -m "CmdForge 0.2.0"
|
||||
git push origin main
|
||||
git push origin v0.2.0
|
||||
```
|
||||
|
||||
If an upload is wrong, fix it, increment the version, rebuild, and publish a
|
||||
new release. Never delete and reuse the version number.
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
requires = ["setuptools>=77.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "cmdforge"
|
||||
version = "0.1.0"
|
||||
description = "Build custom AI-powered CLI commands in YAML"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
version = "0.2.0"
|
||||
description = "Build, compose, and share AI-powered command-line tools you own"
|
||||
readme = "PYPI_README.md"
|
||||
license = "MIT"
|
||||
license-files = ["LICENSE"]
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{name = "Rob"}
|
||||
|
|
@ -18,7 +19,6 @@ classifiers = [
|
|||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS",
|
||||
"Programming Language :: Python :: 3",
|
||||
|
|
@ -45,6 +45,10 @@ dev = [
|
|||
"pytest-cov>=4.0",
|
||||
"tomli>=1.1; python_version < '3.11'",
|
||||
]
|
||||
release = [
|
||||
"build>=1.2",
|
||||
"twine>=6.0",
|
||||
]
|
||||
registry = [
|
||||
"Flask>=2.3",
|
||||
"argon2-cffi>=21.0",
|
||||
|
|
@ -81,6 +85,7 @@ Homepage = "https://cmdforge.brrd.tech"
|
|||
Documentation = "https://cmdforge.brrd.tech/docs"
|
||||
Repository = "https://gitea.brrd.tech/rob/CmdForge.git"
|
||||
Issues = "https://gitea.brrd.tech/rob/CmdForge/issues"
|
||||
Changelog = "https://gitea.brrd.tech/rob/CmdForge/src/branch/main/CHANGELOG.md"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"""CmdForge - A lightweight personal tool builder for AI-powered CLI commands."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.2.0"
|
||||
|
|
|
|||
|
|
@ -451,9 +451,23 @@ def pick_args(tty_input: TTYInput, tool: dict) -> Optional[dict]:
|
|||
def main():
|
||||
"""Entry point for cf command."""
|
||||
global _ui_out
|
||||
import argparse
|
||||
import subprocess
|
||||
import signal
|
||||
|
||||
# Parse metadata flags before touching stdin or /dev/tty. This keeps
|
||||
# ``cf --help`` and ``cf --version`` usable in packaging checks, CI, and
|
||||
# other non-interactive environments.
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="cf",
|
||||
description="Interactively find and run local or registry CmdForge tools",
|
||||
)
|
||||
from .. import __version__
|
||||
parser.add_argument(
|
||||
"--version", action="version", version=f"%(prog)s {__version__}"
|
||||
)
|
||||
parser.parse_args()
|
||||
|
||||
# Handle Ctrl+C gracefully
|
||||
def handle_sigint(sig, frame):
|
||||
# Restore cursor and exit cleanly
|
||||
|
|
|
|||
|
|
@ -17,20 +17,21 @@ install together; optional extras add MCP, interactive PTY providers, and the re
|
|||
<h2 id="pipx">A Clean Personal Installation</h2>
|
||||
<p><code>pipx</code> keeps CmdForge isolated while placing <code>cmdforge</code> and <code>cf</code> on
|
||||
your path:</p>
|
||||
<pre><code class="language-bash">pipx install 'cmdforge[all]'
|
||||
<pre><code class="language-bash">pipx install 'cmdforge[mcp,pty]'
|
||||
cmdforge --version
|
||||
cmdforge --help</code></pre>
|
||||
<p>A normal virtual environment works just as well:</p>
|
||||
<pre><code class="language-bash">python3.10 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
python -m pip install 'cmdforge[all]'</code></pre>
|
||||
python -m pip install 'cmdforge[mcp,pty]'</code></pre>
|
||||
|
||||
<h2 id="extras">Choose Only the Extras You Need</h2>
|
||||
<table class="w-full my-5"><thead class="bg-gray-100"><tr><th>Install</th><th>Adds</th></tr></thead><tbody>
|
||||
<tr class="border-b"><td><code>cmdforge</code></td><td>CLI, desktop GUI, contracts, registry client, and local tool runner</td></tr>
|
||||
<tr class="border-b"><td><code>cmdforge[mcp]</code></td><td>MCP client steps and MCP server integration</td></tr>
|
||||
<tr class="border-b"><td><code>cmdforge[pty]</code></td><td>Interactive pseudo-terminal providers</td></tr>
|
||||
<tr><td><code>cmdforge[all]</code></td><td>All optional runtime features, including the web stack</td></tr>
|
||||
<tr><td><code>cmdforge[mcp,pty]</code></td><td>The recommended personal installation with agent integration</td></tr>
|
||||
<tr><td><code>cmdforge[all]</code></td><td>Every optional feature, including the registry web-server stack</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
<h2 id="provider">Let CmdForge Inspect the Machine</h2>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Tests for packaging metadata."""
|
||||
"""Release metadata checks for the public Python distribution."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
|
@ -9,12 +10,53 @@ if sys.version_info >= (3, 11):
|
|||
else:
|
||||
tomllib = pytest.importorskip("tomli")
|
||||
|
||||
from cmdforge import __version__
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _project_metadata() -> dict:
|
||||
with (ROOT / "pyproject.toml").open("rb") as handle:
|
||||
return tomllib.load(handle)["project"]
|
||||
|
||||
|
||||
def test_runtime_and_distribution_versions_match():
|
||||
assert _project_metadata()["version"] == __version__
|
||||
|
||||
|
||||
def test_public_distribution_metadata_is_complete():
|
||||
project = _project_metadata()
|
||||
|
||||
assert project["name"] == "cmdforge"
|
||||
assert project["readme"] == "PYPI_README.md"
|
||||
assert project["license"] == "MIT"
|
||||
assert project["license-files"] == ["LICENSE"]
|
||||
assert project["requires-python"] == ">=3.10"
|
||||
assert {"Homepage", "Documentation", "Repository", "Issues", "Changelog"} <= set(
|
||||
project["urls"]
|
||||
)
|
||||
|
||||
|
||||
def test_public_readme_describes_installation_and_current_capabilities():
|
||||
readme = (ROOT / "PYPI_README.md").read_text(encoding="utf-8")
|
||||
|
||||
for term in (
|
||||
'pipx install "cmdforge[mcp,pty]"',
|
||||
"cmdforge providers discover",
|
||||
"cmdforge mcp configure codex",
|
||||
"--no-fallback",
|
||||
"--result-envelope json",
|
||||
):
|
||||
assert term in readme
|
||||
|
||||
assert (ROOT / "LICENSE").is_file()
|
||||
|
||||
|
||||
def test_web_templates_and_static_are_declared_as_package_data():
|
||||
with open("pyproject.toml", "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
with (ROOT / "pyproject.toml").open("rb") as handle:
|
||||
data = tomllib.load(handle)
|
||||
|
||||
package_data = data["tool"]["setuptools"]["package-data"]["cmdforge.web"]
|
||||
|
||||
assert "templates/**/*.html" in package_data
|
||||
assert "static/**/*" in package_data
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
"""Behavioral coverage for registry-aware picker and deprecation UX."""
|
||||
|
||||
from io import StringIO
|
||||
import sys
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import cmdforge.cli.picker as picker
|
||||
import pytest
|
||||
from cmdforge import __version__
|
||||
from cmdforge.cli.picker import PickerResult
|
||||
|
||||
|
||||
|
|
@ -89,3 +92,19 @@ def test_local_deprecation_selection_prints_migration_guidance():
|
|||
output = picker._ui_out.getvalue()
|
||||
assert "Moved." in output
|
||||
assert "official/new" in output
|
||||
|
||||
|
||||
def test_picker_help_does_not_require_a_terminal(capsys):
|
||||
with patch.object(sys, "argv", ["cf", "--help"]), pytest.raises(SystemExit) as exc:
|
||||
picker.main()
|
||||
|
||||
assert exc.value.code == 0
|
||||
assert "Interactively find and run" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_picker_version_does_not_require_a_terminal(capsys):
|
||||
with patch.object(sys, "argv", ["cf", "--version"]), pytest.raises(SystemExit) as exc:
|
||||
picker.main()
|
||||
|
||||
assert exc.value.code == 0
|
||||
assert f"cf {__version__}" in capsys.readouterr().out
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ def test_installation_matches_runtime_and_current_provider_onboarding():
|
|||
|
||||
assert "Python 3.10" in installation
|
||||
assert "Python 3.8" not in installation
|
||||
assert "cmdforge[mcp]" in installation
|
||||
assert "cmdforge[mcp,pty]" in installation
|
||||
assert "cmdforge providers discover --add" in installation
|
||||
assert "cmdforge providers discover --add" in provider_setup
|
||||
assert "--type api" in provider_setup
|
||||
|
|
|
|||
Loading…
Reference in New Issue