plain 0.101.2__py3-none-any.whl → 0.103.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/CHANGELOG.md +28 -0
- plain/agents/.claude/rules/plain.md +88 -0
- plain/cli/agent.py +152 -154
- plain/cli/docs.py +88 -52
- plain/cli/llmdocs.py +28 -21
- {plain-0.101.2.dist-info → plain-0.103.0.dist-info}/METADATA +1 -1
- {plain-0.101.2.dist-info → plain-0.103.0.dist-info}/RECORD +12 -15
- plain/skills/README.md +0 -36
- plain/skills/plain-docs/SKILL.md +0 -25
- plain/skills/plain-request/SKILL.md +0 -39
- plain/skills/plain-shell/SKILL.md +0 -24
- /plain/{skills → agents/.claude/skills}/plain-install/SKILL.md +0 -0
- /plain/{skills → agents/.claude/skills}/plain-upgrade/SKILL.md +0 -0
- {plain-0.101.2.dist-info → plain-0.103.0.dist-info}/WHEEL +0 -0
- {plain-0.101.2.dist-info → plain-0.103.0.dist-info}/entry_points.txt +0 -0
- {plain-0.101.2.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
plain/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# plain changelog
|
|
2
2
|
|
|
3
|
+
## [0.103.0](https://github.com/dropseed/plain/releases/plain@0.103.0) (2026-01-30)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- `plain docs` now shows markdown documentation by default (previously required `--source`), with a new `--symbols` flag to show only the symbolicated API surface ([b71dab9d5d](https://github.com/dropseed/plain/commit/b71dab9d5d))
|
|
8
|
+
- `plain docs --list` now shows all official Plain packages (installed and uninstalled) with descriptions and install status ([9cba705d62](https://github.com/dropseed/plain/commit/9cba705d62))
|
|
9
|
+
- `plain docs` for uninstalled packages now shows the install command and an online docs URL instead of a generic error ([9cba705d62](https://github.com/dropseed/plain/commit/9cba705d62))
|
|
10
|
+
- Removed the `plain agent context` command and the `SessionStart` hook setup — agent rules now provide context directly without needing a startup hook ([88d9424643](https://github.com/dropseed/plain/commit/88d9424643))
|
|
11
|
+
- `plain agent install` now cleans up old SessionStart hooks from `.claude/settings.json` ([88d9424643](https://github.com/dropseed/plain/commit/88d9424643))
|
|
12
|
+
|
|
13
|
+
### Upgrade instructions
|
|
14
|
+
|
|
15
|
+
- The `--source` flag for `plain docs` has been removed. Use `--symbols` instead to see the symbolicated API surface.
|
|
16
|
+
- The `--open` flag for `plain docs` has been removed.
|
|
17
|
+
- Run `plain agent install` to clean up the old SessionStart hook from your `.claude/settings.json`.
|
|
18
|
+
|
|
19
|
+
## [0.102.0](https://github.com/dropseed/plain/releases/plain@0.102.0) (2026-01-28)
|
|
20
|
+
|
|
21
|
+
### What's changed
|
|
22
|
+
|
|
23
|
+
- Refactored agent integration from skills-based to rules-based: packages now provide `agents/.claude/rules/` files and `agents/.claude/skills/` directories instead of `skills/` directories ([512040ac51](https://github.com/dropseed/plain/commit/512040ac51))
|
|
24
|
+
- The `plain agent install` command now copies both rules (`.md` files) and skills to the project's `.claude/` directory, and cleans up orphaned `plain*` items ([512040ac51](https://github.com/dropseed/plain/commit/512040ac51))
|
|
25
|
+
- Removed standalone skills (`plain-docs`, `plain-shell`, `plain-request`) that are now provided as passive rules instead ([512040ac51](https://github.com/dropseed/plain/commit/512040ac51))
|
|
26
|
+
|
|
27
|
+
### Upgrade instructions
|
|
28
|
+
|
|
29
|
+
- Run `plain agent install` to update your `.claude/` directory with the new rules-based structure.
|
|
30
|
+
|
|
3
31
|
## [0.101.2](https://github.com/dropseed/plain/releases/plain@0.101.2) (2026-01-28)
|
|
4
32
|
|
|
5
33
|
### What's changed
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Plain Framework
|
|
2
|
+
|
|
3
|
+
Plain is a Python web framework.
|
|
4
|
+
|
|
5
|
+
- Always use `uv run` to execute commands — never use bare `python` or `plain` directly.
|
|
6
|
+
- Plain is a Django fork but has different APIs — never assume Django patterns will work.
|
|
7
|
+
- When unsure about an API or something doesn't work, run `uv run plain docs <package>` first. Add `--symbols` if you need the full API surface.
|
|
8
|
+
- Use the `/plain-install` skill to add new Plain packages.
|
|
9
|
+
- Use the `/plain-upgrade` skill to upgrade Plain packages.
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
Run `uv run plain docs --list` to see all official packages (installed and uninstalled) with descriptions.
|
|
14
|
+
Run `uv run plain docs <package>` for markdown documentation (installed packages only).
|
|
15
|
+
Run `uv run plain docs <package> --symbols` for the symbolicated API surface.
|
|
16
|
+
For uninstalled packages, the CLI shows the install command and an online docs URL.
|
|
17
|
+
|
|
18
|
+
Online docs URL pattern: `https://plainframework.com/docs/<pip-name>/<module/path>/README.md`
|
|
19
|
+
Example: `https://plainframework.com/docs/plain-models/plain/models/README.md`
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
|
|
23
|
+
- `uv run plain docs models` - Models and database docs
|
|
24
|
+
- `uv run plain docs models --symbols` - Models API surface
|
|
25
|
+
- `uv run plain docs templates` - Jinja2 templates
|
|
26
|
+
- `uv run plain docs assets` - Static assets
|
|
27
|
+
|
|
28
|
+
### All official packages
|
|
29
|
+
|
|
30
|
+
- **plain** — Web framework core
|
|
31
|
+
- **plain-admin** — Backend admin interface
|
|
32
|
+
- **plain-api** — Class-based API views
|
|
33
|
+
- **plain-auth** — User authentication and authorization
|
|
34
|
+
- **plain-cache** — Database-backed cache with optional expiration
|
|
35
|
+
- **plain-code** — Preconfigured code formatting and linting
|
|
36
|
+
- **plain-dev** — Local development server with auto-reload
|
|
37
|
+
- **plain-elements** — HTML template components
|
|
38
|
+
- **plain-email** — Send email
|
|
39
|
+
- **plain-esbuild** — Build JavaScript with esbuild
|
|
40
|
+
- **plain-flags** — Feature flags via database models
|
|
41
|
+
- **plain-htmx** — HTMX integration for templates and views
|
|
42
|
+
- **plain-jobs** — Background jobs with a database-driven queue
|
|
43
|
+
- **plain-loginlink** — Link-based authentication
|
|
44
|
+
- **plain-models** — Model data and store it in a database
|
|
45
|
+
- **plain-oauth** — OAuth provider login
|
|
46
|
+
- **plain-observer** — On-page telemetry and observability
|
|
47
|
+
- **plain-pages** — Serve static pages, markdown, and assets
|
|
48
|
+
- **plain-pageviews** — Client-side pageview tracking
|
|
49
|
+
- **plain-passwords** — Password authentication
|
|
50
|
+
- **plain-pytest** — Test with pytest
|
|
51
|
+
- **plain-redirection** — URL redirection with admin and logging
|
|
52
|
+
- **plain-scan** — Test for production best practices
|
|
53
|
+
- **plain-sessions** — Database-backed sessions
|
|
54
|
+
- **plain-start** — Bootstrap a new project from templates
|
|
55
|
+
- **plain-support** — Support forms for your application
|
|
56
|
+
- **plain-tailwind** — Tailwind CSS without JavaScript or npm
|
|
57
|
+
- **plain-toolbar** — Debug toolbar
|
|
58
|
+
- **plain-tunnel** — Remote access to local dev server
|
|
59
|
+
- **plain-vendor** — Vendor CDN scripts and styles
|
|
60
|
+
|
|
61
|
+
## Shell
|
|
62
|
+
|
|
63
|
+
`uv run plain shell` opens an interactive Python shell with Plain configured and database access.
|
|
64
|
+
|
|
65
|
+
Run a one-off command:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
uv run plain shell -c "from app.users.models import User; print(User.query.count())"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Run a script:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
uv run plain run script.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## HTTP Requests
|
|
78
|
+
|
|
79
|
+
Use `uv run plain request` to make test HTTP requests against the dev database.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
uv run plain request /path
|
|
83
|
+
uv run plain request /path --user 1
|
|
84
|
+
uv run plain request /path --header "Accept: application/json"
|
|
85
|
+
uv run plain request /path --method POST --data '{"key": "value"}'
|
|
86
|
+
uv run plain request /path --no-body # Headers only
|
|
87
|
+
uv run plain request /path --no-headers # Body only
|
|
88
|
+
```
|
plain/cli/agent.py
CHANGED
|
@@ -9,34 +9,21 @@ from pathlib import Path
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
13
|
-
"""Get
|
|
12
|
+
def _get_agent_dirs() -> list[Path]:
|
|
13
|
+
"""Get list of agents/.claude/ directories from installed plain.* packages."""
|
|
14
|
+
agent_dirs: list[Path] = []
|
|
14
15
|
|
|
15
|
-
Each skill is a directory containing a SKILL.md file.
|
|
16
|
-
"""
|
|
17
|
-
skills_dirs: dict[str, list[Path]] = {}
|
|
18
|
-
|
|
19
|
-
# Check for plain.* subpackages (including core plain)
|
|
20
16
|
try:
|
|
21
17
|
import plain
|
|
22
18
|
|
|
23
19
|
# Check core plain package (namespace package)
|
|
24
20
|
plain_spec = importlib.util.find_spec("plain")
|
|
25
21
|
if plain_spec and plain_spec.submodule_search_locations:
|
|
26
|
-
# For namespace packages, check all search locations
|
|
27
22
|
for location in plain_spec.submodule_search_locations:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
skill_dirs = [
|
|
33
|
-
d
|
|
34
|
-
for d in skills_dir.iterdir()
|
|
35
|
-
if d.is_dir() and (d / "SKILL.md").exists()
|
|
36
|
-
]
|
|
37
|
-
if skill_dirs:
|
|
38
|
-
skills_dirs["plain"] = skill_dirs
|
|
39
|
-
break # Use the first one found
|
|
23
|
+
agent_dir = Path(location) / "agents" / ".claude"
|
|
24
|
+
if agent_dir.exists() and agent_dir.is_dir():
|
|
25
|
+
agent_dirs.append(agent_dir)
|
|
26
|
+
break
|
|
40
27
|
|
|
41
28
|
# Check other plain.* subpackages
|
|
42
29
|
if hasattr(plain, "__path__"):
|
|
@@ -47,126 +34,142 @@ def _get_packages_with_skills() -> dict[str, list[Path]]:
|
|
|
47
34
|
try:
|
|
48
35
|
spec = importlib.util.find_spec(modname)
|
|
49
36
|
if spec and spec.origin:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if skills_dir.exists() and skills_dir.is_dir():
|
|
54
|
-
# Find subdirectories that contain SKILL.md
|
|
55
|
-
skill_dirs = [
|
|
56
|
-
d
|
|
57
|
-
for d in skills_dir.iterdir()
|
|
58
|
-
if d.is_dir() and (d / "SKILL.md").exists()
|
|
59
|
-
]
|
|
60
|
-
if skill_dirs:
|
|
61
|
-
skills_dirs[modname] = skill_dirs
|
|
37
|
+
agent_dir = Path(spec.origin).parent / "agents" / ".claude"
|
|
38
|
+
if agent_dir.exists() and agent_dir.is_dir():
|
|
39
|
+
agent_dirs.append(agent_dir)
|
|
62
40
|
except Exception:
|
|
63
41
|
continue
|
|
64
42
|
except Exception:
|
|
65
43
|
pass
|
|
66
44
|
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _get_skill_destinations() -> list[Path]:
|
|
71
|
-
"""Get list of skill directories to install to based on what's present."""
|
|
72
|
-
cwd = Path.cwd()
|
|
73
|
-
destinations = []
|
|
45
|
+
return agent_dirs
|
|
74
46
|
|
|
75
|
-
# Check for Claude (.claude/ directory)
|
|
76
|
-
if (cwd / ".claude").exists():
|
|
77
|
-
destinations.append(cwd / ".claude" / "skills")
|
|
78
47
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
destinations.append(cwd / ".codex" / "skills")
|
|
48
|
+
def _install_agent_dir(source_dir: Path, dest_dir: Path) -> tuple[int, int]:
|
|
49
|
+
"""Copy contents of a source agents/.claude/ dir to the project's .claude/ dir.
|
|
82
50
|
|
|
83
|
-
|
|
51
|
+
Handles skills/ subdirectories and rules/ files.
|
|
52
|
+
Returns (installed_count, removed_count) for reporting.
|
|
53
|
+
"""
|
|
54
|
+
installed_count = 0
|
|
84
55
|
|
|
56
|
+
# Copy skills (directories containing SKILL.md)
|
|
57
|
+
source_skills = source_dir / "skills"
|
|
58
|
+
if source_skills.exists():
|
|
59
|
+
dest_skills = dest_dir / "skills"
|
|
60
|
+
dest_skills.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
for skill_dir in source_skills.iterdir():
|
|
62
|
+
if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
|
|
63
|
+
dest_skill = dest_skills / skill_dir.name
|
|
64
|
+
# Check mtime to skip unchanged
|
|
65
|
+
if dest_skill.exists():
|
|
66
|
+
source_mtime = (skill_dir / "SKILL.md").stat().st_mtime
|
|
67
|
+
dest_mtime = (
|
|
68
|
+
(dest_skill / "SKILL.md").stat().st_mtime
|
|
69
|
+
if (dest_skill / "SKILL.md").exists()
|
|
70
|
+
else 0
|
|
71
|
+
)
|
|
72
|
+
if source_mtime <= dest_mtime:
|
|
73
|
+
continue
|
|
74
|
+
shutil.rmtree(dest_skill)
|
|
75
|
+
shutil.copytree(skill_dir, dest_skill)
|
|
76
|
+
installed_count += 1
|
|
77
|
+
|
|
78
|
+
# Copy rules (individual .md files)
|
|
79
|
+
source_rules = source_dir / "rules"
|
|
80
|
+
if source_rules.exists():
|
|
81
|
+
dest_rules = dest_dir / "rules"
|
|
82
|
+
dest_rules.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
for rule_file in source_rules.iterdir():
|
|
84
|
+
if rule_file.is_file() and rule_file.suffix == ".md":
|
|
85
|
+
dest_rule = dest_rules / rule_file.name
|
|
86
|
+
# Check mtime to skip unchanged
|
|
87
|
+
if dest_rule.exists():
|
|
88
|
+
if rule_file.stat().st_mtime <= dest_rule.stat().st_mtime:
|
|
89
|
+
continue
|
|
90
|
+
shutil.copy2(rule_file, dest_rule)
|
|
91
|
+
installed_count += 1
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
dest_skills_dir: Path, skills_by_package: dict[str, list[Path]]
|
|
88
|
-
) -> tuple[int, int]:
|
|
89
|
-
"""Install skills to a destination directory. Returns (installed_count, removed_count)."""
|
|
90
|
-
dest_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
return installed_count, 0
|
|
91
94
|
|
|
92
|
-
# Collect all source skill names
|
|
93
|
-
source_skill_names: set[str] = set()
|
|
94
|
-
for skill_dirs in skills_by_package.values():
|
|
95
|
-
for skill_dir in skill_dirs:
|
|
96
|
-
source_skill_names.add(skill_dir.name)
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
def _cleanup_orphans(dest_dir: Path, agent_dirs: list[Path]) -> int:
|
|
97
|
+
"""Remove plain* items from .claude/ that no longer exist in any source package."""
|
|
99
98
|
removed_count = 0
|
|
100
99
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
# Collect all source skill and rule names
|
|
101
|
+
source_skills: set[str] = set()
|
|
102
|
+
source_rules: set[str] = set()
|
|
103
|
+
for agent_dir in agent_dirs:
|
|
104
|
+
skills_dir = agent_dir / "skills"
|
|
105
|
+
if skills_dir.exists():
|
|
106
|
+
for d in skills_dir.iterdir():
|
|
107
|
+
if d.is_dir() and (d / "SKILL.md").exists():
|
|
108
|
+
source_skills.add(d.name)
|
|
109
|
+
rules_dir = agent_dir / "rules"
|
|
110
|
+
if rules_dir.exists():
|
|
111
|
+
for f in rules_dir.iterdir():
|
|
112
|
+
if f.is_file() and f.suffix == ".md":
|
|
113
|
+
source_rules.add(f.name)
|
|
114
|
+
|
|
115
|
+
# Remove orphaned skills
|
|
116
|
+
dest_skills = dest_dir / "skills"
|
|
117
|
+
if dest_skills.exists():
|
|
118
|
+
for dest in dest_skills.iterdir():
|
|
105
119
|
if (
|
|
106
|
-
|
|
107
|
-
and
|
|
108
|
-
and
|
|
120
|
+
dest.is_dir()
|
|
121
|
+
and dest.name.startswith("plain")
|
|
122
|
+
and dest.name not in source_skills
|
|
109
123
|
):
|
|
110
|
-
shutil.rmtree(
|
|
124
|
+
shutil.rmtree(dest)
|
|
111
125
|
removed_count += 1
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
continue
|
|
126
|
-
|
|
127
|
-
# Copy the entire skill directory
|
|
128
|
-
if dest_dir.exists():
|
|
129
|
-
shutil.rmtree(dest_dir)
|
|
130
|
-
shutil.copytree(skill_dir, dest_dir)
|
|
131
|
-
installed_count += 1
|
|
127
|
+
# Remove orphaned rules
|
|
128
|
+
dest_rules = dest_dir / "rules"
|
|
129
|
+
if dest_rules.exists():
|
|
130
|
+
for dest in dest_rules.iterdir():
|
|
131
|
+
if (
|
|
132
|
+
dest.is_file()
|
|
133
|
+
and dest.name.startswith("plain")
|
|
134
|
+
and dest.suffix == ".md"
|
|
135
|
+
and dest.name not in source_rules
|
|
136
|
+
):
|
|
137
|
+
dest.unlink()
|
|
138
|
+
removed_count += 1
|
|
132
139
|
|
|
133
|
-
return
|
|
140
|
+
return removed_count
|
|
134
141
|
|
|
135
142
|
|
|
136
|
-
def
|
|
137
|
-
"""
|
|
143
|
+
def _cleanup_session_hook(dest_dir: Path) -> None:
|
|
144
|
+
"""Remove the old plain agent context SessionStart hook from settings.json."""
|
|
138
145
|
settings_file = dest_dir / "settings.json"
|
|
139
146
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# Define the Plain hook - calls the agent context command directly
|
|
151
|
-
plain_hook = {
|
|
152
|
-
"matcher": "startup|resume",
|
|
153
|
-
"hooks": [
|
|
154
|
-
{
|
|
155
|
-
"type": "command",
|
|
156
|
-
"command": "uv run plain agent context 2>/dev/null || true",
|
|
157
|
-
}
|
|
158
|
-
],
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
# Get existing SessionStart hooks, remove any existing plain hook
|
|
162
|
-
session_hooks = settings["hooks"].get("SessionStart", [])
|
|
147
|
+
if not settings_file.exists():
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
settings = json.loads(settings_file.read_text())
|
|
151
|
+
|
|
152
|
+
hooks = settings.get("hooks", {})
|
|
153
|
+
session_hooks = hooks.get("SessionStart", [])
|
|
154
|
+
|
|
155
|
+
# Remove any plain agent or plain-context.md hooks
|
|
163
156
|
session_hooks = [h for h in session_hooks if "plain agent" not in str(h)]
|
|
164
|
-
# Also remove old plain-context.md hooks for migration
|
|
165
157
|
session_hooks = [h for h in session_hooks if "plain-context.md" not in str(h)]
|
|
166
|
-
session_hooks.append(plain_hook)
|
|
167
|
-
settings["hooks"]["SessionStart"] = session_hooks
|
|
168
158
|
|
|
169
|
-
|
|
159
|
+
if session_hooks:
|
|
160
|
+
hooks["SessionStart"] = session_hooks
|
|
161
|
+
else:
|
|
162
|
+
hooks.pop("SessionStart", None)
|
|
163
|
+
|
|
164
|
+
if hooks:
|
|
165
|
+
settings["hooks"] = hooks
|
|
166
|
+
else:
|
|
167
|
+
settings.pop("hooks", None)
|
|
168
|
+
|
|
169
|
+
if settings:
|
|
170
|
+
settings_file.write_text(json.dumps(settings, indent=2) + "\n")
|
|
171
|
+
else:
|
|
172
|
+
settings_file.unlink()
|
|
170
173
|
|
|
171
174
|
|
|
172
175
|
@click.group()
|
|
@@ -175,62 +178,57 @@ def agent() -> None:
|
|
|
175
178
|
pass
|
|
176
179
|
|
|
177
180
|
|
|
178
|
-
@agent.command()
|
|
179
|
-
def context() -> None:
|
|
180
|
-
"""Output Plain framework context for AI agents"""
|
|
181
|
-
click.echo("This is a Plain project. Use the /plain-* skills for common tasks.")
|
|
182
|
-
|
|
183
|
-
|
|
184
181
|
@agent.command()
|
|
185
182
|
def install() -> None:
|
|
186
|
-
"""Install skills and
|
|
187
|
-
|
|
183
|
+
"""Install skills and rules to agent directories"""
|
|
184
|
+
cwd = Path.cwd()
|
|
185
|
+
claude_dir = cwd / ".claude"
|
|
188
186
|
|
|
189
|
-
if not
|
|
190
|
-
click.
|
|
187
|
+
if not claude_dir.exists():
|
|
188
|
+
click.secho("No .claude/ directory found.", fg="yellow")
|
|
191
189
|
return
|
|
192
190
|
|
|
193
|
-
|
|
194
|
-
destinations = _get_skill_destinations()
|
|
195
|
-
|
|
196
|
-
if not destinations:
|
|
197
|
-
click.secho(
|
|
198
|
-
"No agent directories found (.claude/ or .codex/)",
|
|
199
|
-
fg="yellow",
|
|
200
|
-
)
|
|
201
|
-
return
|
|
191
|
+
agent_dirs = _get_agent_dirs()
|
|
202
192
|
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
installed_count, removed_count = _install_skills_to(dest, skills_by_package)
|
|
193
|
+
# Clean up orphaned plain-* items
|
|
194
|
+
removed_count = _cleanup_orphans(claude_dir, agent_dirs)
|
|
206
195
|
|
|
207
|
-
|
|
196
|
+
# Install from each package
|
|
197
|
+
total_installed = 0
|
|
198
|
+
for source_dir in agent_dirs:
|
|
199
|
+
installed, _ = _install_agent_dir(source_dir, claude_dir)
|
|
200
|
+
total_installed += installed
|
|
208
201
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
_setup_session_hook(parent_dir)
|
|
202
|
+
# Clean up old session hook
|
|
203
|
+
_cleanup_session_hook(claude_dir)
|
|
212
204
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
click.echo(f"Agent: {', '.join(parts)} in {parent_dir}/")
|
|
205
|
+
parts = []
|
|
206
|
+
if total_installed > 0:
|
|
207
|
+
parts.append(f"installed {total_installed}")
|
|
208
|
+
if removed_count > 0:
|
|
209
|
+
parts.append(f"removed {removed_count}")
|
|
210
|
+
click.echo(f"Agent: {', '.join(parts)} in .claude/") if parts else click.echo(
|
|
211
|
+
"Agent: up to date"
|
|
212
|
+
)
|
|
222
213
|
|
|
223
214
|
|
|
224
215
|
@agent.command()
|
|
225
216
|
def skills() -> None:
|
|
226
217
|
"""List available skills from installed packages"""
|
|
227
|
-
|
|
218
|
+
agent_dirs = _get_agent_dirs()
|
|
219
|
+
|
|
220
|
+
skill_names = []
|
|
221
|
+
for agent_dir in agent_dirs:
|
|
222
|
+
skills_dir = agent_dir / "skills"
|
|
223
|
+
if skills_dir.exists():
|
|
224
|
+
for d in skills_dir.iterdir():
|
|
225
|
+
if d.is_dir() and (d / "SKILL.md").exists():
|
|
226
|
+
skill_names.append(d.name)
|
|
228
227
|
|
|
229
|
-
if not
|
|
228
|
+
if not skill_names:
|
|
230
229
|
click.echo("No skills found in installed packages.")
|
|
231
230
|
return
|
|
232
231
|
|
|
233
232
|
click.echo("Available skills:")
|
|
234
|
-
for
|
|
235
|
-
|
|
236
|
-
click.echo(f" - {skill_dir.name} (from {pkg_name})")
|
|
233
|
+
for name in sorted(skill_names):
|
|
234
|
+
click.echo(f" - {name}")
|
plain/cli/docs.py
CHANGED
|
@@ -1,46 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import importlib.util
|
|
2
|
-
import pkgutil
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import click
|
|
6
7
|
|
|
7
8
|
from .llmdocs import LLMDocs
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
# All known official Plain packages: pip name -> short description
|
|
11
|
+
KNOWN_PACKAGES = {
|
|
12
|
+
"plain": "Web framework core",
|
|
13
|
+
"plain-admin": "Backend admin interface",
|
|
14
|
+
"plain-api": "Class-based API views",
|
|
15
|
+
"plain-auth": "User authentication and authorization",
|
|
16
|
+
"plain-cache": "Database-backed cache with optional expiration",
|
|
17
|
+
"plain-code": "Preconfigured code formatting and linting",
|
|
18
|
+
"plain-dev": "Local development server with auto-reload",
|
|
19
|
+
"plain-elements": "HTML template components",
|
|
20
|
+
"plain-email": "Send email",
|
|
21
|
+
"plain-esbuild": "Build JavaScript with esbuild",
|
|
22
|
+
"plain-flags": "Feature flags via database models",
|
|
23
|
+
"plain-htmx": "HTMX integration for templates and views",
|
|
24
|
+
"plain-jobs": "Background jobs with a database-driven queue",
|
|
25
|
+
"plain-loginlink": "Link-based authentication",
|
|
26
|
+
"plain-models": "Model data and store it in a database",
|
|
27
|
+
"plain-oauth": "OAuth provider login",
|
|
28
|
+
"plain-observer": "On-page telemetry and observability",
|
|
29
|
+
"plain-pages": "Serve static pages, markdown, and assets",
|
|
30
|
+
"plain-pageviews": "Client-side pageview tracking",
|
|
31
|
+
"plain-passwords": "Password authentication",
|
|
32
|
+
"plain-pytest": "Test with pytest",
|
|
33
|
+
"plain-redirection": "URL redirection with admin and logging",
|
|
34
|
+
"plain-scan": "Test for production best practices",
|
|
35
|
+
"plain-sessions": "Database-backed sessions",
|
|
36
|
+
"plain-start": "Bootstrap a new project from templates",
|
|
37
|
+
"plain-support": "Support forms for your application",
|
|
38
|
+
"plain-tailwind": "Tailwind CSS without JavaScript or npm",
|
|
39
|
+
"plain-toolbar": "Debug toolbar",
|
|
40
|
+
"plain-tunnel": "Remote access to local dev server",
|
|
41
|
+
"plain-vendor": "Vendor CDN scripts and styles",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _normalize_module(module: str) -> str:
|
|
46
|
+
"""Normalize a module string to dotted form (e.g. plain-models -> plain.models)."""
|
|
47
|
+
module = module.replace("-", ".")
|
|
48
|
+
if not module.startswith("plain"):
|
|
49
|
+
module = f"plain.{module}"
|
|
50
|
+
return module
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _pip_package_name(module: str) -> str:
|
|
54
|
+
"""Convert a dotted module name to a pip package name (e.g. plain.models -> plain-models)."""
|
|
55
|
+
return module.replace(".", "-")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_installed(module: str) -> bool:
|
|
59
|
+
"""Check if a dotted module name is installed."""
|
|
60
|
+
try:
|
|
61
|
+
return importlib.util.find_spec(module) is not None
|
|
62
|
+
except (ModuleNotFoundError, ValueError):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _online_docs_url(pip_name: str) -> str:
|
|
67
|
+
"""Return the online documentation URL for a package."""
|
|
68
|
+
module = pip_name.replace("-", ".")
|
|
69
|
+
return f"https://plainframework.com/docs/{pip_name}/{module.replace('.', '/')}/"
|
|
9
70
|
|
|
10
71
|
|
|
11
72
|
@click.command()
|
|
12
|
-
@click.option("--
|
|
13
|
-
@click.option("--source", is_flag=True, help="Include symbolicated source code")
|
|
73
|
+
@click.option("--symbols", is_flag=True, help="Show symbolicated API surface only")
|
|
14
74
|
@click.option("--list", "show_list", is_flag=True, help="List available packages")
|
|
15
75
|
@click.argument("module", default="")
|
|
16
|
-
def docs(module: str,
|
|
76
|
+
def docs(module: str, symbols: bool, show_list: bool) -> None:
|
|
17
77
|
"""Show documentation for a package"""
|
|
18
78
|
if show_list:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
plain_spec = importlib.util.find_spec("plain")
|
|
26
|
-
if plain_spec and plain_spec.submodule_search_locations:
|
|
27
|
-
available_packages.append("plain")
|
|
28
|
-
|
|
29
|
-
# Check other plain.* subpackages
|
|
30
|
-
if hasattr(plain, "__path__"):
|
|
31
|
-
for importer, modname, ispkg in pkgutil.iter_modules(
|
|
32
|
-
plain.__path__, "plain."
|
|
33
|
-
):
|
|
34
|
-
if ispkg:
|
|
35
|
-
available_packages.append(modname)
|
|
36
|
-
except Exception:
|
|
37
|
-
pass
|
|
38
|
-
|
|
39
|
-
if available_packages:
|
|
40
|
-
for pkg in sorted(available_packages):
|
|
41
|
-
click.echo(f"- {pkg}")
|
|
42
|
-
else:
|
|
43
|
-
click.echo("No packages found.")
|
|
79
|
+
for pip_name in sorted(KNOWN_PACKAGES):
|
|
80
|
+
description = KNOWN_PACKAGES[pip_name]
|
|
81
|
+
dotted = pip_name.replace("-", ".")
|
|
82
|
+
installed = _is_installed(dotted)
|
|
83
|
+
status = " (installed)" if installed else ""
|
|
84
|
+
click.echo(f" {pip_name}{status} — {description}")
|
|
44
85
|
return
|
|
45
86
|
|
|
46
87
|
if not module:
|
|
@@ -48,32 +89,27 @@ def docs(module: str, open: bool, source: bool, show_list: bool) -> None:
|
|
|
48
89
|
"You must specify a module. Use --list to see available packages."
|
|
49
90
|
)
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
module = module.replace("-", ".")
|
|
53
|
-
|
|
54
|
-
# Automatically prefix if we need to
|
|
55
|
-
if not module.startswith("plain"):
|
|
56
|
-
module = f"plain.{module}"
|
|
92
|
+
module = _normalize_module(module)
|
|
57
93
|
|
|
58
94
|
# Get the module path
|
|
59
95
|
spec = importlib.util.find_spec(module)
|
|
60
96
|
if not spec or not spec.origin:
|
|
61
|
-
|
|
97
|
+
pip_name = _pip_package_name(module)
|
|
98
|
+
if pip_name in KNOWN_PACKAGES:
|
|
99
|
+
msg = (
|
|
100
|
+
f"{module} is not installed.\n\n"
|
|
101
|
+
f" Online docs: {_online_docs_url(pip_name)}"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
msg = f"Module {module} not found. Use --list to see available packages."
|
|
105
|
+
raise click.UsageError(msg)
|
|
62
106
|
|
|
63
107
|
module_path = Path(spec.origin).parent
|
|
64
108
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
readme_path = module_path / "README.md"
|
|
73
|
-
if not readme_path.exists():
|
|
74
|
-
raise click.UsageError(f"README.md not found for {module}")
|
|
75
|
-
|
|
76
|
-
if open:
|
|
77
|
-
click.launch(str(readme_path))
|
|
78
|
-
else:
|
|
79
|
-
click.echo_via_pager(iterate_markdown(readme_path.read_text()))
|
|
109
|
+
llm_docs = LLMDocs([module_path])
|
|
110
|
+
llm_docs.load()
|
|
111
|
+
llm_docs.print(
|
|
112
|
+
relative_to=module_path.parent,
|
|
113
|
+
include_docs=not symbols,
|
|
114
|
+
include_symbols=symbols,
|
|
115
|
+
)
|
plain/cli/llmdocs.py
CHANGED
|
@@ -26,7 +26,7 @@ class LLMDocs:
|
|
|
26
26
|
self.docs.add(path)
|
|
27
27
|
|
|
28
28
|
# Exclude "migrations" code from plain apps, except for plain/models/migrations
|
|
29
|
-
# Also exclude CHANGELOG.md, AGENTS.md, and
|
|
29
|
+
# Also exclude CHANGELOG.md, AGENTS.md, and agents directory
|
|
30
30
|
self.docs = {
|
|
31
31
|
doc
|
|
32
32
|
for doc in self.docs
|
|
@@ -35,7 +35,7 @@ class LLMDocs:
|
|
|
35
35
|
and "/plain/models/migrations/" not in str(doc)
|
|
36
36
|
)
|
|
37
37
|
and doc.name not in ("CHANGELOG.md", "AGENTS.md")
|
|
38
|
-
and "/
|
|
38
|
+
and "/agents/" not in str(doc)
|
|
39
39
|
}
|
|
40
40
|
self.sources = {
|
|
41
41
|
source
|
|
@@ -45,7 +45,7 @@ class LLMDocs:
|
|
|
45
45
|
and "/plain/models/migrations/" not in str(source)
|
|
46
46
|
)
|
|
47
47
|
and source.name != "cli.py"
|
|
48
|
-
and "/
|
|
48
|
+
and "/agents/" not in str(source)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
self.docs = sorted(self.docs)
|
|
@@ -62,28 +62,35 @@ class LLMDocs:
|
|
|
62
62
|
plain_root = Path(*path.parts[: root_index + 1])
|
|
63
63
|
return path.relative_to(plain_root.parent)
|
|
64
64
|
|
|
65
|
-
def print(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
click.secho(f"</Docs: {display_path}>", fg="yellow")
|
|
74
|
-
click.echo()
|
|
75
|
-
|
|
76
|
-
for source in self.sources:
|
|
77
|
-
if symbolicated := self.symbolicate(source):
|
|
65
|
+
def print(
|
|
66
|
+
self,
|
|
67
|
+
relative_to: Path | None = None,
|
|
68
|
+
include_docs: bool = True,
|
|
69
|
+
include_symbols: bool = True,
|
|
70
|
+
) -> None:
|
|
71
|
+
if include_docs:
|
|
72
|
+
for doc in self.docs:
|
|
78
73
|
if relative_to:
|
|
79
|
-
display_path =
|
|
74
|
+
display_path = doc.relative_to(relative_to)
|
|
80
75
|
else:
|
|
81
|
-
display_path = self.display_path(
|
|
82
|
-
click.secho(f"<
|
|
83
|
-
click.echo(
|
|
84
|
-
click.secho(f"</
|
|
76
|
+
display_path = self.display_path(doc)
|
|
77
|
+
click.secho(f"<Docs: {display_path}>", fg="yellow")
|
|
78
|
+
click.echo(doc.read_text())
|
|
79
|
+
click.secho(f"</Docs: {display_path}>", fg="yellow")
|
|
85
80
|
click.echo()
|
|
86
81
|
|
|
82
|
+
if include_symbols:
|
|
83
|
+
for source in self.sources:
|
|
84
|
+
if symbolicated := self.symbolicate(source):
|
|
85
|
+
if relative_to:
|
|
86
|
+
display_path = source.relative_to(relative_to)
|
|
87
|
+
else:
|
|
88
|
+
display_path = self.display_path(source)
|
|
89
|
+
click.secho(f"<Source: {display_path}>", fg="yellow")
|
|
90
|
+
click.echo(symbolicated)
|
|
91
|
+
click.secho(f"</Source: {display_path}>", fg="yellow")
|
|
92
|
+
click.echo()
|
|
93
|
+
|
|
87
94
|
@staticmethod
|
|
88
95
|
def symbolicate(file_path: Path) -> str:
|
|
89
96
|
if "internal" in str(file_path).split("/"):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
plain/CHANGELOG.md,sha256=
|
|
1
|
+
plain/CHANGELOG.md,sha256=FDaTDlnefZUH4Yq9XefuoXUf3CLLA0QKAM6Y0ln_q8I,57385
|
|
2
2
|
plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
|
|
3
3
|
plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
|
|
4
4
|
plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
|
|
@@ -8,6 +8,9 @@ plain/paginator.py,sha256=uModWWSnXISNt1Ecb5C17yoaKiTcHltFHLrfqQ-lio0,6240
|
|
|
8
8
|
plain/signing.py,sha256=aFwfZew9Ot7_26F88wSOU82MdNUQ3i1A7kI1qKS1q-4,11634
|
|
9
9
|
plain/validators.py,sha256=JBeycZFilWs69ai-QQb_4snpDgs1fjMGT0ICwVB1mvE,21297
|
|
10
10
|
plain/wsgi.py,sha256=MWQ09DFNV2GxX752hYJMWka9LTEwzCp6XEC_4TdXF9g,287
|
|
11
|
+
plain/agents/.claude/rules/plain.md,sha256=CgNYXI1pA3DWZv1T5iuempiMIJLD8w9VWbYDlJNuXNI,3617
|
|
12
|
+
plain/agents/.claude/skills/plain-install/SKILL.md,sha256=lPhUMmKAQpjrVMp73T5pBkwkIaJjWTxAqtyS8Hio8mM,746
|
|
13
|
+
plain/agents/.claude/skills/plain-upgrade/SKILL.md,sha256=79otYHv68Tfk3j08kRbg7o9eiNzzFZFyXCfLuIlT2Ho,927
|
|
11
14
|
plain/assets/README.md,sha256=iRZHHoXZCEFooXoDYSomF12XTcDFl7oNbRHpAFwxTM8,4349
|
|
12
15
|
plain/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
16
|
plain/assets/compile.py,sha256=a-e_nKHJLN3fOp9nw1MF7f7oP6q5qX8oYelowXKb_EE,3587
|
|
@@ -21,15 +24,15 @@ plain/chores/core.py,sha256=BxsCSJDQvMjYsAH4QhEoW9ZUEAUIwJgTYyHovPGSLjk,578
|
|
|
21
24
|
plain/chores/registry.py,sha256=IRpx3f6Z1qlqcEpHTe6O6JocNmaplLu7BVOqGrafSXU,1221
|
|
22
25
|
plain/cli/README.md,sha256=8OmOhvKvgFkndwGn8lW6q62mvIB6Zq_NAhEhBf7PVV0,6305
|
|
23
26
|
plain/cli/__init__.py,sha256=o-dmnnNmXM3fZrKwW1qcoAPlcfG5pcFTFYWEBCocf8A,117
|
|
24
|
-
plain/cli/agent.py,sha256=
|
|
27
|
+
plain/cli/agent.py,sha256=ZjCmeroPhX8Dc9Y0J_-ePzdZIZ4kicC3adsV6GL4xNI,7683
|
|
25
28
|
plain/cli/build.py,sha256=UxgyAris4xtOZYS4FLMIOQLQ9DflHZkkenUJ650kZuA,3142
|
|
26
29
|
plain/cli/changelog.py,sha256=ckuP99HtnUSJXf_AELWgmD9S0XjldCzh5urxPiDpRcI,3647
|
|
27
30
|
plain/cli/chores.py,sha256=jhLKxFs_YpyQDvo5chNGiKnFc2_KK5atq53EpkGmMo0,2542
|
|
28
31
|
plain/cli/core.py,sha256=_Kg_YnYeDZB3EKlzdejl4eJepzRd2aF1LSs0Ei2uCI8,6923
|
|
29
|
-
plain/cli/docs.py,sha256=
|
|
32
|
+
plain/cli/docs.py,sha256=XeLt_Zf43NZV4yCkj0nqYcT5ejY7NR_NvyouClZTL0c,4321
|
|
30
33
|
plain/cli/formatting.py,sha256=t0kxJ-r-his8kGcY38g38p699CfFGUgqEI1tJkEbjGs,3802
|
|
31
34
|
plain/cli/install.py,sha256=28lXobloqqN50duuKlUqalAmNHUJdz-JlYYmdpXpzCs,1260
|
|
32
|
-
plain/cli/llmdocs.py,sha256=
|
|
35
|
+
plain/cli/llmdocs.py,sha256=05lMHg6pYUb4zi6CTW5DDAidHFOkZWfG4ReMgUB42aM,5824
|
|
33
36
|
plain/cli/output.py,sha256=uZTHZR-Axeoi2r6fgcDCpDA7iQSRrktBtTf1yBT5djI,1426
|
|
34
37
|
plain/cli/preflight.py,sha256=zWnw9RFmN0CQQByAB2vWLbwlDNrQzUQTtKKySVzRucE,7036
|
|
35
38
|
plain/cli/print.py,sha256=s_yNxtA4vg-AWn4C9TtFH-gOMnzMsXuEr7ak4-oOFPI,265
|
|
@@ -133,12 +136,6 @@ plain/signals/__init__.py,sha256=VDhotllLUQVg3eA1LuAJM9pwGTaf_bzRGzc4mJhd-sY,98
|
|
|
133
136
|
plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
|
|
134
137
|
plain/signals/dispatch/dispatcher.py,sha256=ofpu8wMEx8O-I9SO9Rkk1ABH09-71QnmGZlt8vsZAw8,12317
|
|
135
138
|
plain/signals/dispatch/license.txt,sha256=o9EhDhsC4Q5HbmD-IfNGVTEkXtNE33r5rIt3lleJ8gc,1727
|
|
136
|
-
plain/skills/README.md,sha256=WUhXiLU42pIdcXiT-zugnsOXj4OjiCmec_XcM85KAyo,1434
|
|
137
|
-
plain/skills/plain-docs/SKILL.md,sha256=CoOEhTsOpNczBuGxnHbGIVZ9Yahreq0RoXzqhVwtajY,560
|
|
138
|
-
plain/skills/plain-install/SKILL.md,sha256=lPhUMmKAQpjrVMp73T5pBkwkIaJjWTxAqtyS8Hio8mM,746
|
|
139
|
-
plain/skills/plain-request/SKILL.md,sha256=C37GEWRIpJFr-fbnEb08ENOJg-A7WUZxoYPneFAkHO4,771
|
|
140
|
-
plain/skills/plain-shell/SKILL.md,sha256=njxD1dXVmSVZkcUnEm9_62hn_P6JMQmCpVOEzq29RvY,400
|
|
141
|
-
plain/skills/plain-upgrade/SKILL.md,sha256=79otYHv68Tfk3j08kRbg7o9eiNzzFZFyXCfLuIlT2Ho,927
|
|
142
139
|
plain/templates/README.md,sha256=zhAayenfxtwq6r1-NjoaD-wjaazyhm76BCacGF24ACc,8803
|
|
143
140
|
plain/templates/__init__.py,sha256=bX76FakE9T7mfK3N0deN85HlwHNQpeigytSC9Z8LcOs,451
|
|
144
141
|
plain/templates/core.py,sha256=mbcH0yTeFOI3XOg9dYSroXRIcdv9sETEy4HzY-ugwco,1258
|
|
@@ -194,8 +191,8 @@ plain/views/forms.py,sha256=j5WAMLeW1efU9M0q1W6SFHMDMBZfkFfW4WRLKwrXjqk,2435
|
|
|
194
191
|
plain/views/objects.py,sha256=qKK8lYQKK7DTBPrMUebZ2HevgU9MmyWeXFWG1lT5ZbM,5393
|
|
195
192
|
plain/views/redirect.py,sha256=rt5RF1Rs2yYFLole3Mznu5iU9aeumu49VAaX4WCd_xk,2139
|
|
196
193
|
plain/views/templates.py,sha256=ElyqgpbkoJt72yU1gF7b626TB3R8viDAf-LYloulUBA,1925
|
|
197
|
-
plain-0.
|
|
198
|
-
plain-0.
|
|
199
|
-
plain-0.
|
|
200
|
-
plain-0.
|
|
201
|
-
plain-0.
|
|
194
|
+
plain-0.103.0.dist-info/METADATA,sha256=SQquYimnpZsPSt2gxBB7kw08UrCwGUgqYq0btTNOP34,4550
|
|
195
|
+
plain-0.103.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
196
|
+
plain-0.103.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
|
|
197
|
+
plain-0.103.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
|
198
|
+
plain-0.103.0.dist-info/RECORD,,
|
plain/skills/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# plain.skills
|
|
2
|
-
|
|
3
|
-
**Agent skills for working with Plain projects.**
|
|
4
|
-
|
|
5
|
-
These skills provide context and workflows for common tasks when using [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [Codex](https://codex.openai.com/).
|
|
6
|
-
|
|
7
|
-
## Available skills
|
|
8
|
-
|
|
9
|
-
| Skill | Description |
|
|
10
|
-
| --------------- | -------------------------------------------------------------- |
|
|
11
|
-
| `plain-docs` | Retrieves detailed documentation for Plain packages |
|
|
12
|
-
| `plain-install` | Installs Plain packages and guides through setup steps |
|
|
13
|
-
| `plain-upgrade` | Upgrades Plain packages and applies required migration changes |
|
|
14
|
-
| `plain-shell` | Runs Python with Plain configured and database access |
|
|
15
|
-
| `plain-request` | Makes test HTTP requests against the development database |
|
|
16
|
-
|
|
17
|
-
## Installation
|
|
18
|
-
|
|
19
|
-
To install skills to your project's `.claude/` or `.codex/` directory:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
uv run plain agent install
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
This command:
|
|
26
|
-
|
|
27
|
-
- Copies skill definitions so your agent can use them
|
|
28
|
-
- Sets up a `SessionStart` hook that runs `plain agent context` at the start of every session
|
|
29
|
-
|
|
30
|
-
Run it again after upgrading Plain to get updated skills.
|
|
31
|
-
|
|
32
|
-
## Commands
|
|
33
|
-
|
|
34
|
-
- `plain agent install` - Install skills and set up hooks
|
|
35
|
-
- `plain agent skills` - List available skills from installed packages
|
|
36
|
-
- `plain agent context` - Output framework context (used by the SessionStart hook)
|
plain/skills/plain-docs/SKILL.md
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plain-docs
|
|
3
|
-
description: Retrieves detailed documentation for Plain packages. Use when looking up package APIs or feature details.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Getting Documentation
|
|
7
|
-
|
|
8
|
-
## List Available Packages
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
uv run plain docs --list
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Get Package Documentation
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
uv run plain docs <package> --source
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Examples:
|
|
21
|
-
|
|
22
|
-
- `uv run plain docs models --source` - Models and database
|
|
23
|
-
- `uv run plain docs templates --source` - Jinja2 templates
|
|
24
|
-
- `uv run plain docs assets --source` - Static assets
|
|
25
|
-
- `uv run plain docs tailwind --source` - Tailwind CSS integration
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plain-request
|
|
3
|
-
description: Makes HTTP requests to test URLs, check endpoints, fetch pages, or debug routes. Use when asked to look at a URL, hit an endpoint, test a route, or make GET/POST requests.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Making HTTP Requests
|
|
7
|
-
|
|
8
|
-
Use `uv run plain request` to make test requests against the dev database.
|
|
9
|
-
|
|
10
|
-
## Basic Usage
|
|
11
|
-
|
|
12
|
-
```
|
|
13
|
-
uv run plain request /path
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## With Authentication
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
uv run plain request /path --user 1
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## With Custom Headers
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
uv run plain request /path --header "Accept: application/json"
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## POST/PUT/PATCH with Data
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
uv run plain request /path --method POST --data '{"key": "value"}'
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Limiting Output
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
uv run plain request /path --no-body # Headers only
|
|
38
|
-
uv run plain request /path --no-headers # Body only
|
|
39
|
-
```
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plain-shell
|
|
3
|
-
description: Runs Python with Plain configured and database access. Use for scripts, one-off commands, or interactive sessions.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Running Python with Plain
|
|
7
|
-
|
|
8
|
-
## Interactive Shell
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
uv run plain shell
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## One-off Command
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
uv run plain shell -c "from app.users.models import User; print(User.query.count())"
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Run a Script
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
uv run plain run script.py
|
|
24
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|