gl-cursor-skill 0.1.0__tar.gz

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.
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: Test and build
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Check out repository
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
+ with:
22
+ enable-cache: true
23
+
24
+ - name: Install Python
25
+ run: uv python install 3.13
26
+
27
+ - name: Sync dependencies
28
+ run: uv sync --locked --dev
29
+
30
+ - name: Run tests
31
+ run: uv run pytest
32
+
33
+ - name: Build package
34
+ run: uv build
@@ -0,0 +1,75 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: ["published"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ name: Build distributions
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Check out repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v6
20
+ with:
21
+ enable-cache: true
22
+
23
+ - name: Install Python
24
+ run: uv python install 3.13
25
+
26
+ - name: Sync dependencies
27
+ run: uv sync --locked --dev
28
+
29
+ - name: Check release tag matches project version
30
+ if: github.event_name == 'release'
31
+ run: |
32
+ python - <<'PY'
33
+ import os
34
+ import tomllib
35
+
36
+ tag = os.environ["GITHUB_REF_NAME"]
37
+ version = tomllib.loads(open("pyproject.toml", "rb").read().decode())["project"]["version"]
38
+ expected = f"v{version}"
39
+ if tag != expected:
40
+ raise SystemExit(f"Release tag {tag!r} does not match pyproject version {version!r}; expected {expected!r}")
41
+ PY
42
+
43
+ - name: Run tests
44
+ run: uv run pytest
45
+
46
+ - name: Build package
47
+ run: uv build
48
+
49
+ - name: Upload distributions
50
+ uses: actions/upload-artifact@v4
51
+ with:
52
+ name: distributions
53
+ path: dist/*
54
+ if-no-files-found: error
55
+
56
+ publish:
57
+ name: Publish distributions to PyPI
58
+ needs: build
59
+ runs-on: ubuntu-latest
60
+ environment: pypi
61
+ permissions:
62
+ contents: read
63
+ id-token: write
64
+ steps:
65
+ - name: Download distributions
66
+ uses: actions/download-artifact@v4
67
+ with:
68
+ name: distributions
69
+ path: dist/
70
+
71
+ - name: Install uv
72
+ uses: astral-sh/setup-uv@v6
73
+
74
+ - name: Publish package
75
+ run: uv publish
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: gl-cursor-skill
3
+ Version: 0.1.0
4
+ Summary: Move and install AI agent skills between Claude, Codex, Cursor, and project scopes.
5
+ Project-URL: Homepage, https://pypi.org/project/gl-cursor-skill/
6
+ Author: gulei
7
+ License-Expression: MIT
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: questionary>=2.1.0
10
+ Requires-Dist: rich>=13.7.0
11
+ Requires-Dist: typer>=0.16.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # gl-cursor-skill
15
+
16
+ `gl-cursor-skill` installs AI-agent skills between common local skill folders.
17
+
18
+ Primary usage:
19
+
20
+ ```bash
21
+ uvx gl-cursor-skill add cursor-agent
22
+ ```
23
+
24
+ When options are omitted in an interactive terminal, the CLI asks where to install:
25
+
26
+ ```text
27
+ ? Install for which agent?
28
+ Codex
29
+ Cursor
30
+ Claude Code
31
+
32
+ ? Install scope?
33
+ Global
34
+ Project
35
+ ```
36
+
37
+ For scripts, pass the choices explicitly:
38
+
39
+ ```bash
40
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --yes
41
+ ```
42
+
43
+ ## Supported Targets
44
+
45
+ | Agent | Global target | Project target |
46
+ | --- | --- | --- |
47
+ | Codex | `~/.codex/skills` | `.codex/skills` |
48
+ | Claude Code | `~/.claude/skills` | `.claude/skills` |
49
+ | Cursor | `~/.cursor/skills` | `.cursor/skills` |
50
+
51
+ `claude` and `claudecode` are accepted aliases for `claude-code`.
52
+
53
+ ## Commands
54
+
55
+ ```bash
56
+ gl-cursor-skill add <skill>
57
+ gl-cursor-skill agents
58
+ gl-cursor-skill list
59
+ ```
60
+
61
+ Examples:
62
+
63
+ ```bash
64
+ # Interactive install
65
+ uvx gl-cursor-skill add cursor-agent
66
+
67
+ # Non-interactive install from Claude global skills into Codex global skills
68
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --yes
69
+
70
+ # Install from an explicit local path
71
+ uvx gl-cursor-skill add /path/to/cursor-agent --agent codex --scope global --yes
72
+
73
+ # Preview without copying files
74
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --dry-run --yes
75
+
76
+ # Overwrite an existing target skill
77
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --force --yes
78
+ ```
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ uv sync
84
+ uv run pytest
85
+ uv run gl-cursor-skill --help
86
+ uv build
87
+ ```
88
+
89
+ ## Release From GitHub
90
+
91
+ The repository includes GitHub Actions workflows:
92
+
93
+ - `.github/workflows/ci.yml` runs tests and builds the package on push and pull requests.
94
+ - `.github/workflows/publish.yml` publishes to PyPI when a GitHub Release is published.
95
+
96
+ Recommended setup:
97
+
98
+ 1. Create a GitHub repository and push this project.
99
+ 2. On PyPI, configure a Trusted Publisher for project `gl-cursor-skill`.
100
+ 3. Use these PyPI Trusted Publisher values:
101
+
102
+ ```text
103
+ Owner: <your GitHub username or org>
104
+ Repository name: <your repo name>
105
+ Workflow name: publish.yml
106
+ Environment name: pypi
107
+ ```
108
+
109
+ 4. For each release, bump the version in `pyproject.toml`, tag the commit as `vX.Y.Z`, and publish a GitHub Release from that tag.
110
+
111
+ The workflow checks that the release tag matches the package version. For example, version `0.1.0` must be released with tag `v0.1.0`.
112
+
113
+ Manual local publish also works:
114
+
115
+ ```bash
116
+ UV_PUBLISH_TOKEN="pypi-..." uv publish
117
+ ```
118
+
119
+ If you configure PyPI Trusted Publishing in CI, `uv publish` can run without a token.
@@ -0,0 +1,106 @@
1
+ # gl-cursor-skill
2
+
3
+ `gl-cursor-skill` installs AI-agent skills between common local skill folders.
4
+
5
+ Primary usage:
6
+
7
+ ```bash
8
+ uvx gl-cursor-skill add cursor-agent
9
+ ```
10
+
11
+ When options are omitted in an interactive terminal, the CLI asks where to install:
12
+
13
+ ```text
14
+ ? Install for which agent?
15
+ Codex
16
+ Cursor
17
+ Claude Code
18
+
19
+ ? Install scope?
20
+ Global
21
+ Project
22
+ ```
23
+
24
+ For scripts, pass the choices explicitly:
25
+
26
+ ```bash
27
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --yes
28
+ ```
29
+
30
+ ## Supported Targets
31
+
32
+ | Agent | Global target | Project target |
33
+ | --- | --- | --- |
34
+ | Codex | `~/.codex/skills` | `.codex/skills` |
35
+ | Claude Code | `~/.claude/skills` | `.claude/skills` |
36
+ | Cursor | `~/.cursor/skills` | `.cursor/skills` |
37
+
38
+ `claude` and `claudecode` are accepted aliases for `claude-code`.
39
+
40
+ ## Commands
41
+
42
+ ```bash
43
+ gl-cursor-skill add <skill>
44
+ gl-cursor-skill agents
45
+ gl-cursor-skill list
46
+ ```
47
+
48
+ Examples:
49
+
50
+ ```bash
51
+ # Interactive install
52
+ uvx gl-cursor-skill add cursor-agent
53
+
54
+ # Non-interactive install from Claude global skills into Codex global skills
55
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --yes
56
+
57
+ # Install from an explicit local path
58
+ uvx gl-cursor-skill add /path/to/cursor-agent --agent codex --scope global --yes
59
+
60
+ # Preview without copying files
61
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --dry-run --yes
62
+
63
+ # Overwrite an existing target skill
64
+ uvx gl-cursor-skill add cursor-agent --agent codex --scope global --from claude --force --yes
65
+ ```
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ uv sync
71
+ uv run pytest
72
+ uv run gl-cursor-skill --help
73
+ uv build
74
+ ```
75
+
76
+ ## Release From GitHub
77
+
78
+ The repository includes GitHub Actions workflows:
79
+
80
+ - `.github/workflows/ci.yml` runs tests and builds the package on push and pull requests.
81
+ - `.github/workflows/publish.yml` publishes to PyPI when a GitHub Release is published.
82
+
83
+ Recommended setup:
84
+
85
+ 1. Create a GitHub repository and push this project.
86
+ 2. On PyPI, configure a Trusted Publisher for project `gl-cursor-skill`.
87
+ 3. Use these PyPI Trusted Publisher values:
88
+
89
+ ```text
90
+ Owner: <your GitHub username or org>
91
+ Repository name: <your repo name>
92
+ Workflow name: publish.yml
93
+ Environment name: pypi
94
+ ```
95
+
96
+ 4. For each release, bump the version in `pyproject.toml`, tag the commit as `vX.Y.Z`, and publish a GitHub Release from that tag.
97
+
98
+ The workflow checks that the release tag matches the package version. For example, version `0.1.0` must be released with tag `v0.1.0`.
99
+
100
+ Manual local publish also works:
101
+
102
+ ```bash
103
+ UV_PUBLISH_TOKEN="pypi-..." uv publish
104
+ ```
105
+
106
+ If you configure PyPI Trusted Publishing in CI, `uv publish` can run without a token.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "gl-cursor-skill"
3
+ version = "0.1.0"
4
+ description = "Move and install AI agent skills between Claude, Codex, Cursor, and project scopes."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [
9
+ { name = "gulei" }
10
+ ]
11
+ dependencies = [
12
+ "questionary>=2.1.0",
13
+ "rich>=13.7.0",
14
+ "typer>=0.16.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://pypi.org/project/gl-cursor-skill/"
19
+
20
+ [project.scripts]
21
+ gl-cursor-skill = "gl_cursor_skill.cli:main"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "pytest>=8.2.0",
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["hatchling>=1.26"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Tools for installing local AI-agent skills."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class AgentProfile:
9
+ key: str
10
+ label: str
11
+ global_dir: Path
12
+ project_dir: Path
13
+ aliases: tuple[str, ...] = ()
14
+
15
+
16
+ def home() -> Path:
17
+ return Path.home()
18
+
19
+
20
+ AGENTS: tuple[AgentProfile, ...] = (
21
+ AgentProfile(
22
+ key="codex",
23
+ label="Codex",
24
+ global_dir=home() / ".codex" / "skills",
25
+ project_dir=Path(".codex") / "skills",
26
+ ),
27
+ AgentProfile(
28
+ key="claude-code",
29
+ label="Claude Code",
30
+ global_dir=home() / ".claude" / "skills",
31
+ project_dir=Path(".claude") / "skills",
32
+ aliases=("claude", "claudecode", "claude_code"),
33
+ ),
34
+ AgentProfile(
35
+ key="cursor",
36
+ label="Cursor",
37
+ global_dir=home() / ".cursor" / "skills",
38
+ project_dir=Path(".cursor") / "skills",
39
+ ),
40
+ )
41
+
42
+
43
+ def normalize_agent(value: str) -> AgentProfile:
44
+ wanted = value.strip().lower().replace(" ", "-")
45
+ for agent in AGENTS:
46
+ names = (agent.key, *agent.aliases)
47
+ if wanted in names:
48
+ return agent
49
+ valid = ", ".join(agent.key for agent in AGENTS)
50
+ raise ValueError(f"Unknown agent '{value}'. Expected one of: {valid}")
51
+
52
+
53
+ def agent_choices() -> list[tuple[str, str]]:
54
+ return [(agent.label, agent.key) for agent in AGENTS]
@@ -0,0 +1,267 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import sys
5
+ from typing import Optional
6
+
7
+ import questionary
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ import typer
11
+
12
+ from . import __version__
13
+ from .agents import AGENTS, agent_choices, normalize_agent
14
+ from .core import (
15
+ SkillCandidate,
16
+ SkillError,
17
+ build_install_plan,
18
+ install_skill,
19
+ known_source_dirs,
20
+ normalize_scope,
21
+ resolve_source_candidates,
22
+ )
23
+
24
+
25
+ app = typer.Typer(
26
+ add_completion=False,
27
+ help="Install AI-agent skills between Claude, Codex, Cursor, and project scopes.",
28
+ no_args_is_help=True,
29
+ )
30
+ console = Console()
31
+
32
+
33
+ def _version_callback(value: bool) -> None:
34
+ if value:
35
+ console.print(f"gl-cursor-skill {__version__}")
36
+ raise typer.Exit()
37
+
38
+
39
+ @app.callback()
40
+ def callback(
41
+ version: bool = typer.Option(
42
+ False,
43
+ "--version",
44
+ help="Show the installed gl-cursor-skill version.",
45
+ callback=_version_callback,
46
+ is_eager=True,
47
+ ),
48
+ ) -> None:
49
+ pass
50
+
51
+
52
+ @app.command()
53
+ def agents() -> None:
54
+ """Show known agent skill directories."""
55
+ table = Table(title="Known agent skill directories")
56
+ table.add_column("Agent")
57
+ table.add_column("Global")
58
+ table.add_column("Project")
59
+
60
+ for agent in AGENTS:
61
+ table.add_row(agent.label, str(agent.global_dir), str(agent.project_dir))
62
+
63
+ console.print(table)
64
+
65
+
66
+ @app.command(name="list")
67
+ def list_skills(
68
+ project_dir: Optional[Path] = typer.Option(
69
+ None,
70
+ "--project-dir",
71
+ "-p",
72
+ help="Project root used when listing project skill folders.",
73
+ ),
74
+ ) -> None:
75
+ """List discovered local skills."""
76
+ table = Table(title="Discovered skills")
77
+ table.add_column("Source")
78
+ table.add_column("Skill")
79
+ table.add_column("Path")
80
+
81
+ found_any = False
82
+ for base in known_source_dirs(project_dir):
83
+ if not base.path.is_dir():
84
+ continue
85
+ for child in sorted(base.path.iterdir(), key=lambda item: item.name.lower()):
86
+ if child.is_dir() and (child / "SKILL.md").is_file():
87
+ found_any = True
88
+ table.add_row(base.label, child.name, str(child))
89
+
90
+ if found_any:
91
+ console.print(table)
92
+ else:
93
+ console.print("[yellow]No skills found in known directories.[/yellow]")
94
+
95
+
96
+ @app.command()
97
+ def add(
98
+ skill: Optional[str] = typer.Argument(
99
+ None,
100
+ help="Skill name or path to a skill directory containing SKILL.md.",
101
+ ),
102
+ agent: Optional[str] = typer.Option(
103
+ None,
104
+ "--agent",
105
+ "-a",
106
+ help="Target agent: codex, cursor, or claude-code.",
107
+ ),
108
+ scope: Optional[str] = typer.Option(
109
+ None,
110
+ "--scope",
111
+ "-s",
112
+ help="Install scope: global or project.",
113
+ ),
114
+ source: Optional[str] = typer.Option(
115
+ None,
116
+ "--from",
117
+ "-f",
118
+ help="Source agent or path. Examples: claude, codex, cursor, /path/to/skill.",
119
+ ),
120
+ project_dir: Optional[Path] = typer.Option(
121
+ None,
122
+ "--project-dir",
123
+ "-p",
124
+ help="Project root used for project scope and project-source discovery.",
125
+ ),
126
+ dest: Optional[Path] = typer.Option(
127
+ None,
128
+ "--dest",
129
+ help="Override the destination skills directory.",
130
+ ),
131
+ force: bool = typer.Option(
132
+ False,
133
+ "--force",
134
+ help="Overwrite the target skill if it already exists.",
135
+ ),
136
+ dry_run: bool = typer.Option(
137
+ False,
138
+ "--dry-run",
139
+ help="Print the planned install without copying files.",
140
+ ),
141
+ yes: bool = typer.Option(
142
+ False,
143
+ "--yes",
144
+ "-y",
145
+ help="Do not prompt; use defaults where possible.",
146
+ ),
147
+ ) -> None:
148
+ """Install a skill into an agent's global or project skill directory."""
149
+ interactive = _can_prompt(yes)
150
+ project_root = project_dir.expanduser() if project_dir is not None else Path.cwd()
151
+
152
+ try:
153
+ skill_name = _resolve_skill_arg(skill, interactive)
154
+ target_agent = normalize_agent(agent) if agent else _choose_agent(interactive, yes)
155
+ target_scope = normalize_scope(scope) if scope else _choose_scope(interactive, yes)
156
+ source_dir = _choose_source(skill_name, source, project_root, interactive, yes)
157
+ plan = build_install_plan(source_dir, target_agent, target_scope, project_root, dest)
158
+ _print_plan(plan, dry_run, force)
159
+ install_skill(plan, force=force, dry_run=dry_run)
160
+ except (SkillError, ValueError) as error:
161
+ console.print(f"[red]Error:[/red] {error}")
162
+ raise typer.Exit(code=1) from error
163
+
164
+ if dry_run:
165
+ console.print("[cyan]Dry run complete. No files were changed.[/cyan]")
166
+ else:
167
+ console.print(f"[green]Installed[/green] {plan.skill_name} -> {plan.target_dir}")
168
+ console.print("Restart the target agent if it does not pick up new skills automatically.")
169
+
170
+
171
+ def _can_prompt(yes: bool) -> bool:
172
+ return not yes and sys.stdin.isatty() and sys.stdout.isatty()
173
+
174
+
175
+ def _resolve_skill_arg(skill: Optional[str], interactive: bool) -> str:
176
+ if skill:
177
+ return skill
178
+ if not interactive:
179
+ raise SkillError("Skill is required in non-interactive mode.")
180
+ answer = questionary.text("Skill name or path:").ask()
181
+ if not answer:
182
+ raise SkillError("Skill is required.")
183
+ return answer
184
+
185
+
186
+ def _choose_agent(interactive: bool, yes: bool):
187
+ if yes:
188
+ return normalize_agent("codex")
189
+ if not interactive:
190
+ raise SkillError("Missing --agent in non-interactive mode.")
191
+
192
+ choices = [questionary.Choice(title=label, value=value) for label, value in agent_choices()]
193
+ answer = questionary.select("Install for which agent?", choices=choices).ask()
194
+ if not answer:
195
+ raise SkillError("Target agent is required.")
196
+ return normalize_agent(answer)
197
+
198
+
199
+ def _choose_scope(interactive: bool, yes: bool) -> str:
200
+ if yes:
201
+ return "global"
202
+ if not interactive:
203
+ raise SkillError("Missing --scope in non-interactive mode.")
204
+
205
+ answer = questionary.select(
206
+ "Install scope?",
207
+ choices=[
208
+ questionary.Choice(title="Global", value="global"),
209
+ questionary.Choice(title="Project", value="project"),
210
+ ],
211
+ ).ask()
212
+ if not answer:
213
+ raise SkillError("Scope is required.")
214
+ return normalize_scope(answer)
215
+
216
+
217
+ def _choose_source(
218
+ skill: str,
219
+ source: Optional[str],
220
+ project_root: Path,
221
+ interactive: bool,
222
+ yes: bool,
223
+ ) -> Path:
224
+ candidates = resolve_source_candidates(skill, source, project_root)
225
+ if not candidates:
226
+ hint = f" from '{source}'" if source else ""
227
+ raise SkillError(f"Could not find skill '{skill}'{hint}.")
228
+
229
+ if len(candidates) == 1:
230
+ return candidates[0].path
231
+
232
+ if yes:
233
+ return candidates[0].path
234
+
235
+ if not interactive:
236
+ lines = "\n".join(f"- {candidate.label}: {candidate.path}" for candidate in candidates)
237
+ raise SkillError(f"Multiple source skills found; pass --from or run interactively.\n{lines}")
238
+
239
+ choices = [
240
+ questionary.Choice(title=f"{candidate.label}: {candidate.path}", value=str(index))
241
+ for index, candidate in enumerate(candidates)
242
+ ]
243
+ answer = questionary.select("Install from which source?", choices=choices).ask()
244
+ if answer is None:
245
+ raise SkillError("Source selection is required.")
246
+ return candidates[int(answer)].path
247
+
248
+
249
+ def _print_plan(plan, dry_run: bool, force: bool) -> None:
250
+ table = Table(title="Install plan")
251
+ table.add_column("Field")
252
+ table.add_column("Value")
253
+ table.add_row("Skill", plan.skill_name)
254
+ table.add_row("Target agent", plan.target_agent.label)
255
+ table.add_row("Scope", plan.scope)
256
+ table.add_row("Source", str(plan.source_dir))
257
+ table.add_row("Destination", str(plan.target_dir))
258
+ table.add_row("Target exists", "yes" if plan.target_dir.exists() else "no")
259
+ table.add_row("Mode", "dry-run" if dry_run else "copy")
260
+ table.add_row("Overwrite", "yes" if force else "no")
261
+ console.print(table)
262
+ if dry_run and plan.target_dir.exists() and not force:
263
+ console.print("[yellow]Target already exists; actual install would need --force.[/yellow]")
264
+
265
+
266
+ def main() -> None:
267
+ app()
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import shutil
6
+
7
+ from .agents import AGENTS, AgentProfile, normalize_agent
8
+
9
+
10
+ class SkillError(RuntimeError):
11
+ pass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class SkillCandidate:
16
+ path: Path
17
+ label: str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class InstallPlan:
22
+ source_dir: Path
23
+ target_dir: Path
24
+ skill_name: str
25
+ target_agent: AgentProfile
26
+ scope: str
27
+
28
+
29
+ def find_skill_file(path: Path) -> Path | None:
30
+ if path.is_file() and path.name.lower() == "skill.md":
31
+ return path
32
+ if not path.is_dir():
33
+ return None
34
+ for name in ("SKILL.md", "skill.md"):
35
+ candidate = path / name
36
+ if candidate.is_file():
37
+ return candidate
38
+ return None
39
+
40
+
41
+ def ensure_skill_dir(path: Path) -> Path:
42
+ expanded = path.expanduser()
43
+ if expanded.is_file() and expanded.name.lower() == "skill.md":
44
+ expanded = expanded.parent
45
+ skill_file = find_skill_file(expanded)
46
+ if skill_file is None:
47
+ raise SkillError(f"Not a skill directory: {expanded}")
48
+ return expanded
49
+
50
+
51
+ def target_base_dir(
52
+ agent: AgentProfile,
53
+ scope: str,
54
+ project_root: Path | None = None,
55
+ explicit_dest: Path | None = None,
56
+ ) -> Path:
57
+ if explicit_dest is not None:
58
+ return explicit_dest.expanduser()
59
+
60
+ normalized_scope = normalize_scope(scope)
61
+ if normalized_scope == "global":
62
+ return agent.global_dir
63
+
64
+ root = project_root.expanduser() if project_root is not None else Path.cwd()
65
+ return root / agent.project_dir
66
+
67
+
68
+ def normalize_scope(value: str) -> str:
69
+ normalized = value.strip().lower()
70
+ if normalized not in {"global", "project"}:
71
+ raise ValueError("Scope must be either 'global' or 'project'")
72
+ return normalized
73
+
74
+
75
+ def known_source_dirs(project_root: Path | None = None) -> list[SkillCandidate]:
76
+ root = project_root.expanduser() if project_root is not None else Path.cwd()
77
+ candidates: list[SkillCandidate] = []
78
+
79
+ for agent in AGENTS:
80
+ candidates.append(SkillCandidate(agent.global_dir, f"{agent.label} global"))
81
+ candidates.append(SkillCandidate(root / agent.project_dir, f"{agent.label} project"))
82
+
83
+ return candidates
84
+
85
+
86
+ def resolve_source_candidates(
87
+ skill: str,
88
+ source: str | None = None,
89
+ project_root: Path | None = None,
90
+ ) -> list[SkillCandidate]:
91
+ skill_path = Path(skill).expanduser()
92
+ if skill_path.exists():
93
+ source_dir = ensure_skill_dir(skill_path)
94
+ return [SkillCandidate(source_dir, "explicit path")]
95
+
96
+ source_path = Path(source).expanduser() if source else None
97
+ if source_path is not None and source_path.exists():
98
+ if find_skill_file(source_path) is not None:
99
+ source_dir = ensure_skill_dir(source_path)
100
+ elif source_path.is_dir():
101
+ source_dir = ensure_skill_dir(source_path / skill)
102
+ else:
103
+ source_dir = ensure_skill_dir(source_path)
104
+ return [SkillCandidate(source_dir, "explicit --from path")]
105
+
106
+ search_dirs = _source_search_dirs(source, project_root)
107
+ found: list[SkillCandidate] = []
108
+ seen: set[Path] = set()
109
+ for base in search_dirs:
110
+ candidate_dir = base.path / skill
111
+ if find_skill_file(candidate_dir) is None:
112
+ continue
113
+ resolved = candidate_dir.resolve()
114
+ if resolved in seen:
115
+ continue
116
+ seen.add(resolved)
117
+ found.append(SkillCandidate(candidate_dir, base.label))
118
+
119
+ return found
120
+
121
+
122
+ def _source_search_dirs(source: str | None, project_root: Path | None) -> list[SkillCandidate]:
123
+ if source is None:
124
+ return known_source_dirs(project_root)
125
+
126
+ try:
127
+ agent = normalize_agent(source)
128
+ except ValueError as error:
129
+ raise SkillError(str(error)) from error
130
+
131
+ root = project_root.expanduser() if project_root is not None else Path.cwd()
132
+ return [
133
+ SkillCandidate(agent.global_dir, f"{agent.label} global"),
134
+ SkillCandidate(root / agent.project_dir, f"{agent.label} project"),
135
+ ]
136
+
137
+
138
+ def build_install_plan(
139
+ source_dir: Path,
140
+ target_agent: AgentProfile,
141
+ scope: str,
142
+ project_root: Path | None = None,
143
+ explicit_dest: Path | None = None,
144
+ ) -> InstallPlan:
145
+ source = ensure_skill_dir(source_dir)
146
+ base = target_base_dir(target_agent, scope, project_root, explicit_dest)
147
+ return InstallPlan(
148
+ source_dir=source,
149
+ target_dir=base / source.name,
150
+ skill_name=source.name,
151
+ target_agent=target_agent,
152
+ scope=normalize_scope(scope),
153
+ )
154
+
155
+
156
+ def install_skill(plan: InstallPlan, force: bool = False, dry_run: bool = False) -> None:
157
+ if plan.target_dir.exists():
158
+ if dry_run:
159
+ return
160
+ if not force:
161
+ raise SkillError(f"Target already exists: {plan.target_dir}")
162
+ if plan.target_dir.is_dir():
163
+ shutil.rmtree(plan.target_dir)
164
+ else:
165
+ plan.target_dir.unlink()
166
+
167
+ if dry_run:
168
+ return
169
+
170
+ plan.target_dir.parent.mkdir(parents=True, exist_ok=True)
171
+ shutil.copytree(plan.source_dir, plan.target_dir)
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from gl_cursor_skill.agents import normalize_agent
8
+ from gl_cursor_skill.core import (
9
+ SkillError,
10
+ build_install_plan,
11
+ ensure_skill_dir,
12
+ install_skill,
13
+ normalize_scope,
14
+ resolve_source_candidates,
15
+ )
16
+
17
+
18
+ def make_skill(path: Path) -> Path:
19
+ path.mkdir(parents=True)
20
+ (path / "SKILL.md").write_text("---\nname: demo\n---\n# Demo\n", encoding="utf-8")
21
+ return path
22
+
23
+
24
+ def test_normalize_agent_aliases() -> None:
25
+ assert normalize_agent("codex").key == "codex"
26
+ assert normalize_agent("claudecode").key == "claude-code"
27
+ assert normalize_agent("claude").key == "claude-code"
28
+
29
+
30
+ def test_normalize_scope() -> None:
31
+ assert normalize_scope("global") == "global"
32
+ assert normalize_scope("project") == "project"
33
+ with pytest.raises(ValueError):
34
+ normalize_scope("workspace")
35
+
36
+
37
+ def test_ensure_skill_dir_accepts_directory_and_skill_file(tmp_path: Path) -> None:
38
+ skill = make_skill(tmp_path / "cursor-agent")
39
+ assert ensure_skill_dir(skill) == skill
40
+ assert ensure_skill_dir(skill / "SKILL.md") == skill
41
+
42
+
43
+ def test_install_skill_to_explicit_destination(tmp_path: Path) -> None:
44
+ source = make_skill(tmp_path / "source" / "cursor-agent")
45
+ dest = tmp_path / "dest"
46
+ plan = build_install_plan(source, normalize_agent("codex"), "global", explicit_dest=dest)
47
+
48
+ install_skill(plan)
49
+
50
+ assert (dest / "cursor-agent" / "SKILL.md").is_file()
51
+
52
+
53
+ def test_install_skill_requires_force_for_existing_target(tmp_path: Path) -> None:
54
+ source = make_skill(tmp_path / "source" / "cursor-agent")
55
+ dest = tmp_path / "dest"
56
+ plan = build_install_plan(source, normalize_agent("codex"), "global", explicit_dest=dest)
57
+ install_skill(plan)
58
+
59
+ with pytest.raises(SkillError):
60
+ install_skill(plan)
61
+
62
+ install_skill(plan, force=True)
63
+ assert (dest / "cursor-agent" / "SKILL.md").is_file()
64
+
65
+
66
+ def test_dry_run_does_not_require_force_for_existing_target(tmp_path: Path) -> None:
67
+ source = make_skill(tmp_path / "source" / "cursor-agent")
68
+ dest = tmp_path / "dest"
69
+ plan = build_install_plan(source, normalize_agent("codex"), "global", explicit_dest=dest)
70
+ install_skill(plan)
71
+
72
+ install_skill(plan, dry_run=True)
73
+
74
+ assert (dest / "cursor-agent" / "SKILL.md").is_file()
75
+
76
+
77
+ def test_resolve_source_candidates_from_explicit_path(tmp_path: Path) -> None:
78
+ source = make_skill(tmp_path / "source" / "cursor-agent")
79
+
80
+ candidates = resolve_source_candidates(str(source))
81
+
82
+ assert len(candidates) == 1
83
+ assert candidates[0].path == source
84
+
85
+
86
+ def test_resolve_source_candidates_from_exact_from_path(tmp_path: Path) -> None:
87
+ source = make_skill(tmp_path / "source" / "cursor-agent")
88
+
89
+ candidates = resolve_source_candidates("cursor-agent", source=str(source))
90
+
91
+ assert len(candidates) == 1
92
+ assert candidates[0].path == source
93
+
94
+
95
+ def test_resolve_source_candidates_from_parent_from_path(tmp_path: Path) -> None:
96
+ source = make_skill(tmp_path / "source" / "cursor-agent")
97
+
98
+ candidates = resolve_source_candidates("cursor-agent", source=str(source.parent))
99
+
100
+ assert len(candidates) == 1
101
+ assert candidates[0].path == source
@@ -0,0 +1,266 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "annotated-doc"
7
+ version = "0.0.4"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "colorama"
16
+ version = "0.4.6"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "exceptiongroup"
25
+ version = "1.3.1"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ dependencies = [
28
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
29
+ ]
30
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "gl-cursor-skill"
37
+ version = "0.1.0"
38
+ source = { editable = "." }
39
+ dependencies = [
40
+ { name = "questionary" },
41
+ { name = "rich" },
42
+ { name = "typer" },
43
+ ]
44
+
45
+ [package.dev-dependencies]
46
+ dev = [
47
+ { name = "pytest" },
48
+ ]
49
+
50
+ [package.metadata]
51
+ requires-dist = [
52
+ { name = "questionary", specifier = ">=2.1.0" },
53
+ { name = "rich", specifier = ">=13.7.0" },
54
+ { name = "typer", specifier = ">=0.16.0" },
55
+ ]
56
+
57
+ [package.metadata.requires-dev]
58
+ dev = [{ name = "pytest", specifier = ">=8.2.0" }]
59
+
60
+ [[package]]
61
+ name = "iniconfig"
62
+ version = "2.3.0"
63
+ source = { registry = "https://pypi.org/simple" }
64
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
65
+ wheels = [
66
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
67
+ ]
68
+
69
+ [[package]]
70
+ name = "markdown-it-py"
71
+ version = "4.2.0"
72
+ source = { registry = "https://pypi.org/simple" }
73
+ dependencies = [
74
+ { name = "mdurl" },
75
+ ]
76
+ sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
77
+ wheels = [
78
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
79
+ ]
80
+
81
+ [[package]]
82
+ name = "mdurl"
83
+ version = "0.1.2"
84
+ source = { registry = "https://pypi.org/simple" }
85
+ sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
86
+ wheels = [
87
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
88
+ ]
89
+
90
+ [[package]]
91
+ name = "packaging"
92
+ version = "26.2"
93
+ source = { registry = "https://pypi.org/simple" }
94
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
95
+ wheels = [
96
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
97
+ ]
98
+
99
+ [[package]]
100
+ name = "pluggy"
101
+ version = "1.6.0"
102
+ source = { registry = "https://pypi.org/simple" }
103
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
104
+ wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
106
+ ]
107
+
108
+ [[package]]
109
+ name = "prompt-toolkit"
110
+ version = "3.0.52"
111
+ source = { registry = "https://pypi.org/simple" }
112
+ dependencies = [
113
+ { name = "wcwidth" },
114
+ ]
115
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
116
+ wheels = [
117
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
118
+ ]
119
+
120
+ [[package]]
121
+ name = "pygments"
122
+ version = "2.20.0"
123
+ source = { registry = "https://pypi.org/simple" }
124
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
125
+ wheels = [
126
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
127
+ ]
128
+
129
+ [[package]]
130
+ name = "pytest"
131
+ version = "9.1.1"
132
+ source = { registry = "https://pypi.org/simple" }
133
+ dependencies = [
134
+ { name = "colorama", marker = "sys_platform == 'win32'" },
135
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
136
+ { name = "iniconfig" },
137
+ { name = "packaging" },
138
+ { name = "pluggy" },
139
+ { name = "pygments" },
140
+ { name = "tomli", marker = "python_full_version < '3.11'" },
141
+ ]
142
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
143
+ wheels = [
144
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
145
+ ]
146
+
147
+ [[package]]
148
+ name = "questionary"
149
+ version = "2.1.1"
150
+ source = { registry = "https://pypi.org/simple" }
151
+ dependencies = [
152
+ { name = "prompt-toolkit" },
153
+ ]
154
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
155
+ wheels = [
156
+ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
157
+ ]
158
+
159
+ [[package]]
160
+ name = "rich"
161
+ version = "15.0.0"
162
+ source = { registry = "https://pypi.org/simple" }
163
+ dependencies = [
164
+ { name = "markdown-it-py" },
165
+ { name = "pygments" },
166
+ ]
167
+ sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
168
+ wheels = [
169
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
170
+ ]
171
+
172
+ [[package]]
173
+ name = "shellingham"
174
+ version = "1.5.4"
175
+ source = { registry = "https://pypi.org/simple" }
176
+ sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
177
+ wheels = [
178
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
179
+ ]
180
+
181
+ [[package]]
182
+ name = "tomli"
183
+ version = "2.4.1"
184
+ source = { registry = "https://pypi.org/simple" }
185
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
186
+ wheels = [
187
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
188
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
189
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
190
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
191
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
192
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
193
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
194
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
195
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
196
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
197
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
198
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
199
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
200
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
201
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
202
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
203
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
204
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
205
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
206
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
207
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
208
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
209
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
210
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
211
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
212
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
213
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
214
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
215
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
216
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
217
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
218
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
219
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
220
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
221
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
222
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
223
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
224
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
225
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
226
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
227
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
228
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
229
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
230
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
231
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
232
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
233
+ ]
234
+
235
+ [[package]]
236
+ name = "typer"
237
+ version = "0.26.8"
238
+ source = { registry = "https://pypi.org/simple" }
239
+ dependencies = [
240
+ { name = "annotated-doc" },
241
+ { name = "colorama", marker = "sys_platform == 'win32'" },
242
+ { name = "rich" },
243
+ { name = "shellingham" },
244
+ ]
245
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/f7/68adc395201b20b872d68e975386832e8005ffeacedd43a1d837a32815be/typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e", size = 202097, upload-time = "2026-06-26T09:22:45.705Z" }
246
+ wheels = [
247
+ { url = "https://files.pythonhosted.org/packages/80/87/b9fd69c92c6102a066e1b86a35243f53e70bd4c709f2a26d9f4fee4f4dc0/typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c", size = 122564, upload-time = "2026-06-26T09:22:44.72Z" },
248
+ ]
249
+
250
+ [[package]]
251
+ name = "typing-extensions"
252
+ version = "4.16.0"
253
+ source = { registry = "https://pypi.org/simple" }
254
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
255
+ wheels = [
256
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
257
+ ]
258
+
259
+ [[package]]
260
+ name = "wcwidth"
261
+ version = "0.8.2"
262
+ source = { registry = "https://pypi.org/simple" }
263
+ sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" }
264
+ wheels = [
265
+ { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" },
266
+ ]