mirror of
https://github.com/mims-harvard/ToolUniverse.git
synced 2026-09-19 07:31:47 +08:00
e9bd73205c
* Add Codex plugin packaging * Wire Codex plugin into skill-sync + release pipeline; bring to parity The Codex plugin packaging lived outside the Claude plugin's release flow, so its bundled skills and version drifted (134 skills @ 1.2.4 vs Claude's 136 @ 1.2.18). This puts both packagings on one pipeline: - plugin/sync-skills.sh now rebuilds BOTH plugin/skills/ (Claude) and plugins/tooluniverse/skills/ (Codex) from the canonical skills/ tree; Codex drops Claude-Code-specific skills (tooluniverse-claude-code-plugin). - scripts/release-plugin.sh now also bumps plugins/tooluniverse/.codex-plugin/ plugin.json on release, so Codex's version tracks Claude's. One-time parity: - Codex skills resynced (135 = 136 user-facing minus the Claude-only skill); adds tooluniverse-peptide-target-deorphanization, drops stale entries. - Codex plugin.json 1.2.4 -> 1.2.18 to match the current release. - Restored skills/tooluniverse-custom-tool/references/json-tool.md: PR #194 edited only the Claude plugin copy, leaving the source missing the API-key declaration docs, which any resync would otherwise drop. Codex auto-upgrades owner/repo Git marketplaces on startup, so this reaches Codex users on next launch; Claude users get it via the version bump. * Fix Codex skill sync: delegate to the Codex-specific normalizer The prior pipeline commit rebuilt plugins/tooluniverse/skills/ with the same filter as the Claude copy, which left in the disable-model-invocation marker (rejected by Codex validation; 133 skills) and descriptions over Codex's 1024-char limit (4 skills). Have plugin/sync-skills.sh delegate the Codex copy to scripts/sync-codex-plugin-skills.sh — which strips the marker and compacts long descriptions — instead of duplicating and diverging from that logic. Also add an evals/ exclude to the Codex sync so dev eval scaffolding does not ship, matching the Claude filter. * Run the Codex plugin packaging test in CI tests/test_codex_plugin.py lived at the tests/ top level, outside pytest.ini's testpaths (tests/unit, tests/integration, tests/tools), so CI never collected it — its per-skill assertions (description <= 1024 chars, disable-model-invocation not set) silently never ran, which is why the earlier broken Codex skill regen passed CI. Move it under tests/unit/, fix the repo-root parents[] depth, and mark it unit so the Codex skill set is validated on every run. * Rename Codex marketplace from tooluniverse-local to tooluniverse The catalog name/displayName carried a 'local' suffix left over from local-dir testing, but this is the published GitHub marketplace users add via `codex plugin marketplace add mims-harvard/ToolUniverse`. Drop the misleading 'local' so it lists as 'tooluniverse' / 'ToolUniverse'. * Update Codex plugin README: add install steps, point sync at the shared script - Add an Install section (codex plugin marketplace add mims-harvard/ToolUniverse + codex plugin add tooluniverse) — the README had no end-user install steps. - Point the Development sync at plugin/sync-skills.sh, the unified entry point that rebuilds BOTH the Claude and Codex skill copies; the old instruction ran only the Codex-specific sync, so following it left the Claude copy stale. * Version plugins as <package MAJOR.MINOR>.<plugin revision> Tie both plugin versions to the PyPI package's MAJOR.MINOR while letting them carry an independent PATCH (the plugin revision). release-plugin.sh now derives the next version from pyproject.toml's MAJOR.MINOR and bumps the plugin PATCH (resetting to .0 when the package's MAJOR.MINOR advances), instead of bumping the plugin's own semver freely. This keeps Claude + Codex plugin versions coherent with the package they serve while a skill-only change ships as a PATCH bump — without forcing an empty PyPI package release. * Update RELEASING.md for the package-tracked plugin version scheme Document the <package MAJOR.MINOR>.<plugin revision> scheme, the 4 bumped plugin manifests (Codex included), and that both Claude (version-driven) and Codex (git-driven) plugins auto-update — replacing the stale level-based / 3-manifest / Claude-only instructions. * Add tooluniverse-codex-plugin install skill (Codex-only) A one-step install + troubleshooting guide for the ToolUniverse plugin on OpenAI Codex, mirroring tooluniverse-claude-code-plugin but adapted to Codex's commands (codex plugin marketplace add / plugin add -m), startup auto-upgrade behavior, and MCP-via-uvx model — and noting Codex ships MCP tools + skills only (no slash commands). Symmetric to the Claude install skill: plugin/sync-skills.sh now excludes it from the Claude packaging, while the Codex sync includes it (and drops the Claude-only tooluniverse-claude-code-plugin). Description tuned via the skill-creator trigger optimizer (perfect precision; install-skill recall is inherently low and largely platform-bound).
225 lines
8.0 KiB
Bash
Executable File
225 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Release a new version of the ToolUniverse Claude Code AND Codex plugins.
|
|
#
|
|
# Versioning scheme:
|
|
# Both plugins share the PyPI `tooluniverse` package's MAJOR.MINOR and carry
|
|
# their own PATCH (the plugin revision). This keeps the plugin versions
|
|
# coherent with the package they serve over MCP, while letting plugin-only
|
|
# changes (e.g. skill edits) ship WITHOUT an empty package release:
|
|
# - plugin-only change -> PATCH bump (pkg 1.2.x -> plugin 1.2.18 → 1.2.19)
|
|
# - package MAJOR.MINOR moves-> reset to .0 (pkg 1.3.0 -> plugin 1.3.0)
|
|
# The package keeps its own independent PyPI release line (pyproject + vX.Y.Z
|
|
# tags); a plugin release never bumps the package.
|
|
#
|
|
# What this does:
|
|
# 1. Bump the version in all 4 plugin manifests:
|
|
# plugin/.claude-plugin/plugin.json, plugin/.claude-plugin/marketplace.json,
|
|
# .claude-plugin/marketplace.json, plugins/tooluniverse/.codex-plugin/plugin.json
|
|
# (absent manifests in the current layout are skipped)
|
|
# 2. Commit the bump
|
|
# 3. Tag the commit (vX.Y.Z)
|
|
# 4. Push commit + tag to origin
|
|
# 5. The release-plugin.yml workflow then builds the zip + creates a GitHub Release
|
|
#
|
|
# Users receive the new version automatically: Claude Code on the version bump
|
|
# (next startup), Codex on the git push (startup marketplace auto-upgrade).
|
|
#
|
|
# Usage:
|
|
# bash scripts/release-plugin.sh auto # next plugin revision (tracks the package)
|
|
# bash scripts/release-plugin.sh patch # same as auto (level is advisory only)
|
|
# bash scripts/release-plugin.sh 1.3.0 # explicit version (manual override)
|
|
# bash scripts/release-plugin.sh auto --dry-run # show planned changes only
|
|
# bash scripts/release-plugin.sh auto --no-push # bump+commit+tag, don't push
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 <auto|patch|minor|major|X.Y.Z> [--dry-run] [--no-push]
|
|
|
|
The plugin version tracks the PyPI package's MAJOR.MINOR and bumps its own PATCH.
|
|
auto/patch/minor/major all do the same thing (next plugin revision); pass an
|
|
explicit X.Y.Z only to override manually.
|
|
|
|
Examples:
|
|
$0 auto # next plugin revision (tracks the package MAJOR.MINOR)
|
|
$0 auto --dry-run # preview the bump without changing anything
|
|
$0 1.3.0 --no-push # explicit version; bump, commit, tag locally for review
|
|
|
|
Without --no-push, this script pushes commit + tag to origin and triggers the
|
|
release workflow.
|
|
|
|
Full reference (all 3 release paths, what gets bumped, safety mechanisms):
|
|
scripts/RELEASING.md
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
LEVEL="${1:-}"
|
|
case "$LEVEL" in
|
|
-h|--help|help)
|
|
usage ;;
|
|
"")
|
|
usage ;;
|
|
esac
|
|
|
|
DRY_RUN=0
|
|
NO_PUSH=0
|
|
shift
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--no-push) NO_PUSH=1 ;;
|
|
*) echo "Unknown flag: $arg"; usage ;;
|
|
esac
|
|
done
|
|
|
|
# Baseline = the highest of the manifest version and any existing vX.Y.Z git tag.
|
|
# The plugin manifest can drift BEHIND the released tag line (e.g. PyPI-driven
|
|
# releases bump pyproject/tags but not plugin.json). Taking the max keeps the
|
|
# plugin PATCH (revision) monotonic and avoids colliding with an existing tag.
|
|
CURRENT=$(python3 -c "
|
|
import json, re, subprocess
|
|
def parse(v):
|
|
m = re.match(r'^(\d+)\.(\d+)\.(\d+)$', v)
|
|
return tuple(int(x) for x in m.groups()) if m else (0, 0, 0)
|
|
versions = [json.load(open('plugin/.claude-plugin/plugin.json'))['version']]
|
|
tags = subprocess.run(['git', 'tag', '--list', 'v*'],
|
|
capture_output=True, text=True).stdout.split()
|
|
versions += [t[1:] for t in tags if re.match(r'^v\d+\.\d+\.\d+$', t)]
|
|
print(max(versions, key=parse))
|
|
")
|
|
echo "Current plugin version: $CURRENT (max of plugin.json and existing v* tags)"
|
|
|
|
case "$LEVEL" in
|
|
patch|minor|major|auto)
|
|
# The plugin version tracks the PyPI package's MAJOR.MINOR and owns its
|
|
# PATCH (the plugin revision). The patch/minor/major argument is ignored:
|
|
# whether the plugin moves to a new MAJOR.MINOR is decided by the package,
|
|
# not by this script. A plugin-only change is always a PATCH bump, so it
|
|
# ships without forcing an (empty) package release.
|
|
NEW=$(python3 -c "
|
|
import re, tomllib
|
|
def parse(v):
|
|
m = re.match(r'^(\d+)\.(\d+)\.(\d+)$', v)
|
|
return tuple(int(x) for x in m.groups()) if m else (0, 0, 0)
|
|
pkg = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']
|
|
pmaj, pmin = (int(x) for x in re.match(r'^(\d+)\.(\d+)', pkg).groups())
|
|
cmaj, cmin, cpatch = parse('$CURRENT')
|
|
if (cmaj, cmin) == (pmaj, pmin):
|
|
# Same line as the package -> next plugin revision.
|
|
print(f'{pmaj}.{pmin}.{cpatch + 1}')
|
|
elif (pmaj, pmin) > (cmaj, cmin):
|
|
# Package advanced its MAJOR.MINOR -> resync the plugin to it.
|
|
print(f'{pmaj}.{pmin}.0')
|
|
else:
|
|
# Plugin already ahead of the package line (package not yet released for it);
|
|
# stay on the current line and bump the revision.
|
|
print(f'{cmaj}.{cmin}.{cpatch + 1}')
|
|
")
|
|
;;
|
|
[0-9]*.[0-9]*.[0-9]*)
|
|
if [[ ! "$LEVEL" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "Error: '$LEVEL' is not a valid X.Y.Z version" >&2
|
|
exit 1
|
|
fi
|
|
NEW="$LEVEL"
|
|
;;
|
|
*)
|
|
echo "Error: '$LEVEL' is not a valid level or version" >&2
|
|
usage
|
|
;;
|
|
esac
|
|
|
|
if [ "$NEW" = "$CURRENT" ]; then
|
|
echo "Error: new version equals current version ($NEW). Nothing to do." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if git rev-parse -q --verify "refs/tags/v$NEW" >/dev/null; then
|
|
echo "Error: tag v$NEW already exists. Choose a higher version." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "New version: $NEW"
|
|
echo "Tag will be: v$NEW"
|
|
echo
|
|
|
|
BRANCH=$(git branch --show-current)
|
|
echo "Current branch: $BRANCH"
|
|
|
|
if [ -n "$(git status --porcelain)" ]; then
|
|
echo "Error: working tree has uncommitted changes. Commit or stash first." >&2
|
|
git status --short >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
echo
|
|
echo "(--dry-run) Would change (absent manifests are skipped):"
|
|
for f in plugin/.claude-plugin/plugin.json \
|
|
plugin/.claude-plugin/marketplace.json \
|
|
.claude-plugin/marketplace.json \
|
|
plugins/tooluniverse/.codex-plugin/plugin.json; do
|
|
[ -f "$f" ] && echo " $f $CURRENT → $NEW" || echo " $f (absent, skipped)"
|
|
done
|
|
echo " Then commit \"Release v$NEW\", tag v$NEW, push to origin/$BRANCH"
|
|
exit 0
|
|
fi
|
|
|
|
python3 - <<EOF
|
|
import json, pathlib
|
|
new = "$NEW"
|
|
files = [
|
|
("plugin/.claude-plugin/plugin.json", "version"),
|
|
("plugin/.claude-plugin/marketplace.json", "metadata.version"),
|
|
(".claude-plugin/marketplace.json", "metadata.version"),
|
|
("plugins/tooluniverse/.codex-plugin/plugin.json", "version"),
|
|
]
|
|
for path, field in files:
|
|
p = pathlib.Path(path)
|
|
if not p.exists():
|
|
# Not all manifests exist in every layout; skip rather than crash.
|
|
print(f" skip (absent) {path}")
|
|
continue
|
|
data = json.loads(p.read_text())
|
|
if field == "version":
|
|
data["version"] = new
|
|
elif field == "metadata.version":
|
|
data.setdefault("metadata", {})["version"] = new
|
|
p.write_text(json.dumps(data, indent=2) + "\n")
|
|
print(f" bumped {path}")
|
|
EOF
|
|
|
|
echo
|
|
for f in plugin/.claude-plugin/plugin.json \
|
|
plugin/.claude-plugin/marketplace.json \
|
|
.claude-plugin/marketplace.json \
|
|
plugins/tooluniverse/.codex-plugin/plugin.json; do
|
|
[ -f "$f" ] && git add "$f"
|
|
done
|
|
git commit -m "Release v$NEW"
|
|
git tag -a "v$NEW" -m "v$NEW"
|
|
echo
|
|
echo "Created commit + tag v$NEW locally."
|
|
|
|
if [ "$NO_PUSH" -eq 1 ]; then
|
|
echo
|
|
echo "(--no-push) Stopping here. To finish the release manually:"
|
|
echo " git push origin $BRANCH && git push origin v$NEW"
|
|
exit 0
|
|
fi
|
|
|
|
echo
|
|
echo "Pushing to origin..."
|
|
git push origin "$BRANCH"
|
|
git push origin "v$NEW"
|
|
echo
|
|
echo "✓ Released v$NEW"
|
|
echo " - Auto-update users will receive it on next Claude Code startup"
|
|
echo " - GitHub Release zip will be built and attached by the workflow:"
|
|
echo " https://github.com/mims-harvard/ToolUniverse/actions/workflows/release-plugin.yml"
|