gl-cursor-skill 0.1.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.
- gl_cursor_skill/__init__.py +3 -0
- gl_cursor_skill/agents.py +54 -0
- gl_cursor_skill/cli.py +267 -0
- gl_cursor_skill/core.py +171 -0
- gl_cursor_skill-0.1.0.dist-info/METADATA +119 -0
- gl_cursor_skill-0.1.0.dist-info/RECORD +8 -0
- gl_cursor_skill-0.1.0.dist-info/WHEEL +4 -0
- gl_cursor_skill-0.1.0.dist-info/entry_points.txt +2 -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]
|
gl_cursor_skill/cli.py
ADDED
|
@@ -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()
|
gl_cursor_skill/core.py
ADDED
|
@@ -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,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,8 @@
|
|
|
1
|
+
gl_cursor_skill/__init__.py,sha256=K_dSDZrbyWw2QrMcX7EYkC2dTRMhZuSlxYR4ZxNSt14,73
|
|
2
|
+
gl_cursor_skill/agents.py,sha256=ypkjRZjzZ_VxjGHdZ1Geei1FutpkHl5mjeEqKrVN67o,1362
|
|
3
|
+
gl_cursor_skill/cli.py,sha256=P_qeb21HgEstEe90cjDXmUry99I9QkoQ1Jop86-dSR0,8246
|
|
4
|
+
gl_cursor_skill/core.py,sha256=iIS7029zinfjsTc66i67h3nZqJlLkwyj2m5Nwg5kQA4,5073
|
|
5
|
+
gl_cursor_skill-0.1.0.dist-info/METADATA,sha256=YjIVtCArbebx7liBY7vFpuH1vRrmiGjU7AZWinfKS3w,3054
|
|
6
|
+
gl_cursor_skill-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
gl_cursor_skill-0.1.0.dist-info/entry_points.txt,sha256=a8o0l_RJH5NET2TCkOg5etbOo_fP-zy-EKH2A2mgTv8,61
|
|
8
|
+
gl_cursor_skill-0.1.0.dist-info/RECORD,,
|