Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "mgraphctl",
"interface": {
"displayName": "mgraphctl"
},
"plugins": [
{
"name": "mgraphctl",
"source": {
"source": "local",
"path": "./"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
20 changes: 11 additions & 9 deletions .claude/skills/releasing-a-version/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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"
Expand All @@ -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

Expand Down Expand Up @@ -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` |
Expand Down
26 changes: 26 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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."
]
}
}
13 changes: 7 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -68,7 +69,7 @@ jobs:
if [ ! -s notes.md ]; then
echo "No CHANGELOG.md section '## [$version] — <date>'"; 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" \
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 6 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <repo> --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`.

Expand All @@ -40,7 +40,9 @@ skills/mgraphctl/
reference/commands/<noun>.md # one `### noun verb` heading per registered verb
evals/evals.json
tests/ # test_cli_<noun>.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
Expand All @@ -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/<noun>.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`).

Expand All @@ -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
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions VERSIONING.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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. |
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Loading