Skip to content

Commit 4717c09

Browse files
authored
Merge pull request #14 from deeleeramone/claude/fervent-shamir-84a4fd
Add Claude Code Plugin
2 parents c1f7981 + 905c6ce commit 4717c09

106 files changed

Lines changed: 2439 additions & 43 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Claude plugin manifests
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'claude/**'
7+
- '.github/workflows/plugin-manifest.yml'
8+
push:
9+
branches: [main]
10+
paths:
11+
- 'claude/**'
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
validate:
18+
name: Validate plugin manifests
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
# Full history so the CHANGELOG-diff check below can reach the
24+
# PR base without needing to re-fetch.
25+
fetch-depth: 0
26+
27+
- name: Check manifests parse and versions match
28+
shell: bash
29+
run: |
30+
set -euo pipefail
31+
python3 <<'PY'
32+
import json, sys, pathlib
33+
34+
repo = pathlib.Path(".")
35+
marketplace = repo / "claude" / ".claude-plugin" / "marketplace.json"
36+
if not marketplace.exists():
37+
sys.exit(f"::error::{marketplace} missing")
38+
try:
39+
mk = json.loads(marketplace.read_text(encoding="utf-8"))
40+
except json.JSONDecodeError as e:
41+
sys.exit(f"::error file={marketplace}::invalid JSON: {e}")
42+
43+
plugins = mk.get("plugins") or []
44+
if not plugins:
45+
sys.exit(f"::error file={marketplace}::no plugins listed")
46+
47+
failures = []
48+
for entry in plugins:
49+
name = entry.get("name")
50+
source = entry.get("source", "")
51+
entry_version = entry.get("version")
52+
if not name or not source or not entry_version:
53+
failures.append(f"{entry}: missing name/source/version")
54+
continue
55+
if not isinstance(source, str) or not source.startswith("./"):
56+
failures.append(f"{name}: source must be a './plugins/...' relative path")
57+
continue
58+
plugin_root = (repo / "claude" / source[2:]).resolve()
59+
if not plugin_root.exists():
60+
failures.append(f"{name}: source path does not exist: {plugin_root}")
61+
continue
62+
pj = plugin_root / ".claude-plugin" / "plugin.json"
63+
if not pj.exists():
64+
failures.append(f"{name}: missing {pj}")
65+
continue
66+
try:
67+
pm = json.loads(pj.read_text(encoding="utf-8"))
68+
except json.JSONDecodeError as e:
69+
failures.append(f"{name}: {pj} invalid JSON: {e}")
70+
continue
71+
if pm.get("version") != entry_version:
72+
failures.append(
73+
f"{name}: version mismatch — plugin.json={pm.get('version')!r}, "
74+
f"marketplace.json={entry_version!r}"
75+
)
76+
if pm.get("name") != name:
77+
failures.append(
78+
f"{name}: plugin.json name={pm.get('name')!r} != "
79+
f"marketplace entry name={name!r}"
80+
)
81+
# .mcp.json must parse if present
82+
mcp = plugin_root / ".mcp.json"
83+
if mcp.exists():
84+
try:
85+
json.loads(mcp.read_text(encoding="utf-8"))
86+
except json.JSONDecodeError as e:
87+
failures.append(f"{name}: {mcp} invalid JSON: {e}")
88+
# hooks.json must parse if present
89+
hooks = plugin_root / "hooks" / "hooks.json"
90+
if hooks.exists():
91+
try:
92+
json.loads(hooks.read_text(encoding="utf-8"))
93+
except json.JSONDecodeError as e:
94+
failures.append(f"{name}: {hooks} invalid JSON: {e}")
95+
96+
if failures:
97+
for f in failures:
98+
print(f"::error::{f}")
99+
sys.exit(1)
100+
print(f"ok — {len(plugins)} plugin(s) validated")
101+
PY
102+
103+
- name: Check CHANGELOG was updated when plugin.json version changes
104+
if: github.event_name == 'pull_request'
105+
shell: bash
106+
run: |
107+
set -euo pipefail
108+
base_sha="${{ github.event.pull_request.base.sha }}"
109+
# Two-dot diff: list files that differ between the PR base and
110+
# HEAD without needing a merge-base. fetch-depth: 0 above makes
111+
# both commits available locally.
112+
changed=$(git diff --name-only "$base_sha" HEAD -- 'claude/plugins/*/.claude-plugin/plugin.json')
113+
if [[ -z "$changed" ]]; then
114+
echo "No plugin.json changes — nothing to check."
115+
exit 0
116+
fi
117+
missing=0
118+
for pj in $changed; do
119+
plugin_dir=$(dirname "$(dirname "$pj")")
120+
changelog="$plugin_dir/CHANGELOG.md"
121+
if ! git diff --name-only "$base_sha" HEAD -- "$changelog" | grep -q .; then
122+
echo "::error file=$changelog::CHANGELOG.md not updated alongside $pj"
123+
missing=1
124+
fi
125+
done
126+
exit $missing

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,4 @@ docs/site/*
213213
# Generated frontend assets (downloaded by pywry/build_assets.py at build/install time)
214214
pywry/pywry/frontend/assets/*.js.gz
215215
pywry/pywry/frontend/assets/*.css.gz
216+
.hatch_build/

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<div align="center">
22

3-
![PyWry](https://github.com/deeleeramone/PyWry/blob/82db0c977a8ec812bf8652c0be14bf62b66b66a1/pywry/pywry/frontend/assets/PyWry.png?raw=true)
3+
<picture>
4+
<source media="(prefers-color-scheme: dark)" srcset="pywry/pywry/frontend/assets/PyWry-dark.svg">
5+
<img src="pywry/pywry/frontend/assets/PyWry-light.svg" alt="PyWry" width="640">
6+
</picture>
47

58
</div>
69

@@ -12,6 +15,12 @@ PyWry is a cross-platform rendering engine and desktop UI toolkit for Python. On
1215

1316
**Build Once, Render Anywhere:** Prototype interactive data apps in a Jupyter Notebook, easily deploy them as web apps, and seamlessly compile them into secure, lightweight standalone desktop executables via `pywry[freeze]`.
1417

18+
<div align="center">
19+
20+
![PyWry — live TradingView chart driving a streaming chat widget](pywry_tv_chat_screencap_lg.gif)
21+
22+
</div>
23+
1524
## Installation
1625

1726
Python 3.10–3.14, virtual environment recommended.
@@ -128,7 +137,43 @@ pip install 'pywry[mcp]'
128137
pywry mcp --transport stdio
129138
```
130139

131-
See the [MCP docs](https://deeleeramone.github.io/PyWry/mcp/) for Claude Desktop setup and tool reference.
140+
Widget-creating tools — `create_widget`, `show_plotly`, `show_dataframe`, `show_tvchart`, `create_chat_widget` — return an **`AppArtifact`**: a self-contained HTML snapshot delivered as an MCP `EmbeddedResource` with `mimeType: text/html` and URI `pywry-app://<widget_id>/<revision>`. Clients that render HTML resources (Claude Desktop's artifact pane, mcp-ui clients, PyWry's own chat widget) show the app inline.
141+
142+
Each render bumps a per-widget revision. The latest revision keeps a live WebSocket bridge to Python; older revisions freeze at their last known state. Call `get_widget_app(widget_id)` to re-snapshot after a mutation.
143+
144+
See the [MCP docs](https://deeleeramone.github.io/PyWry/mcp/) for tool reference and client setup.
145+
146+
## Claude Code Plugin
147+
148+
Installable under [`claude/plugins/pywry/`](claude/plugins/pywry/) as one `/plugin install` unit. Ships:
149+
150+
- **MCP server** — same 66 tools as above, auto-connected
151+
- **`pywry-orientation` skill** — teaches the agent when to reach for PyWry tools
152+
- **Slash commands**`/pywry:doctor`, `/pywry:scaffold`, `/pywry:examples`
153+
- **`pywry-builder` subagent** — for multi-step widget construction
154+
- **Post-edit hook** — runs `ruff format` on touched `.py` files
155+
156+
**Install:**
157+
158+
```
159+
/plugin marketplace add deeleeramone/PyWry --path claude/.claude-plugin/marketplace.json
160+
/plugin install pywry@pywry
161+
```
162+
163+
**Prerequisite:** `pip install 'pywry[dev]'` (or `pywry[all]`). Then `/pywry:doctor` to verify.
164+
165+
**PyPI-bundled install** (skips the GitHub round-trip once pywry is already installed):
166+
167+
```bash
168+
pywry plugin-path # prints the bundled plugin root
169+
```
170+
171+
```
172+
/plugin marketplace add $(pywry plugin-path)
173+
/plugin install pywry@pywry
174+
```
175+
176+
See [claude/README.md](claude/README.md) for the full install-path matrix, mono-repo layout, and versioning policy.
132177

133178
## Standalone Executables
134179

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "pywry",
3+
"owner": {
4+
"name": "PyWry",
5+
"email": "pywry2@gmail.com"
6+
},
7+
"plugins": [
8+
{
9+
"name": "pywry",
10+
"source": "./plugins/pywry/",
11+
"description": "Native Claude Code integration for PyWry — MCP tools for generating and rendering HTML components, chat artifacts, and building native, web, or Jupyter applications with live preview. Built-in support for AgGrid, Plotly, TradingView, and more.",
12+
"version": "0.1.0",
13+
"author": {
14+
"name": "PyWry",
15+
"email": "pywry2@gmail.com",
16+
"url": "https://github.com/deeleeramone/PyWry"
17+
},
18+
"license": "Apache-2.0",
19+
"homepage": "https://github.com/deeleeramone/PyWry",
20+
"repository": "https://github.com/deeleeramone/PyWry",
21+
"category": "data-visualization",
22+
"keywords": [
23+
"pywry",
24+
"webview",
25+
"tauri",
26+
"plotly",
27+
"tradingview",
28+
"aggrid",
29+
"dashboard",
30+
"charting",
31+
"visualization",
32+
"data-science",
33+
"finance",
34+
"interactive",
35+
"real-time",
36+
"chat",
37+
"llm",
38+
"agent",
39+
"artifact",
40+
"jupyter",
41+
"notebook",
42+
"python",
43+
"mcp"
44+
]
45+
}
46+
]
47+
}

claude/CONTRIBUTING.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Contributing to the PyWry Claude Code plugin
2+
3+
This document covers the mechanics of changing anything under `claude/`.
4+
For general PyWry development see the top-level [CLAUDE.md](../CLAUDE.md)
5+
and the PyWry contributing guide.
6+
7+
## What lives here
8+
9+
- **`claude/.claude-plugin/marketplace.json`** — the single, canonical
10+
marketplace manifest for this repo. It lists every plugin under
11+
`claude/plugins/*`. Discovered automatically when a Claude Code user
12+
adds `deeleeramone/PyWry --path claude/.claude-plugin/marketplace.json`
13+
as a marketplace.
14+
- **`claude/plugins/<name>/`** — one plugin each. Every plugin has its
15+
own `.claude-plugin/plugin.json`, `.mcp.json`, assets, CHANGELOG, and
16+
README.
17+
18+
## Versioning
19+
20+
Every plugin follows [semver](https://semver.org/) independently of the
21+
PyWry Python package.
22+
23+
| Bump | When |
24+
|---|---|
25+
| **Patch** (0.1.0 → 0.1.1) | Bug fixes, doc tweaks, tightening a skill, renaming an internal helper |
26+
| **Minor** (0.1.0 → 0.2.0) | New slash command, new skill, new subagent, new hook, new MCP tool surfaced |
27+
| **Major** (0.1.0 → 1.0.0) | Renaming the plugin, changing the marketplace name, removing a command/skill/tool, bumping the minimum required PyWry version |
28+
29+
The version appears in **two** places and they **must match**:
30+
31+
1. `claude/plugins/<name>/.claude-plugin/plugin.json``version`
32+
2. `claude/.claude-plugin/marketplace.json``plugins[…].version`
33+
34+
CI ([.github/workflows/plugin-manifest.yml](../.github/workflows/plugin-manifest.yml))
35+
fails PRs that leave those out of sync or change any `claude/plugins/<name>/`
36+
file without bumping the plugin's version.
37+
38+
## Release checklist
39+
40+
Use [RELEASING.md](plugins/pywry/RELEASING.md) for the step-by-step —
41+
summary below:
42+
43+
1. Land all pending changes on `main`.
44+
2. Bump the version in both manifest files.
45+
3. Update `claude/plugins/<name>/CHANGELOG.md` with the release notes
46+
(keep a `## [Unreleased]` section at the top).
47+
4. Commit with a message like
48+
`Release claude/plugins/pywry v0.2.0`.
49+
5. Tag:
50+
```bash
51+
git tag -a plugin-pywry-v0.2.0 -m "pywry plugin v0.2.0"
52+
git push origin plugin-pywry-v0.2.0
53+
```
54+
6. Users pin with `/plugin install pywry@pywry --version plugin-pywry-v0.2.0`
55+
to avoid tracking `main`.
56+
57+
## Adding a new plugin
58+
59+
```
60+
claude/plugins/<new-plugin-name>/
61+
├── .claude-plugin/
62+
│ └── plugin.json # {name, version, description, author, ...}
63+
├── .mcp.json # (optional) MCP server declaration
64+
├── agents/ # (optional)
65+
├── commands/ # (optional)
66+
├── hooks/ # (optional)
67+
├── skills/ # (optional)
68+
├── README.md
69+
└── CHANGELOG.md
70+
```
71+
72+
Then append an entry to `claude/.claude-plugin/marketplace.json`:
73+
74+
```json
75+
{
76+
"name": "<new-plugin-name>",
77+
"source": "./plugins/<new-plugin-name>/",
78+
"description": "...",
79+
"version": "0.1.0",
80+
"license": "Apache-2.0",
81+
"homepage": "https://github.com/deeleeramone/PyWry",
82+
"keywords": ["..."]
83+
}
84+
```
85+
86+
CI will reject the PR if:
87+
88+
- `.claude-plugin/plugin.json` or `.mcp.json` is malformed JSON.
89+
- The plugin version in the plugin manifest doesn't match the
90+
marketplace entry.
91+
- A referenced skill/command/agent/hook file is missing.
92+
- Any `source` path in `marketplace.json` doesn't exist on disk.
93+
94+
## Running the plugin locally during development
95+
96+
```bash
97+
# From the repo root, in the worktree or main checkout:
98+
pip install -e './pywry[dev]' # install worktree pywry
99+
/plugin marketplace add $(pwd)/claude # absolute path
100+
/plugin install pywry@pywry
101+
```
102+
103+
Then `/pywry:doctor` to verify.

0 commit comments

Comments
 (0)