Add registry endpoint and CLI command for updating tool READMEs

- PATCH /api/v1/tools/<owner>/<name>/readme updates README without
  affecting config hash or requiring version bump
- Add RegistryClient.update_readme() method
- Add `cmdforge registry update-readme` CLI command with --all flag
  for batch updating all published tools

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-02-01 01:35:10 -04:00
parent f2cb9c0057
commit e83c9c15f3
4 changed files with 148 additions and 0 deletions

View File

@ -190,6 +190,12 @@ def main():
p_reg_publish.add_argument("--owner", default="", help="Owner override (admin only, e.g. 'official')")
p_reg_publish.set_defaults(func=cmd_registry)
# registry update-readme
p_reg_update_readme = registry_sub.add_parser("update-readme", help="Update README for a published tool")
p_reg_update_readme.add_argument("tool", help="Tool name (local name, will resolve owner)")
p_reg_update_readme.add_argument("--all", action="store_true", dest="update_all", help="Update README for all published tools that have a local README.md")
p_reg_update_readme.set_defaults(func=cmd_registry)
# registry my-tools
p_reg_mytools = registry_sub.add_parser("my-tools", help="List your published tools")
p_reg_mytools.set_defaults(func=cmd_registry)

View File

@ -29,6 +29,8 @@ def cmd_registry(args):
return _cmd_registry_update(args)
elif args.registry_cmd == "publish":
return _cmd_registry_publish(args)
elif args.registry_cmd == "update-readme":
return _cmd_registry_update_readme(args)
elif args.registry_cmd == "my-tools":
return _cmd_registry_my_tools(args)
elif args.registry_cmd == "status":
@ -47,6 +49,7 @@ def cmd_registry(args):
print(" info <tool> Show tool information")
print(" update Update local index cache")
print(" publish [path] Publish a tool")
print(" update-readme Update README for published tool(s)")
print(" my-tools List your published tools")
print(" status <tool> Check moderation status of a tool")
print(" browse Browse tools (GUI)")
@ -635,6 +638,86 @@ def _cmd_registry_publish(args):
return 0
def _cmd_registry_update_readme(args):
"""Update README for published tool(s) on the registry."""
from ..registry_client import RegistryError, get_client
from ..tool import TOOLS_DIR, list_tools
try:
client = get_client()
except RegistryError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if getattr(args, "update_all", False):
# Update all published tools that have a local README
tools = list_tools()
updated = 0
skipped = 0
failed = 0
for tool_name in tools:
tool_dir = TOOLS_DIR / tool_name
config_path = tool_dir / "config.yaml"
readme_path = tool_dir / "README.md"
if not readme_path.exists():
continue
# Check if published
try:
config = yaml.safe_load(config_path.read_text()) or {}
except Exception:
continue
owner = config.get("registry_owner", "")
if not owner:
skipped += 1
continue
readme_content = readme_path.read_text()
try:
result = client.update_readme(owner, tool_name, readme_content)
count = result.get("versions_updated", 0)
print(f" {owner}/{tool_name}: updated ({count} version(s))")
updated += 1
except RegistryError as e:
print(f" {owner}/{tool_name}: FAILED - {e}", file=sys.stderr)
failed += 1
print(f"\nDone: {updated} updated, {failed} failed, {skipped} skipped (not published)")
return 0
# Single tool mode
tool_name = args.tool
tool_dir = TOOLS_DIR / tool_name
config_path = tool_dir / "config.yaml"
readme_path = tool_dir / "README.md"
if not config_path.exists():
print(f"Error: Tool '{tool_name}' not found", file=sys.stderr)
return 1
if not readme_path.exists():
print(f"Error: No README.md found for '{tool_name}'", file=sys.stderr)
return 1
config = yaml.safe_load(config_path.read_text()) or {}
owner = config.get("registry_owner", "")
if not owner:
print(f"Error: Tool '{tool_name}' has no registry_owner — not published?", file=sys.stderr)
return 1
readme_content = readme_path.read_text()
try:
result = client.update_readme(owner, tool_name, readme_content)
count = result.get("versions_updated", 0)
print(f"Updated README for {owner}/{tool_name} ({count} version(s))")
except RegistryError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
return 0
def _cmd_registry_my_tools(args):
"""List your published tools."""
from ..registry_client import RegistryError, get_client

View File

@ -2815,6 +2815,36 @@ def create_app() -> Flask:
return jsonify({"data": {"status": "active", "owner": owner, "name": name}})
@app.route("/api/v1/tools/<owner>/<name>/readme", methods=["PATCH"])
@require_token
def update_tool_readme(owner: str, name: str) -> Response:
"""Update the README for an existing tool without affecting config or hash."""
if g.current_publisher["slug"] != owner:
return error_response("FORBIDDEN", "You can only update your own tools", 403)
data = request.get_json() or {}
readme = data.get("readme")
if readme is None:
return error_response("VALIDATION_ERROR", "Missing 'readme' field", 400)
version = data.get("version")
if version:
result = g.db.execute(
"UPDATE tools SET readme = ? WHERE owner = ? AND name = ? AND version = ?",
[readme, owner, name, version],
)
else:
# Update all versions
result = g.db.execute(
"UPDATE tools SET readme = ? WHERE owner = ? AND name = ?",
[readme, owner, name],
)
if result.rowcount == 0:
return error_response("TOOL_NOT_FOUND", f"Tool {owner}/{name} not found", 404)
g.db.commit()
return jsonify({"data": {"status": "updated", "owner": owner, "name": name, "versions_updated": result.rowcount}})
@app.route("/api/v1/me/settings", methods=["PUT"])
@require_token
def update_settings() -> Response:

View File

@ -666,6 +666,35 @@ class RegistryClient:
return response.json().get("data", {})
def update_readme(self, owner: str, name: str, readme: str, version: str = "") -> Dict[str, Any]:
"""
Update the README for an existing published tool.
Args:
owner: Tool owner slug
name: Tool name
readme: New README content
version: Optional specific version to update (default: all versions)
Returns:
Dict with update status
"""
payload: Dict[str, Any] = {"readme": readme}
if version:
payload["version"] = version
response = self._request(
"PATCH",
f"/tools/{owner}/{name}/readme",
json_data=payload,
require_auth=True,
)
if response.status_code not in (200, 201):
self._handle_error_response(response)
return response.json().get("data", {})
def validate_token(self) -> tuple[bool, str]:
"""
Validate that the current token is valid.