diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..87d4440 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "mgraphctl", + "interface": { + "displayName": "mgraphctl" + }, + "plugins": [ + { + "name": "mgraphctl", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Productivity" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0f95f20..e192b9f 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "mgraphctl", "displayName": "mgraphctl", "source": "./", - "version": "0.2.1", + "version": "0.3.0", "description": "Work with Microsoft 365 — Outlook mail and calendar, Teams chats and channels, presence, meetings and transcripts, SharePoint, OneDrive, OneNote, Planner, To Do, people and org chart — through the Microsoft Graph API (Python CLI run with uv).", "author": { "name": "Sviatoslav Sviridov", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 6f15c8f..72be06a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin.json", "name": "mgraphctl", - "version": "0.2.1", + "version": "0.3.0", "description": "Work with Microsoft 365 — Outlook mail and calendar, Teams chats and channels, presence, meetings and transcripts, SharePoint, OneDrive, OneNote, Planner, To Do, people and org chart — through the Microsoft Graph API (Python CLI run with uv).", "author": { "name": "Sviatoslav Sviridov", diff --git a/.claude/skills/releasing-a-version/SKILL.md b/.claude/skills/releasing-a-version/SKILL.md index 13fe9b7..a61cd5a 100644 --- a/.claude/skills/releasing-a-version/SKILL.md +++ b/.claude/skills/releasing-a-version/SKILL.md @@ -1,17 +1,18 @@ --- name: releasing-a-version -description: Use when releasing, cutting, tagging, or shipping a new version of the mgraphctl plugin - bumping the five version strings, rolling the CHANGELOG [Unreleased] block into a dated header, opening the dev->main pull request, and cutting the mgraphctl--vX.Y.Z tag with `claude plugin tag`. Specific to the mgraphctl repo. +description: Use when releasing, cutting, tagging, or shipping a new version of the mgraphctl plugin - bumping the six version strings, rolling the CHANGELOG [Unreleased] block into a dated header, opening the dev->main pull request, and cutting the mgraphctl--vX.Y.Z tag with `claude plugin tag`. Specific to the mgraphctl repo. --- # Releasing a version -Two artifacts ship from this repo at one version, from one tag: the plugin (the marketplace -installs the repo at the tag) and the `mgraphctl` package on PyPI (published by the tag's -workflow). Rules live in `VERSIONING.md`. +Three artifacts ship from this repo at one version: the Claude Code and Codex plugins and the +`mgraphctl` package on PyPI. Claude installs the release tag; Codex installs its configured Git +ref (`main` by default, or an explicit release tag). The tag publishes the PyPI package. +Rules live in `VERSIONING.md`. | Artifact | Version files | Tag form | Tag branch | |---|---|---|---| -| mgraphctl plugin | `pyproject.toml`, `src/mgraphctl/__init__.py`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json` (`plugins[name=mgraphctl].version`), `skills/mgraphctl/SKILL.md` (`metadata.version`) | `mgraphctl--vX.Y.Z` (annotated, via `claude plugin tag`) | `main` only | +| mgraphctl plugin | `pyproject.toml`, `src/mgraphctl/__init__.py`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.claude-plugin/marketplace.json` (`plugins[name=mgraphctl].version`), `skills/mgraphctl/SKILL.md` (`metadata.version`) | `mgraphctl--vX.Y.Z` (annotated, via `claude plugin tag`) | `main` only | `marketplace.json` top-level `version` is the catalog's own version — leave it alone. @@ -67,12 +68,13 @@ Skipping it is a valid choice, but say so in the release notes if a known fix is ## Step 3 - Release commit on `dev` -Set the version in all five files. `uv version` handles the first and keeps `uv.lock` in step; +Set the version in all six files. `uv version` handles the first and keeps `uv.lock` in step; the rest are direct edits: ```bash uv version X.Y.Z # pyproject.toml + uv.lock # src/mgraphctl/__init__.py __version__ = "X.Y.Z" +# .codex-plugin/plugin.json "version": "X.Y.Z" # .claude-plugin/plugin.json "version": "X.Y.Z" # .claude-plugin/marketplace.json plugins[0].version = "X.Y.Z" (not the top-level one) # skills/mgraphctl/SKILL.md metadata.version: "X.Y.Z" @@ -85,8 +87,8 @@ post-release change, so the file never carries an empty section. `VERSIONING.md`: add a row to the version-history table. ```bash -uv run pytest tests/test_version.py tests/test_docs.py -q # the five strings agree -git add pyproject.toml uv.lock src/mgraphctl/__init__.py .claude-plugin/ skills/mgraphctl/SKILL.md CHANGELOG.md VERSIONING.md +uv run pytest tests/test_version.py tests/test_docs.py -q # the six strings agree +git add pyproject.toml uv.lock src/mgraphctl/__init__.py .claude-plugin/ .codex-plugin/ skills/mgraphctl/SKILL.md CHANGELOG.md VERSIONING.md git commit -m "$(cat <<'EOF' chore(release): vX.Y.Z @@ -154,7 +156,7 @@ entry's `source.ref` to `mgraphctl--vX.Y.Z` there and release that repo per its | Mistake | Effect | |---|---| -| Bumped some of the five version files | `test_version.py` fails; `claude plugin tag` refuses if the two manifests disagree | +| Bumped some of the six version files | `test_version.py` fails; `claude plugin tag` refuses if the two manifests disagree | | Touched `marketplace.json` top-level `version` | That is the catalog's version, unrelated to the plugin | | Left `[Unreleased]` undated | Release workflow fails: no CHANGELOG section for the version | | Ran `claude plugin tag` on `dev` | Tags HEAD wherever you are; consumers pin a commit that may never reach `main` | diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..19dbc2a --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "name": "mgraphctl", + "version": "0.3.0", + "description": "Work with Microsoft 365 through the Microsoft Graph API using a bundled Python CLI run with uv.", + "author": { + "name": "Sviatoslav Sviridov", + "email": "sviridov@gmail.com" + }, + "repository": "https://github.com/svd/mgraphctl", + "license": "MIT", + "skills": "./skills/", + "interface": { + "displayName": "mgraphctl", + "shortDescription": "Microsoft 365 mail, meetings, files, and tasks", + "longDescription": "Use Outlook, Teams, SharePoint, OneDrive, OneNote, Planner, and To Do through Microsoft Graph. Requires uv and a user-configured Entra application and login.", + "developerName": "Sviatoslav Sviridov", + "category": "Productivity", + "capabilities": ["Read", "Write"], + "websiteURL": "https://github.com/svd/mgraphctl", + "defaultPrompt": [ + "Summarize my unread emails.", + "Show my meetings for today.", + "Find my open tasks." + ] + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d72954..fca1c4b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,8 @@ name: Release -# Fires on the tags `claude plugin tag` creates: mgraphctl--vX.Y.Z. One tag ships two -# things at one version: the plugin (the marketplace installs the repo at the tag) and the -# `mgraphctl` package on PyPI (Trusted Publishing, no stored token). The GitHub Release +# Fires on the tags `claude plugin tag` creates: mgraphctl--vX.Y.Z. One tag ships three +# things at one version: the Claude Code and Codex plugins and the `mgraphctl` package +# on PyPI (Trusted Publishing, no stored token). The GitHub Release # page comes last, with notes from CHANGELOG.md; no build artifacts are attached to it, # PyPI is the one place the wheel lives. on: @@ -34,8 +34,9 @@ jobs: tag="${GITHUB_REF_NAME#mgraphctl--v}" plugin=$(python3 -c 'import json; print(json.load(open(".claude-plugin/plugin.json"))["version"])') pkg=$(grep -m1 -E '^version *= *"' pyproject.toml | sed -E 's/.*"(.*)".*/\1/') - if [ "$tag" != "$plugin" ] || [ "$tag" != "$pkg" ]; then - echo "Tag $GITHUB_REF_NAME does not match plugin.json ($plugin) / pyproject.toml ($pkg)" + codex=$(python3 -c 'import json; print(json.load(open(".codex-plugin/plugin.json"))["version"])') + if [ "$tag" != "$codex" ] || [ "$tag" != "$plugin" ] || [ "$tag" != "$pkg" ]; then + echo "Tag $GITHUB_REF_NAME does not match Claude ($plugin) / Codex ($codex) / pyproject.toml ($pkg)" exit 1 fi @@ -68,7 +69,7 @@ jobs: if [ ! -s notes.md ]; then echo "No CHANGELOG.md section '## [$version] — '"; exit 1 fi - printf '\n---\n\nPlugin: `claude plugin marketplace add svd/mgraphctl` then `claude plugin install mgraphctl@mgraphctl`\n\nCLI only: `uv tool install mgraphctl==%s` · [PyPI](https://pypi.org/project/mgraphctl/%s/)\n' \ + printf '\n---\n\nPlugin: `claude plugin marketplace add svd/mgraphctl` then `claude plugin install mgraphctl@mgraphctl`\n\nCodex: `codex plugin marketplace add svd/mgraphctl --ref main` then `codex plugin add mgraphctl@mgraphctl`\n\nCLI only: `uv tool install mgraphctl==%s` · [PyPI](https://pypi.org/project/mgraphctl/%s/)\n' \ "$version" "$version" >> notes.md gh release create "$GITHUB_REF_NAME" \ --title "mgraphctl v$version" \ diff --git a/.gitignore b/.gitignore index 84aa4a8..69c67d5 100644 --- a/.gitignore +++ b/.gitignore @@ -220,3 +220,9 @@ __marimo__/ # Claude Code session handoffs are local working notes .claude/session-handoffs/ .claude/settings.local.json + +# understand-anything plugin state +.ua/ + +# codegraph index +.codegraph/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f84bb81..ed64f00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.3.0] — 2026-09-07 + +- Add a Codex plugin manifest and repository marketplace. Claude Code and Codex share the + bundled CLI and skill, with host-specific command-path instructions and one release version. + ## [0.2.1] — 2026-09-06 - The skill's command reference is one file per noun. `reference/commands.md` keeps the diff --git a/CLAUDE.md b/CLAUDE.md index 941b077..5fe6c20 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ claude --plugin-dir ~/src/mgraphctl # try the skill without installing the plu ## What this repo is -A Claude Code plugin with one skill, `mgraphctl`, backed by a Python CLI of the same name. +A Claude Code and Codex plugin with one shared skill, `mgraphctl`, backed by a Python CLI of the same name. The root `mgraphctl` shim runs `uv run --project --frozen --no-dev mgraphctl`, creating the venv on first use. The marketplace installs the repo at a tag; the same tag publishes the CLI to PyPI as `mgraphctl`. @@ -40,7 +40,9 @@ skills/mgraphctl/ reference/commands/.md # one `### noun verb` heading per registered verb evals/evals.json tests/ # test_cli_.py per noun; @covers ties tests to verbs -.claude-plugin/ # plugin.json + single-entry marketplace.json +.claude-plugin/ # Claude plugin.json + marketplace.json +.codex-plugin/ # Codex plugin.json +.agents/plugins/ # Codex marketplace.json .claude/skills/releasing-a-version/ docs/{specs,plans,research}/ # design history; ruff excludes docs/ scripts/ # stdlib-only helpers CI runs: scan_secrets.py, extract-changelog.sh @@ -51,7 +53,7 @@ scripts/ # stdlib-only helpers CI runs: scan_secrets.py, e - Every registered verb has a `@covers` test, a `--json` flag, and a heading in some `reference/commands/.md`, which `reference/commands.md` links (`test_surface.py`, `test_docs.py`). -- The five version strings agree and are bare `X.Y.Z` (`test_version.py`). +- The six version strings agree and are bare `X.Y.Z` (`test_version.py`). - SKILL.md frontmatter follows the Agent Skills rules: name pattern, description <= 1024 chars, no XML-shaped tags (`test_docs.py`). @@ -69,7 +71,7 @@ release surface. `.github/workflows/ci.yml` runs tests on Python 3.10-3.13, ruff | Artifact | Version source | Tag | Made with | |---|---|---|---| -| mgraphctl plugin | `plugin.json` (+ four mirrors, see VERSIONING.md) | `mgraphctl--vX.Y.Z` on `main` | `claude plugin tag --push .` | +| mgraphctl plugin | `plugin.json` (+ five mirrors, see VERSIONING.md) | `mgraphctl--vX.Y.Z` on `main` | `claude plugin tag --push .` | | mgraphctl on PyPI | `pyproject.toml` (same version) | same tag | the tag's Release workflow, Trusted Publishing | Pushing the tag runs `.github/workflows/release.yml`: annotated-tag and version checks, the diff --git a/README.md b/README.md index 16fdc6b..4d7ad64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mgraphctl -Microsoft 365 access from Claude Code through the Microsoft Graph API: Outlook mail and +Microsoft 365 access from Claude Code and Codex through the Microsoft Graph API: Outlook mail and calendar, Teams chats and channel messages, presence, meetings and transcripts, SharePoint, OneDrive, OneNote, Planner, Microsoft To Do, AI meeting insights, contacts, and the org chart. @@ -15,12 +15,33 @@ verb supports `--dry-run`, and no data command ever opens a browser. ## Install +### Claude Code + ```bash claude plugin marketplace add svd/mgraphctl claude plugin install mgraphctl@mgraphctl ``` -The CLI is also on PyPI for use outside Claude Code, under the same version as the plugin: +### Codex + +```bash +codex plugin marketplace add svd/mgraphctl --ref main +codex plugin add mgraphctl@mgraphctl +``` + +Start a new Codex task after installation to load the skill. For development, register this +checkout instead with `codex plugin marketplace add /absolute/path/to/mgraphctl`, then run the +same `codex plugin add` command. The marketplace packages the entire repository so the skill, +launcher, Python sources and lockfile stay together. + +The skill resolves the launcher from its installed location. In Codex, replace +`${CLAUDE_PLUGIN_ROOT}/mgraphctl` in the examples below with the quoted absolute launcher path +reported by the skill. That Claude variable is not required in Codex. For login in your own +terminal, use the actual absolute path, not the variable. + +### Standalone CLI + +The CLI is also on PyPI, under the same version as the plugin: ```bash uv tool install mgraphctl # or: pipx install mgraphctl / uvx mgraphctl status @@ -59,7 +80,7 @@ Check status first — it never opens a browser and always says what to do next: ${CLAUDE_PLUGIN_ROOT}/mgraphctl status ``` -Signing in is interactive, so run it yourself in your own terminal rather than asking Claude to +Signing in is interactive, so run it yourself in your own terminal rather than asking the agent to run it for you: ```bash diff --git a/VERSIONING.md b/VERSIONING.md index b3e53d2..7259485 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -1,19 +1,24 @@ # Versioning -Two artifacts ship from this repo at one shared version: the `mgraphctl` Claude Code plugin, +Three artifacts ship from this repo at one shared version: the `mgraphctl` Claude Code and Codex plugins, installed straight from a git ref, and the `mgraphctl` Python package on PyPI, for the CLI on its own. A release is a commit on `main` carrying a bare `X.Y.Z` version, plus the annotated tag `mgraphctl--vX.Y.Z` that `claude plugin tag` creates on it. The tag is the distribution: -the marketplace entry points at it, and pushing it runs the workflow that publishes to PyPI. +Claude installs that tag; Codex installs its configured ref. Pushing the tag publishes to PyPI. | Artifact | Channel | Consumer command | |---|---|---| -| plugin | git tag via marketplace | `claude plugin install mgraphctl@mgraphctl` | +| Claude Code plugin | git tag via marketplace | `claude plugin install mgraphctl@mgraphctl` | +| Codex plugin | git ref via marketplace | `codex plugin add mgraphctl@mgraphctl` | | package | PyPI | `uv tool install mgraphctl` | -One version, not two, because the skill calls the CLI through the shim from the same checkout: +One shared version, because the skill calls the CLI through the shim from the same checkout: a skill change and a CLI change are always released together, so nothing needs to drift. +Codex follows the ref configured with `codex plugin marketplace add svd/mgraphctl --ref main`. +To pin a release, use `--ref mgraphctl--vX.Y.Z`. Its marketplace entry points at the repository +root and has no separate version field; the Codex plugin manifest carries the version. + ## Bump rules Semantic versioning, `MAJOR.MINOR.PATCH`, judged against what the skill and CLI expose. @@ -28,8 +33,8 @@ Choose the highest bump the range since the last tag warrants. ## Conventions -- The version is one string held in five places, all bare `X.Y.Z`: `pyproject.toml`, - `src/mgraphctl/__init__.py`, `.claude-plugin/plugin.json`, the `mgraphctl` entry in +- The version is one string held in six places, all bare `X.Y.Z`: `pyproject.toml`, + `src/mgraphctl/__init__.py`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, the `mgraphctl` entry in `.claude-plugin/marketplace.json`, and `metadata.version` in `skills/mgraphctl/SKILL.md`. `tests/test_version.py` fails if any two disagree, and rejects `-SNAPSHOT` / `.devN` suffixes. - Between releases the version stays at the last released value. Pending work accumulates under @@ -73,3 +78,4 @@ summarises it. | `mgraphctl--v0.1.0` | 2026-09-05 | Initial release: parity with `msgraph`, config file, keychain token storage, client id guard. | | `mgraphctl--v0.2.0` | 2026-09-06 | Python floor drops to 3.10, so the plugin runs under the Claude desktop app. | | `mgraphctl--v0.2.1` | 2026-09-06 | Command reference split into one file per noun. | +| `mgraphctl--v0.3.0` | 2026-09-07 | Codex plugin manifest and repository marketplace added. | diff --git a/pyproject.toml b/pyproject.toml index 572ab2b..cb555e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mgraphctl" -version = "0.2.1" +version = "0.3.0" description = "Microsoft Graph CLI behind the mgraphctl Claude Code skill" readme = "README.md" license = "MIT" diff --git a/skills/mgraphctl/SKILL.md b/skills/mgraphctl/SKILL.md index 34d5b58..e341e05 100644 --- a/skills/mgraphctl/SKILL.md +++ b/skills/mgraphctl/SKILL.md @@ -14,7 +14,7 @@ description: > allowed-tools: Bash(${CLAUDE_PLUGIN_ROOT}/mgraphctl *) metadata: author: Sviatoslav Sviridov - version: "0.2.1" + version: "0.3.0" --- # Microsoft Graph (Python CLI) @@ -30,17 +30,27 @@ table, Graph call and scopes for all 123 verbs. Open the one noun's file a task run takes 10-40 seconds and prints a notice on stderr while it works. Every later run is fast. Nothing needs installing by hand, and nothing is written inside the plugin directory. -Spell out the full path on every single call: +Choose the command path for the host before running anything: + +- **Claude Code:** use `${CLAUDE_PLUGIN_ROOT}/mgraphctl`. The `allowed-tools` frontmatter + applies to Claude's Bash tool only. +- **Codex:** take the absolute path of this loaded `SKILL.md`, go two directories above its + containing directory (`skills/mgraphctl/`), and append `/mgraphctl`. Replace + `${CLAUDE_PLUGIN_ROOT}/mgraphctl` in every example and referenced command with that quoted + absolute path. For example, a skill at `/cache/mgraphctl/skills/mgraphctl/SKILL.md` uses + `"/cache/mgraphctl/mgraphctl" status`. Do not expect a `CLAUDE_PLUGIN_ROOT` environment variable + or Claude's Bash preapproval in Codex. Use the installed skill path, not the task's cwd. + +Spell out the full path on every single call. The examples below use Claude's spelling: ```bash ${CLAUDE_PLUGIN_ROOT}/mgraphctl status ${CLAUDE_PLUGIN_ROOT}/mgraphctl mail list --unread --limit 10 ``` -Never define an alias, a shell variable, a `cd`, or any other shorthand for that path. The tool -permission matches this literal command prefix, and each Bash call is a fresh shell — a variable -set in one call does not exist in the next. `${CLAUDE_PLUGIN_ROOT}` is the only variable that -belongs in one of these command lines. +Never define an alias, a shell variable, a `cd`, or any other shorthand for that path. +In Claude Code the permission matches this literal command prefix; in Codex write the resolved, +quoted absolute path each time. Shell state need not persist between tool calls. ## Login and status @@ -287,9 +297,9 @@ ${CLAUDE_PLUGIN_ROOT}/mgraphctl people search "Anna" --json more results are available, tell the user rather than silently presenting a partial answer. 5. **Microsoft 365 only.** Decline Slack, Gmail, Google Drive and other non-Microsoft requests — this skill cannot reach them. -6. **Never guess today's date.** Only the `mgraphctl` prefix is pre-approved, so you cannot run - `date`. Prefer relative inputs the CLI resolves itself — `--days 7`, `today`, `tomorrow`, - `yesterday`, `+2d`, `-14d` — and read real dates off the output of `status` or +6. **Never guess today's date.** Claude's preapproval covers only the `mgraphctl` prefix; + Codex uses its own tool permissions. Prefer relative inputs the CLI resolves itself — + `--days 7`, `today`, `tomorrow`, `yesterday`, `+2d`, `-14d` — and read real dates off the output of `status` or `calendar list`, which print offsets. If an absolute date is unavoidable, take it from the conversation or ask the user. The zone comes from `--tz` or `MGRAPHCTL_TZ`. 7. **Ids from `mail move` change.** Moving a message gives it a new id, so re-list after moving diff --git a/skills/mgraphctl/reference/commands.md b/skills/mgraphctl/reference/commands.md index c8f5892..b504207 100644 --- a/skills/mgraphctl/reference/commands.md +++ b/skills/mgraphctl/reference/commands.md @@ -14,7 +14,10 @@ ${CLAUDE_PLUGIN_ROOT}/mgraphctl mail list --unread --limit 10 ${CLAUDE_PLUGIN_ROOT}/mgraphctl teams channel send TEAM CHANNEL --body "hi" --dry-run ``` -There is no alias and no shorter spelling: `${CLAUDE_PLUGIN_ROOT}` is the only variable that ever +In Codex, replace `${CLAUDE_PLUGIN_ROOT}/mgraphctl` in every example with the quoted absolute +launcher path resolved from the loaded skill, as described in [Setup](../SKILL.md#setup). + +In Claude Code there is no alias or shorter spelling: `${CLAUDE_PLUGIN_ROOT}` is the only variable that ever appears in a command line. - **Scopes** — the scopes the local gate checks before the first request. "A or B" means either diff --git a/src/mgraphctl/__init__.py b/src/mgraphctl/__init__.py index 078da65..3a1a3cc 100644 --- a/src/mgraphctl/__init__.py +++ b/src/mgraphctl/__init__.py @@ -1,3 +1,3 @@ """mgraphctl: Microsoft Graph CLI for the mgraphctl skill.""" -__version__ = "0.2.1" +__version__ = "0.3.0" diff --git a/tests/test_codex_plugin.py b/tests/test_codex_plugin.py new file mode 100644 index 0000000..841f49f --- /dev/null +++ b/tests/test_codex_plugin.py @@ -0,0 +1,24 @@ +"""The Codex marketplace must distribute the skill and its complete CLI project.""" + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_marketplace_contains_a_self_contained_plugin(): + marketplace = json.loads((ROOT / ".agents/plugins/marketplace.json").read_text()) + entries = [entry for entry in marketplace["plugins"] if entry["name"] == "mgraphctl"] + assert len(entries) == 1 + source = entries[0]["source"] + assert source["source"] == "local" + plugin_root = (ROOT / source["path"]).resolve() + assert plugin_root == ROOT + manifest = json.loads((plugin_root / ".codex-plugin/plugin.json").read_text()) + assert manifest["name"] == entries[0]["name"] + skill = plugin_root / manifest["skills"] / "mgraphctl/SKILL.md" + assert skill.is_file() + # Codex resolves the executable two directories above the loaded skill directory. + assert skill.parent.parent.parent / "mgraphctl" == plugin_root / "mgraphctl" + for path in ("mgraphctl", "pyproject.toml", "uv.lock", "src/mgraphctl/cli.py"): + assert (plugin_root / path).is_file(), path diff --git a/tests/test_version.py b/tests/test_version.py index 93aeee5..9bd9a6d 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,7 +1,7 @@ """Every version string in the repo must agree (spec §2.3, VERSIONING.md). -Five surfaces carry the version: the package (`mgraphctl.__version__` and the installed -metadata), the plugin manifest, the plugin's own marketplace entry, and the skill's frontmatter. +Six surfaces carry the version: the package (`mgraphctl.__version__` and the installed +metadata), the two plugin manifests, the Claude marketplace entry, and the skill's frontmatter. `claude plugin tag` checks the two manifests against each other; this test ties the rest to them. """ @@ -25,6 +25,8 @@ def test_versions_agree(): assert mgraphctl.__version__ == importlib.metadata.version("mgraphctl") assert json.loads(PLUGIN_JSON.read_text())["version"] == mgraphctl.__version__ + codex_plugin = ROOT / ".codex-plugin" / "plugin.json" + assert json.loads(codex_plugin.read_text())["version"] == mgraphctl.__version__ def test_marketplace_entry_agrees(): diff --git a/uv.lock b/uv.lock index ad9485f..394503b 100644 --- a/uv.lock +++ b/uv.lock @@ -446,7 +446,7 @@ wheels = [ [[package]] name = "mgraphctl" -version = "0.2.1" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "httpx" },