agent-catalog-cli 0.1.0__tar.gz → 0.2.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.
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/PKG-INFO +27 -16
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/README.md +26 -15
- agent_catalog_cli-0.2.0/agents_catalog/app.py +289 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/agents_catalog/cache.py +7 -3
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/agents_catalog/scanner.py +84 -20
- agent_catalog_cli-0.2.0/agents_catalog/skills/agent-catalog/SKILL.md +54 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/pyproject.toml +1 -1
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/uv.lock +1 -1
- agent_catalog_cli-0.1.0/agents_catalog/app.py +0 -292
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/.gitignore +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/.python-version +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/.skill-scan-cache.json +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/LICENSE +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/agents_catalog/__init__.py +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/agents_catalog/destinations.py +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/agents_catalog/installer.py +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/agents_catalog/models.py +0 -0
- {agent_catalog_cli-0.1.0 → agent_catalog_cli-0.2.0}/main.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-catalog-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Scan a directory for AI-assistant skills, rules and subagents and copy selected ones into a chosen assistant's folder layout.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -38,37 +38,48 @@ Names and descriptions come from YAML frontmatter (`name`, `description`); rules
|
|
|
38
38
|
uv run agent-catalog init /path/to/source
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
### `
|
|
41
|
+
### `add skills <target_dir>` / `add agents <target_dir>` / `add rules <target_dir>`
|
|
42
42
|
|
|
43
|
-
Additive scans
|
|
43
|
+
Additive scans, discovered recursively anywhere under `target_dir`, **appended** to the existing `.agent-catalog.json` (deduped by absolute path, so re-runs and overlap with `init` are safe). `skip` dirs (`.git`, `.venv`, `node_modules`, …) are excluded.
|
|
44
44
|
|
|
45
|
-
- `
|
|
46
|
-
- `
|
|
45
|
+
- `add skills` — `<skill>/` packages (containing `SKILL.md`) under folders named `skills/`.
|
|
46
|
+
- `add agents` — `*.md` files directly under any `agents/` folder.
|
|
47
|
+
- `add rules` — `*.md` / `*.mdc` under any `rules/` or `instructions/` folder, plus `CLAUDE.md` / `AGENTS.md` memory files.
|
|
48
|
+
|
|
49
|
+
> `add plugins` (folders named `claude-plugin/` / `cursor-plugin/`) is temporarily suspended.
|
|
47
50
|
|
|
48
51
|
```bash
|
|
49
|
-
uv run agent-catalog
|
|
50
|
-
uv run agent-catalog
|
|
52
|
+
uv run agent-catalog add skills /path/to/source
|
|
53
|
+
uv run agent-catalog add agents /path/to/source
|
|
54
|
+
uv run agent-catalog add rules /path/to/source
|
|
51
55
|
```
|
|
52
56
|
|
|
53
|
-
### `pick
|
|
57
|
+
### `pick skills [TERM]` / `pick agents [TERM]` / `pick rules [TERM]`
|
|
58
|
+
|
|
59
|
+
Loads the cache and opens an interactive multi-select (Arrows to move, type to filter live, `<Space>` to toggle, `Enter` to confirm), asks which coding assistant to target, and copies the selected artifacts into that assistant's folder.
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
`TERM` is **optional**. When provided, the catalog is pre-narrowed by a case-insensitive substring matched against each artifact's **name and description** before the picker opens — handy for large catalogs. Live type-to-filter still applies on top. Omit `TERM` to list everything.
|
|
56
62
|
|
|
57
63
|
```bash
|
|
58
|
-
uv run agent-catalog pick
|
|
59
|
-
uv run agent-catalog pick
|
|
60
|
-
uv run agent-catalog pick
|
|
64
|
+
uv run agent-catalog pick skills # copy into ./ (cwd)
|
|
65
|
+
uv run agent-catalog pick skills wiki # pre-filter to name/description containing "wiki"
|
|
66
|
+
uv run agent-catalog pick rules --target ./proj # copy into another project root
|
|
67
|
+
uv run agent-catalog pick agents --assistant .claude --overwrite
|
|
61
68
|
```
|
|
62
69
|
|
|
63
|
-
### `
|
|
70
|
+
### `install-skill`
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
Installs the bundled `agent-catalog` management skill (shipped inside the package at `agents_catalog/skills/agent-catalog/`) into a chosen root. Non-interactive.
|
|
66
73
|
|
|
67
74
|
```bash
|
|
68
|
-
uv run agent-catalog
|
|
69
|
-
uv run agent-catalog
|
|
75
|
+
uv run agent-catalog install-skill --target . --assistant .cursor # ./.cursor/skills/agent-catalog/
|
|
76
|
+
uv run agent-catalog install-skill --target ~ --assistant .cursor # ~/.cursor/skills/agent-catalog/ (global)
|
|
70
77
|
```
|
|
71
78
|
|
|
79
|
+
- `--target, -t` — root to install into (default: cwd; `~` for global).
|
|
80
|
+
- `--assistant, -a` — assistant folder convention (default: `.cursor`).
|
|
81
|
+
- `--overwrite` — refresh an existing copy.
|
|
82
|
+
|
|
72
83
|
Options (all pick commands):
|
|
73
84
|
|
|
74
85
|
- `--target, -t` — project root to copy into (default: cwd).
|
|
@@ -27,37 +27,48 @@ Names and descriptions come from YAML frontmatter (`name`, `description`); rules
|
|
|
27
27
|
uv run agent-catalog init /path/to/source
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
### `
|
|
30
|
+
### `add skills <target_dir>` / `add agents <target_dir>` / `add rules <target_dir>`
|
|
31
31
|
|
|
32
|
-
Additive scans
|
|
32
|
+
Additive scans, discovered recursively anywhere under `target_dir`, **appended** to the existing `.agent-catalog.json` (deduped by absolute path, so re-runs and overlap with `init` are safe). `skip` dirs (`.git`, `.venv`, `node_modules`, …) are excluded.
|
|
33
33
|
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
34
|
+
- `add skills` — `<skill>/` packages (containing `SKILL.md`) under folders named `skills/`.
|
|
35
|
+
- `add agents` — `*.md` files directly under any `agents/` folder.
|
|
36
|
+
- `add rules` — `*.md` / `*.mdc` under any `rules/` or `instructions/` folder, plus `CLAUDE.md` / `AGENTS.md` memory files.
|
|
37
|
+
|
|
38
|
+
> `add plugins` (folders named `claude-plugin/` / `cursor-plugin/`) is temporarily suspended.
|
|
36
39
|
|
|
37
40
|
```bash
|
|
38
|
-
uv run agent-catalog
|
|
39
|
-
uv run agent-catalog
|
|
41
|
+
uv run agent-catalog add skills /path/to/source
|
|
42
|
+
uv run agent-catalog add agents /path/to/source
|
|
43
|
+
uv run agent-catalog add rules /path/to/source
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
### `pick
|
|
46
|
+
### `pick skills [TERM]` / `pick agents [TERM]` / `pick rules [TERM]`
|
|
47
|
+
|
|
48
|
+
Loads the cache and opens an interactive multi-select (Arrows to move, type to filter live, `<Space>` to toggle, `Enter` to confirm), asks which coding assistant to target, and copies the selected artifacts into that assistant's folder.
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
`TERM` is **optional**. When provided, the catalog is pre-narrowed by a case-insensitive substring matched against each artifact's **name and description** before the picker opens — handy for large catalogs. Live type-to-filter still applies on top. Omit `TERM` to list everything.
|
|
45
51
|
|
|
46
52
|
```bash
|
|
47
|
-
uv run agent-catalog pick
|
|
48
|
-
uv run agent-catalog pick
|
|
49
|
-
uv run agent-catalog pick
|
|
53
|
+
uv run agent-catalog pick skills # copy into ./ (cwd)
|
|
54
|
+
uv run agent-catalog pick skills wiki # pre-filter to name/description containing "wiki"
|
|
55
|
+
uv run agent-catalog pick rules --target ./proj # copy into another project root
|
|
56
|
+
uv run agent-catalog pick agents --assistant .claude --overwrite
|
|
50
57
|
```
|
|
51
58
|
|
|
52
|
-
### `
|
|
59
|
+
### `install-skill`
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
Installs the bundled `agent-catalog` management skill (shipped inside the package at `agents_catalog/skills/agent-catalog/`) into a chosen root. Non-interactive.
|
|
55
62
|
|
|
56
63
|
```bash
|
|
57
|
-
uv run agent-catalog
|
|
58
|
-
uv run agent-catalog
|
|
64
|
+
uv run agent-catalog install-skill --target . --assistant .cursor # ./.cursor/skills/agent-catalog/
|
|
65
|
+
uv run agent-catalog install-skill --target ~ --assistant .cursor # ~/.cursor/skills/agent-catalog/ (global)
|
|
59
66
|
```
|
|
60
67
|
|
|
68
|
+
- `--target, -t` — root to install into (default: cwd; `~` for global).
|
|
69
|
+
- `--assistant, -a` — assistant folder convention (default: `.cursor`).
|
|
70
|
+
- `--overwrite` — refresh an existing copy.
|
|
71
|
+
|
|
61
72
|
Options (all pick commands):
|
|
62
73
|
|
|
63
74
|
- `--target, -t` — project root to copy into (default: cwd).
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Typer CLI: index AI-assistant artifacts and copy selected ones into a project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import questionary
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from . import cache as cache_mod
|
|
14
|
+
from . import destinations as dest_mod
|
|
15
|
+
from . import scanner
|
|
16
|
+
from .installer import CopyResult, install
|
|
17
|
+
from .models import Artifact, Catalog, Kind
|
|
18
|
+
|
|
19
|
+
# The agent-catalog management skill ships inside the package so the CLI can install it.
|
|
20
|
+
SELF_SKILL_DIR = Path(__file__).resolve().parent / "skills" / "agent-catalog"
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
help="Scan a directory for AI-assistant skills, rules and subagents, then copy selected ones into a project.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
add_completion=False,
|
|
26
|
+
)
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
_KIND_LABEL = {"skill": "skills", "agent": "agents", "rule": "rules"}
|
|
30
|
+
|
|
31
|
+
add_app = typer.Typer(
|
|
32
|
+
help="Additively index skill, plugin, agent and rule collections into the cache.",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
)
|
|
35
|
+
pick_app = typer.Typer(
|
|
36
|
+
help="Interactively select indexed artifacts and copy them into a project.",
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
)
|
|
39
|
+
app.add_typer(add_app, name="add")
|
|
40
|
+
app.add_typer(pick_app, name="pick")
|
|
41
|
+
|
|
42
|
+
# Shared parameter defaults reused across the `add` / `pick` subcommands (DRY).
|
|
43
|
+
TARGET_DIR_ARG = typer.Argument(
|
|
44
|
+
...,
|
|
45
|
+
exists=True,
|
|
46
|
+
file_okay=False,
|
|
47
|
+
dir_okay=True,
|
|
48
|
+
readable=True,
|
|
49
|
+
resolve_path=True,
|
|
50
|
+
help="Directory to scan for skill/plugin/agent/rule collections.",
|
|
51
|
+
)
|
|
52
|
+
TERM_ARG = typer.Argument(
|
|
53
|
+
None,
|
|
54
|
+
metavar="[TERM]",
|
|
55
|
+
help=(
|
|
56
|
+
"Optional case-insensitive substring matched against artifact name and "
|
|
57
|
+
"description to pre-filter the picker. Omit to list everything."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
TARGET_OPT = typer.Option(
|
|
61
|
+
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
62
|
+
)
|
|
63
|
+
ASSISTANT_OPT = typer.Option(
|
|
64
|
+
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
65
|
+
)
|
|
66
|
+
OVERWRITE_OPT = typer.Option(
|
|
67
|
+
False,
|
|
68
|
+
"--overwrite",
|
|
69
|
+
help=(
|
|
70
|
+
"Replace artifacts that already exist at the destination (skills merge over the "
|
|
71
|
+
"folder, overwriting overlapping files; single files are replaced). Without it, "
|
|
72
|
+
"existing targets are skipped."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@app.command()
|
|
78
|
+
def init(
|
|
79
|
+
target_dir: Path = typer.Argument(
|
|
80
|
+
...,
|
|
81
|
+
exists=True,
|
|
82
|
+
file_okay=False,
|
|
83
|
+
dir_okay=True,
|
|
84
|
+
readable=True,
|
|
85
|
+
resolve_path=True,
|
|
86
|
+
help="Directory to scan for .agents/.cursor/.claude/.github/.codex configs.",
|
|
87
|
+
),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Scan TARGET_DIR and cache discovered skills, subagents and rules."""
|
|
90
|
+
catalog = scanner.scan(target_dir)
|
|
91
|
+
path = cache_mod.write_cache(catalog)
|
|
92
|
+
|
|
93
|
+
table = Table(title="Indexed artifacts")
|
|
94
|
+
table.add_column("Kind")
|
|
95
|
+
table.add_column("Count", justify="right")
|
|
96
|
+
table.add_row("Skills", str(len(catalog.skills)))
|
|
97
|
+
table.add_row("Agents", str(len(catalog.agents)))
|
|
98
|
+
table.add_row("Rules", str(len(catalog.rules)))
|
|
99
|
+
console.print(table)
|
|
100
|
+
console.print(f"Cache written to [bold]{path}[/bold]")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _add(new: list[Artifact], kind: Kind) -> None:
|
|
104
|
+
catalog = cache_mod.append_artifacts(new)
|
|
105
|
+
total = len(catalog.by_kind(kind))
|
|
106
|
+
console.print(
|
|
107
|
+
f"Added [bold]{len(new)}[/bold] {kind}(s); cache now has {total} {kind}(s)."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@add_app.command("skills")
|
|
112
|
+
def add_skills(target_dir: Path = TARGET_DIR_ARG) -> None:
|
|
113
|
+
"""Find skills/<skill> packages under TARGET_DIR and add them to the cache."""
|
|
114
|
+
_add(scanner.scan_collections(target_dir, scanner.SKILL_COLLECTION_DIRS), "skill")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Suspended for now: re-enable by uncommenting to restore `add plugins`.
|
|
118
|
+
# @add_app.command("plugins")
|
|
119
|
+
# def add_plugins(target_dir: Path = TARGET_DIR_ARG) -> None:
|
|
120
|
+
# """Find claude-plugin/<skill> and cursor-plugin/<skill> packages under TARGET_DIR and add them."""
|
|
121
|
+
# _add(scanner.scan_collections(target_dir, scanner.PLUGIN_COLLECTION_DIRS), "skill")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@add_app.command("agents")
|
|
125
|
+
def add_agents(target_dir: Path = TARGET_DIR_ARG) -> None:
|
|
126
|
+
"""Find *.md under agents/ folders anywhere under TARGET_DIR and add them to the cache."""
|
|
127
|
+
_add(scanner.scan_agent_collections(target_dir), "agent")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@add_app.command("rules")
|
|
131
|
+
def add_rules(target_dir: Path = TARGET_DIR_ARG) -> None:
|
|
132
|
+
"""Find rules under rules//instructions/ folders (plus memory files) under TARGET_DIR and add them."""
|
|
133
|
+
_add(scanner.scan_rule_collections(target_dir), "rule")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _load_catalog() -> Catalog:
|
|
137
|
+
try:
|
|
138
|
+
return cache_mod.read_cache()
|
|
139
|
+
except FileNotFoundError:
|
|
140
|
+
console.print(
|
|
141
|
+
"[red]No cache found.[/red] Run [bold]agent-catalog init <target_dir>[/bold] first."
|
|
142
|
+
)
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _choice_label(a: Artifact) -> str:
|
|
147
|
+
desc = a.description.strip().replace("\n", " ")
|
|
148
|
+
if len(desc) > 100:
|
|
149
|
+
desc = desc[:97] + "..."
|
|
150
|
+
suffix = f" - {desc}" if desc else ""
|
|
151
|
+
return f"[{a.source_root}] {a.name}{suffix}"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _filter(artifacts: list[Artifact], term: Optional[str]) -> list[Artifact]:
|
|
155
|
+
if not term:
|
|
156
|
+
return artifacts
|
|
157
|
+
t = term.lower()
|
|
158
|
+
return [a for a in artifacts if t in a.name.lower() or t in a.description.lower()]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _pick(
|
|
162
|
+
kind: Kind,
|
|
163
|
+
target: Path,
|
|
164
|
+
assistant: Optional[str],
|
|
165
|
+
overwrite: bool,
|
|
166
|
+
term: Optional[str] = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
catalog = _load_catalog()
|
|
169
|
+
artifacts = _filter(catalog.by_kind(kind), term)
|
|
170
|
+
label = _KIND_LABEL[kind]
|
|
171
|
+
|
|
172
|
+
if not artifacts:
|
|
173
|
+
suffix = f" matching '{term}'" if term else ""
|
|
174
|
+
console.print(f"[yellow]No {label}{suffix} indexed.[/yellow]")
|
|
175
|
+
raise typer.Exit(code=0)
|
|
176
|
+
|
|
177
|
+
choices = [questionary.Choice(title=_choice_label(a), value=a) for a in artifacts]
|
|
178
|
+
selected: list[Artifact] = (
|
|
179
|
+
questionary.checkbox(
|
|
180
|
+
f"Select {label} to copy (type to filter, Space to toggle, Enter to confirm):",
|
|
181
|
+
choices=choices,
|
|
182
|
+
use_search_filter=True,
|
|
183
|
+
use_jk_keys=False,
|
|
184
|
+
).ask()
|
|
185
|
+
or []
|
|
186
|
+
)
|
|
187
|
+
if not selected:
|
|
188
|
+
console.print("[yellow]Nothing selected.[/yellow]")
|
|
189
|
+
raise typer.Exit(code=0)
|
|
190
|
+
|
|
191
|
+
if assistant is None:
|
|
192
|
+
assistant = questionary.select(
|
|
193
|
+
"Copy into which coding assistant?",
|
|
194
|
+
choices=dest_mod.ASSISTANTS,
|
|
195
|
+
).ask()
|
|
196
|
+
if assistant is None:
|
|
197
|
+
console.print("[yellow]No assistant chosen.[/yellow]")
|
|
198
|
+
raise typer.Exit(code=0)
|
|
199
|
+
if assistant not in dest_mod.ASSISTANTS:
|
|
200
|
+
console.print(f"[red]Unknown assistant '{assistant}'.[/red]")
|
|
201
|
+
raise typer.Exit(code=1)
|
|
202
|
+
|
|
203
|
+
dest_dir = dest_mod.destination_dir(target, assistant, kind)
|
|
204
|
+
results = install(selected, dest_dir, overwrite=overwrite)
|
|
205
|
+
_print_results(results)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _print_results(results: list[CopyResult]) -> None:
|
|
209
|
+
for r in results:
|
|
210
|
+
if r.status == "copied":
|
|
211
|
+
console.print(f"[green]copied[/green] {r.artifact.name} -> {r.destination}")
|
|
212
|
+
elif r.status == "skipped":
|
|
213
|
+
console.print(
|
|
214
|
+
f"[yellow]skipped[/yellow] {r.artifact.name} (exists; use --overwrite) -> {r.destination}"
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
console.print(f"[red]missing[/red] {r.artifact.name} (source gone: {r.artifact.path})")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pick_app.command("skills")
|
|
221
|
+
def pick_skills(
|
|
222
|
+
term: Optional[str] = TERM_ARG,
|
|
223
|
+
target: Path = TARGET_OPT,
|
|
224
|
+
assistant: Optional[str] = ASSISTANT_OPT,
|
|
225
|
+
overwrite: bool = OVERWRITE_OPT,
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Select indexed skills and copy them into a project. TERM is optional; when given it pre-filters by name/description."""
|
|
228
|
+
_pick("skill", target, assistant, overwrite, term=term)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@pick_app.command("agents")
|
|
232
|
+
def pick_agents(
|
|
233
|
+
term: Optional[str] = TERM_ARG,
|
|
234
|
+
target: Path = TARGET_OPT,
|
|
235
|
+
assistant: Optional[str] = ASSISTANT_OPT,
|
|
236
|
+
overwrite: bool = OVERWRITE_OPT,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Select indexed subagents and copy them into a project. TERM is optional; when given it pre-filters by name/description."""
|
|
239
|
+
_pick("agent", target, assistant, overwrite, term=term)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@pick_app.command("rules")
|
|
243
|
+
def pick_rules(
|
|
244
|
+
term: Optional[str] = TERM_ARG,
|
|
245
|
+
target: Path = TARGET_OPT,
|
|
246
|
+
assistant: Optional[str] = ASSISTANT_OPT,
|
|
247
|
+
overwrite: bool = OVERWRITE_OPT,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Select indexed rules and copy them into a project. TERM is optional; when given it pre-filters by name/description."""
|
|
250
|
+
_pick("rule", target, assistant, overwrite, term=term)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.command("install-skill")
|
|
254
|
+
def install_skill(
|
|
255
|
+
target: Path = typer.Option(
|
|
256
|
+
Path.cwd(),
|
|
257
|
+
"--target",
|
|
258
|
+
"-t",
|
|
259
|
+
resolve_path=True,
|
|
260
|
+
help="Root to install into ('.' for the current workspace, '~' for global).",
|
|
261
|
+
),
|
|
262
|
+
assistant: str = typer.Option(
|
|
263
|
+
".cursor", "--assistant", "-a", help="Assistant folder convention to install under."
|
|
264
|
+
),
|
|
265
|
+
overwrite: bool = typer.Option(
|
|
266
|
+
False, "--overwrite", help="Replace the skill if it already exists at the destination."
|
|
267
|
+
),
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Install the bundled agent-catalog management skill into TARGET/<assistant>/skills/."""
|
|
270
|
+
if assistant not in dest_mod.ASSISTANTS:
|
|
271
|
+
console.print(f"[red]Unknown assistant '{assistant}'.[/red]")
|
|
272
|
+
raise typer.Exit(code=1)
|
|
273
|
+
if not (SELF_SKILL_DIR / "SKILL.md").is_file():
|
|
274
|
+
console.print(f"[red]Bundled skill not found at {SELF_SKILL_DIR}.[/red]")
|
|
275
|
+
raise typer.Exit(code=1)
|
|
276
|
+
|
|
277
|
+
artifact = Artifact(
|
|
278
|
+
kind="skill",
|
|
279
|
+
name=SELF_SKILL_DIR.name,
|
|
280
|
+
description="agent-catalog management skill",
|
|
281
|
+
path=str(SELF_SKILL_DIR),
|
|
282
|
+
source_root="agents_catalog",
|
|
283
|
+
)
|
|
284
|
+
dest_dir = dest_mod.destination_dir(target, assistant, "skill")
|
|
285
|
+
_print_results(install([artifact], dest_dir, overwrite=overwrite))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
app()
|
|
@@ -29,12 +29,16 @@ def read_cache(directory: Path | None = None) -> Catalog:
|
|
|
29
29
|
return Catalog.from_dict(data)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def
|
|
33
|
-
"""Additively merge new
|
|
32
|
+
def append_artifacts(new: list[Artifact], directory: Path | None = None) -> Catalog:
|
|
33
|
+
"""Additively merge new artifacts into the cache by kind (deduped by path)."""
|
|
34
34
|
try:
|
|
35
35
|
catalog = read_cache(directory)
|
|
36
36
|
except FileNotFoundError:
|
|
37
37
|
catalog = Catalog(source=str((directory or Path.cwd()).resolve()))
|
|
38
|
-
|
|
38
|
+
for artifact in new:
|
|
39
|
+
catalog.by_kind(artifact.kind).append(artifact)
|
|
40
|
+
catalog.skills = scanner.dedup(catalog.skills)
|
|
41
|
+
catalog.agents = scanner.dedup(catalog.agents)
|
|
42
|
+
catalog.rules = scanner.dedup(catalog.rules)
|
|
39
43
|
write_cache(catalog, directory)
|
|
40
44
|
return catalog
|
|
@@ -20,9 +20,11 @@ ROOT_RULE_FILES = {".github": ("custom-instructions.md",)}
|
|
|
20
20
|
# Directories skipped while searching for memory files.
|
|
21
21
|
SKIP_DIRS = {".git", ".venv", "node_modules", "__pycache__", ".obsidian"}
|
|
22
22
|
|
|
23
|
-
# Patterned
|
|
23
|
+
# Patterned collection folder names indexed by the additive `add` subcommands.
|
|
24
24
|
SKILL_COLLECTION_DIRS = {"skills"}
|
|
25
25
|
PLUGIN_COLLECTION_DIRS = {"claude-plugin", "cursor-plugin"}
|
|
26
|
+
AGENT_COLLECTION_DIRS = {"agents"}
|
|
27
|
+
RULE_COLLECTION_DIRS = {"rules", "instructions"}
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def _read_text(path: Path) -> str:
|
|
@@ -126,35 +128,80 @@ def _rule_artifact(file: Path, root_name: str) -> Artifact:
|
|
|
126
128
|
)
|
|
127
129
|
|
|
128
130
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
def _find_skill_dirs(base: Path) -> list[Path]:
|
|
132
|
+
"""Resolved package dirs of every SKILL.md under ``base``."""
|
|
133
|
+
return [m.parent.resolve() for m in base.rglob(SKILL_FILE) if m.is_file()]
|
|
134
|
+
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
skills.append(_skill_artifact(skill_md, root_name))
|
|
138
|
-
skill_dirs.append(skill_md.parent.resolve())
|
|
136
|
+
def _inside_skill(path: Path, skill_dirs: list[Path]) -> bool:
|
|
137
|
+
rp = path.resolve()
|
|
138
|
+
return any(rp == d or d in rp.parents for d in skill_dirs)
|
|
139
139
|
|
|
140
|
-
def inside_skill(p: Path) -> bool:
|
|
141
|
-
rp = p.resolve()
|
|
142
|
-
return any(rp == d or d in rp.parents for d in skill_dirs)
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
def _is_skipped(path: Path, base: Path) -> bool:
|
|
142
|
+
try:
|
|
143
|
+
rel = path.relative_to(base)
|
|
144
|
+
except ValueError:
|
|
145
|
+
return False
|
|
146
|
+
return any(part in SKIP_DIRS for part in rel.parts)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _collect_agents(
|
|
150
|
+
base: Path,
|
|
151
|
+
skill_dirs: list[Path],
|
|
152
|
+
source_root: str | None = None,
|
|
153
|
+
exclude_skip: bool = False,
|
|
154
|
+
) -> list[Artifact]:
|
|
155
|
+
"""``*.md`` directly under any ``agents/`` dir, ignoring skill-internal ones.
|
|
156
|
+
|
|
157
|
+
``source_root`` overrides the recorded root; when ``None`` the containing
|
|
158
|
+
folder name is used (the collection-add convention)."""
|
|
159
|
+
out: list[Artifact] = []
|
|
160
|
+
for agents_dir in base.rglob("agents"):
|
|
161
|
+
if not agents_dir.is_dir() or _inside_skill(agents_dir, skill_dirs):
|
|
162
|
+
continue
|
|
163
|
+
if exclude_skip and _is_skipped(agents_dir, base):
|
|
146
164
|
continue
|
|
165
|
+
sr = agents_dir.name if source_root is None else source_root
|
|
147
166
|
for file in sorted(agents_dir.glob("*.md")):
|
|
148
167
|
if file.is_file():
|
|
149
|
-
|
|
168
|
+
out.append(_agent_artifact(file, sr))
|
|
169
|
+
return out
|
|
170
|
+
|
|
150
171
|
|
|
151
|
-
|
|
172
|
+
def _collect_rules(
|
|
173
|
+
base: Path,
|
|
174
|
+
skill_dirs: list[Path],
|
|
175
|
+
source_root: str | None = None,
|
|
176
|
+
exclude_skip: bool = False,
|
|
177
|
+
) -> list[Artifact]:
|
|
178
|
+
"""``*.md`` / ``*.mdc`` recursively under any ``rules/`` or ``instructions/`` dir."""
|
|
179
|
+
out: list[Artifact] = []
|
|
180
|
+
rule_dirs = sorted(
|
|
181
|
+
(d for name in RULE_COLLECTION_DIRS for d in base.rglob(name)),
|
|
182
|
+
key=str,
|
|
183
|
+
)
|
|
152
184
|
for rules_dir in rule_dirs:
|
|
153
|
-
if not rules_dir.is_dir() or
|
|
185
|
+
if not rules_dir.is_dir() or _inside_skill(rules_dir, skill_dirs):
|
|
186
|
+
continue
|
|
187
|
+
if exclude_skip and _is_skipped(rules_dir, base):
|
|
154
188
|
continue
|
|
189
|
+
sr = rules_dir.name if source_root is None else source_root
|
|
155
190
|
for file in sorted(rules_dir.rglob("*")):
|
|
156
|
-
if
|
|
157
|
-
|
|
191
|
+
if (
|
|
192
|
+
file.is_file()
|
|
193
|
+
and file.suffix in RULE_SUFFIXES
|
|
194
|
+
and not _inside_skill(file, skill_dirs)
|
|
195
|
+
):
|
|
196
|
+
out.append(_rule_artifact(file, sr))
|
|
197
|
+
return out
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _scan_root(root: Path, root_name: str) -> tuple[list[Artifact], list[Artifact], list[Artifact]]:
|
|
201
|
+
skill_dirs = _find_skill_dirs(root)
|
|
202
|
+
skills = [_skill_artifact(d / SKILL_FILE, root_name) for d in skill_dirs]
|
|
203
|
+
agents = _collect_agents(root, skill_dirs, source_root=root_name)
|
|
204
|
+
rules = _collect_rules(root, skill_dirs, source_root=root_name)
|
|
158
205
|
|
|
159
206
|
for filename in ROOT_RULE_FILES.get(root_name, ()):
|
|
160
207
|
candidate = root / filename
|
|
@@ -193,6 +240,23 @@ def scan_collections(target_dir: Path, folder_names: set[str]) -> list[Artifact]
|
|
|
193
240
|
return out
|
|
194
241
|
|
|
195
242
|
|
|
243
|
+
def scan_agent_collections(target_dir: Path) -> list[Artifact]:
|
|
244
|
+
"""Index ``*.md`` under any ``agents/`` folder anywhere under target_dir."""
|
|
245
|
+
target_dir = target_dir.resolve()
|
|
246
|
+
skill_dirs = _find_skill_dirs(target_dir)
|
|
247
|
+
return _collect_agents(target_dir, skill_dirs, exclude_skip=True)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def scan_rule_collections(target_dir: Path) -> list[Artifact]:
|
|
251
|
+
"""Index rules under any ``rules/`` / ``instructions/`` folder anywhere under
|
|
252
|
+
target_dir, plus CLAUDE.md / AGENTS.md memory files."""
|
|
253
|
+
target_dir = target_dir.resolve()
|
|
254
|
+
skill_dirs = _find_skill_dirs(target_dir)
|
|
255
|
+
rules = _collect_rules(target_dir, skill_dirs, exclude_skip=True)
|
|
256
|
+
rules.extend(_scan_memory_files(target_dir))
|
|
257
|
+
return rules
|
|
258
|
+
|
|
259
|
+
|
|
196
260
|
def dedup(artifacts: list[Artifact]) -> list[Artifact]:
|
|
197
261
|
seen: set[str] = set()
|
|
198
262
|
out: list[Artifact] = []
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-catalog
|
|
3
|
+
description: Manage a catalog of AI-assistant skills, subagents and rules with the agent-catalog CLI, and install selected ones into the current workspace or the global agent skills. Use when the user wants to catalog, index, search, find, add, or install skills/subagents/rules, or set up reusable agent assets in a workspace.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# agent-catalog
|
|
7
|
+
|
|
8
|
+
Drive the `agent-catalog` CLI to build a catalog of reusable AI-assistant assets (skills, subagents, rules) and install the right ones into the workspace you operate in or into the global agent skills.
|
|
9
|
+
|
|
10
|
+
## Prerequisite
|
|
11
|
+
|
|
12
|
+
The `agent-catalog` command. From the CLI repo: `uv run agent-catalog ...`. Installed globally: `pip install agent-catalog-cli`, then `agent-catalog ...`. Examples below use the bare command.
|
|
13
|
+
|
|
14
|
+
## Build / refresh the catalog (non-interactive)
|
|
15
|
+
|
|
16
|
+
These commands take a directory and write/append `.agent-catalog.json` in the current directory. Safe to run unattended.
|
|
17
|
+
|
|
18
|
+
- `agent-catalog init <dir>` — scan the config roots `.agents/ .cursor/ .claude/ .github/ .codex/` under `<dir>` (overwrites the cache).
|
|
19
|
+
- `agent-catalog add skills <dir>` — additively index `skills/<skill>/` packages found anywhere under `<dir>`.
|
|
20
|
+
- `agent-catalog add plugins <dir>` — additively index `claude-plugin/<skill>/` and `cursor-plugin/<skill>/` packages.
|
|
21
|
+
- `agent-catalog add agents <dir>` — additively index `*.md` under any `agents/` folder.
|
|
22
|
+
- `agent-catalog add rules <dir>` — additively index `*.md` / `*.mdc` under any `rules/` or `instructions/` folder, plus `CLAUDE.md` / `AGENTS.md` memory files.
|
|
23
|
+
|
|
24
|
+
`add ...` append and dedupe by absolute path, so re-runs and overlap with `init` are safe.
|
|
25
|
+
|
|
26
|
+
## Discover / search (read the cache directly)
|
|
27
|
+
|
|
28
|
+
To list or filter what is available before installing, read `.agent-catalog.json`. It has `skills`, `agents`, `rules` arrays; each entry carries `name`, `description`, `path`, `source_root`. Filter on `name`/`description` to pick targets — no extra tooling needed.
|
|
29
|
+
|
|
30
|
+
## Install into the workspace or globally (via CLI)
|
|
31
|
+
|
|
32
|
+
Installation is done with the `pick` commands. `--target` chooses the root and `--assistant` the folder convention; together they decide where files land (`<target>/<assistant>/skills/<name>/`).
|
|
33
|
+
|
|
34
|
+
- Current workspace: `agent-catalog pick skills --target . --assistant .cursor` → `./.cursor/skills/<name>/`
|
|
35
|
+
- Global agent skills: `agent-catalog pick skills --target ~ --assistant .cursor` → `~/.cursor/skills/<name>/`
|
|
36
|
+
- Pre-filter a large catalog: `agent-catalog pick skills <term> --target . --assistant .cursor` (optional `<term>`, case-insensitive over name/description).
|
|
37
|
+
- Subagents and rules: same flags with `pick agents` / `pick rules`.
|
|
38
|
+
- Add `--overwrite` to replace assets that already exist at the destination.
|
|
39
|
+
|
|
40
|
+
`pick ...` opens an interactive checkbox (requires a TTY): run the command, then the human selects entries (type to filter, Space to toggle, Enter to confirm) and chooses the assistant if `--assistant` was omitted. `init` and `add ...` need no interaction.
|
|
41
|
+
|
|
42
|
+
## Install this skill
|
|
43
|
+
|
|
44
|
+
This management skill ships inside the package. Install it directly (non-interactive):
|
|
45
|
+
|
|
46
|
+
- Workspace: `agent-catalog install-skill --target . --assistant .cursor` → `./.cursor/skills/agent-catalog/`
|
|
47
|
+
- Global: `agent-catalog install-skill --target ~ --assistant .cursor` → `~/.cursor/skills/agent-catalog/`
|
|
48
|
+
|
|
49
|
+
Add `--overwrite` to refresh an existing copy.
|
|
50
|
+
|
|
51
|
+
## When not to use
|
|
52
|
+
|
|
53
|
+
- One-off file copies of a single known path — just copy it.
|
|
54
|
+
- Authoring or editing skill contents — use `create-skill`. This skill catalogs and installs existing assets; it does not write them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-catalog-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Scan a directory for AI-assistant skills, rules and subagents and copy selected ones into a chosen assistant's folder layout."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
"""Typer CLI: index AI-assistant artifacts and copy selected ones into a project."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
import questionary
|
|
9
|
-
import typer
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.table import Table
|
|
12
|
-
|
|
13
|
-
from . import cache as cache_mod
|
|
14
|
-
from . import destinations as dest_mod
|
|
15
|
-
from . import scanner
|
|
16
|
-
from .installer import install
|
|
17
|
-
from .models import Artifact, Catalog, Kind
|
|
18
|
-
|
|
19
|
-
app = typer.Typer(
|
|
20
|
-
help="Scan a directory for AI-assistant skills, rules and subagents, then copy selected ones into a project.",
|
|
21
|
-
no_args_is_help=True,
|
|
22
|
-
add_completion=False,
|
|
23
|
-
)
|
|
24
|
-
console = Console()
|
|
25
|
-
|
|
26
|
-
_KIND_LABEL = {"skill": "skills", "agent": "agents", "rule": "rules"}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@app.command()
|
|
30
|
-
def init(
|
|
31
|
-
target_dir: Path = typer.Argument(
|
|
32
|
-
...,
|
|
33
|
-
exists=True,
|
|
34
|
-
file_okay=False,
|
|
35
|
-
dir_okay=True,
|
|
36
|
-
readable=True,
|
|
37
|
-
resolve_path=True,
|
|
38
|
-
help="Directory to scan for .agents/.cursor/.claude/.github/.codex configs.",
|
|
39
|
-
),
|
|
40
|
-
) -> None:
|
|
41
|
-
"""Scan TARGET_DIR and cache discovered skills, subagents and rules."""
|
|
42
|
-
catalog = scanner.scan(target_dir)
|
|
43
|
-
path = cache_mod.write_cache(catalog)
|
|
44
|
-
|
|
45
|
-
table = Table(title="Indexed artifacts")
|
|
46
|
-
table.add_column("Kind")
|
|
47
|
-
table.add_column("Count", justify="right")
|
|
48
|
-
table.add_row("Skills", str(len(catalog.skills)))
|
|
49
|
-
table.add_row("Agents", str(len(catalog.agents)))
|
|
50
|
-
table.add_row("Rules", str(len(catalog.rules)))
|
|
51
|
-
console.print(table)
|
|
52
|
-
console.print(f"Cache written to [bold]{path}[/bold]")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _index_collections(target_dir: Path, folders: set[str]) -> None:
|
|
56
|
-
new = scanner.scan_collections(target_dir, folders)
|
|
57
|
-
catalog = cache_mod.append_skills(new)
|
|
58
|
-
console.print(
|
|
59
|
-
f"Indexed [bold]{len(new)}[/bold] skill(s); cache now has {len(catalog.skills)} skill(s)."
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@app.command("index-skills")
|
|
64
|
-
def index_skills(
|
|
65
|
-
target_dir: Path = typer.Argument(
|
|
66
|
-
...,
|
|
67
|
-
exists=True,
|
|
68
|
-
file_okay=False,
|
|
69
|
-
dir_okay=True,
|
|
70
|
-
readable=True,
|
|
71
|
-
resolve_path=True,
|
|
72
|
-
help="Directory to search for skills/<skill> packages.",
|
|
73
|
-
),
|
|
74
|
-
) -> None:
|
|
75
|
-
"""Find skills/<skill> packages under TARGET_DIR and add them to the cache."""
|
|
76
|
-
_index_collections(target_dir, scanner.SKILL_COLLECTION_DIRS)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@app.command("index-plugins")
|
|
80
|
-
def index_plugins(
|
|
81
|
-
target_dir: Path = typer.Argument(
|
|
82
|
-
...,
|
|
83
|
-
exists=True,
|
|
84
|
-
file_okay=False,
|
|
85
|
-
dir_okay=True,
|
|
86
|
-
readable=True,
|
|
87
|
-
resolve_path=True,
|
|
88
|
-
help="Directory to search for claude-plugin/<skill> and cursor-plugin/<skill> packages.",
|
|
89
|
-
),
|
|
90
|
-
) -> None:
|
|
91
|
-
"""Find claude-plugin/<skill> and cursor-plugin/<skill> packages under TARGET_DIR and add them to the cache."""
|
|
92
|
-
_index_collections(target_dir, scanner.PLUGIN_COLLECTION_DIRS)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _load_catalog() -> Catalog:
|
|
96
|
-
try:
|
|
97
|
-
return cache_mod.read_cache()
|
|
98
|
-
except FileNotFoundError:
|
|
99
|
-
console.print(
|
|
100
|
-
"[red]No cache found.[/red] Run [bold]agent-catalog init <target_dir>[/bold] first."
|
|
101
|
-
)
|
|
102
|
-
raise typer.Exit(code=1)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _choice_label(a: Artifact) -> str:
|
|
106
|
-
desc = a.description.strip().replace("\n", " ")
|
|
107
|
-
if len(desc) > 100:
|
|
108
|
-
desc = desc[:97] + "..."
|
|
109
|
-
suffix = f" - {desc}" if desc else ""
|
|
110
|
-
return f"[{a.source_root}] {a.name}{suffix}"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def _filter(artifacts: list[Artifact], term: Optional[str]) -> list[Artifact]:
|
|
114
|
-
if not term:
|
|
115
|
-
return artifacts
|
|
116
|
-
t = term.lower()
|
|
117
|
-
return [a for a in artifacts if t in a.name.lower()]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _pick(
|
|
121
|
-
kind: Kind,
|
|
122
|
-
target: Path,
|
|
123
|
-
assistant: Optional[str],
|
|
124
|
-
overwrite: bool,
|
|
125
|
-
term: Optional[str] = None,
|
|
126
|
-
) -> None:
|
|
127
|
-
catalog = _load_catalog()
|
|
128
|
-
artifacts = _filter(catalog.by_kind(kind), term)
|
|
129
|
-
label = _KIND_LABEL[kind]
|
|
130
|
-
|
|
131
|
-
if not artifacts:
|
|
132
|
-
suffix = f" matching '{term}'" if term else ""
|
|
133
|
-
console.print(f"[yellow]No {label}{suffix} indexed.[/yellow]")
|
|
134
|
-
raise typer.Exit(code=0)
|
|
135
|
-
|
|
136
|
-
choices = [questionary.Choice(title=_choice_label(a), value=a) for a in artifacts]
|
|
137
|
-
selected: list[Artifact] = (
|
|
138
|
-
questionary.checkbox(
|
|
139
|
-
f"Select {label} to copy (type to filter, Space to toggle, Enter to confirm):",
|
|
140
|
-
choices=choices,
|
|
141
|
-
use_search_filter=True,
|
|
142
|
-
use_jk_keys=False,
|
|
143
|
-
).ask()
|
|
144
|
-
or []
|
|
145
|
-
)
|
|
146
|
-
if not selected:
|
|
147
|
-
console.print("[yellow]Nothing selected.[/yellow]")
|
|
148
|
-
raise typer.Exit(code=0)
|
|
149
|
-
|
|
150
|
-
if assistant is None:
|
|
151
|
-
assistant = questionary.select(
|
|
152
|
-
"Copy into which coding assistant?",
|
|
153
|
-
choices=dest_mod.ASSISTANTS,
|
|
154
|
-
).ask()
|
|
155
|
-
if assistant is None:
|
|
156
|
-
console.print("[yellow]No assistant chosen.[/yellow]")
|
|
157
|
-
raise typer.Exit(code=0)
|
|
158
|
-
if assistant not in dest_mod.ASSISTANTS:
|
|
159
|
-
console.print(f"[red]Unknown assistant '{assistant}'.[/red]")
|
|
160
|
-
raise typer.Exit(code=1)
|
|
161
|
-
|
|
162
|
-
dest_dir = dest_mod.destination_dir(target, assistant, kind)
|
|
163
|
-
results = install(selected, dest_dir, overwrite=overwrite)
|
|
164
|
-
|
|
165
|
-
for r in results:
|
|
166
|
-
if r.status == "copied":
|
|
167
|
-
console.print(f"[green]copied[/green] {r.artifact.name} -> {r.destination}")
|
|
168
|
-
elif r.status == "skipped":
|
|
169
|
-
console.print(
|
|
170
|
-
f"[yellow]skipped[/yellow] {r.artifact.name} (exists; use --overwrite) -> {r.destination}"
|
|
171
|
-
)
|
|
172
|
-
else:
|
|
173
|
-
console.print(f"[red]missing[/red] {r.artifact.name} (source gone: {r.artifact.path})")
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@app.command("pick-skills")
|
|
177
|
-
def pick_skills(
|
|
178
|
-
target: Path = typer.Option(
|
|
179
|
-
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
180
|
-
),
|
|
181
|
-
assistant: Optional[str] = typer.Option(
|
|
182
|
-
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
183
|
-
),
|
|
184
|
-
overwrite: bool = typer.Option(
|
|
185
|
-
False,
|
|
186
|
-
"--overwrite",
|
|
187
|
-
help="Replace artifacts that already exist at the destination (skills merge over the folder, overwriting overlapping files; single files are replaced). Without it, existing targets are skipped.",
|
|
188
|
-
),
|
|
189
|
-
) -> None:
|
|
190
|
-
"""Interactively select indexed skills and copy them into a project."""
|
|
191
|
-
_pick("skill", target, assistant, overwrite)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@app.command("pick-agents")
|
|
195
|
-
def pick_agents(
|
|
196
|
-
target: Path = typer.Option(
|
|
197
|
-
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
198
|
-
),
|
|
199
|
-
assistant: Optional[str] = typer.Option(
|
|
200
|
-
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
201
|
-
),
|
|
202
|
-
overwrite: bool = typer.Option(
|
|
203
|
-
False,
|
|
204
|
-
"--overwrite",
|
|
205
|
-
help="Replace artifacts that already exist at the destination (skills merge over the folder, overwriting overlapping files; single files are replaced). Without it, existing targets are skipped.",
|
|
206
|
-
),
|
|
207
|
-
) -> None:
|
|
208
|
-
"""Interactively select indexed subagents and copy them into a project."""
|
|
209
|
-
_pick("agent", target, assistant, overwrite)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@app.command("pick-rules")
|
|
213
|
-
def pick_rules(
|
|
214
|
-
target: Path = typer.Option(
|
|
215
|
-
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
216
|
-
),
|
|
217
|
-
assistant: Optional[str] = typer.Option(
|
|
218
|
-
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
219
|
-
),
|
|
220
|
-
overwrite: bool = typer.Option(
|
|
221
|
-
False,
|
|
222
|
-
"--overwrite",
|
|
223
|
-
help="Replace artifacts that already exist at the destination (skills merge over the folder, overwriting overlapping files; single files are replaced). Without it, existing targets are skipped.",
|
|
224
|
-
),
|
|
225
|
-
) -> None:
|
|
226
|
-
"""Interactively select indexed rules and copy them into a project."""
|
|
227
|
-
_pick("rule", target, assistant, overwrite)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _resolve_term(term: Optional[str]) -> Optional[str]:
|
|
231
|
-
return term if term else questionary.text("Filter term:").ask()
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
@app.command("pick-skills:filter")
|
|
235
|
-
def pick_skills_filter(
|
|
236
|
-
term: Optional[str] = typer.Argument(None, help="Pre-filter by substring in name."),
|
|
237
|
-
target: Path = typer.Option(
|
|
238
|
-
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
239
|
-
),
|
|
240
|
-
assistant: Optional[str] = typer.Option(
|
|
241
|
-
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
242
|
-
),
|
|
243
|
-
overwrite: bool = typer.Option(
|
|
244
|
-
False,
|
|
245
|
-
"--overwrite",
|
|
246
|
-
help="Replace artifacts that already exist at the destination (skills merge over the folder, overwriting overlapping files; single files are replaced). Without it, existing targets are skipped.",
|
|
247
|
-
),
|
|
248
|
-
) -> None:
|
|
249
|
-
"""Pre-filter indexed skills by a term, then select and copy them."""
|
|
250
|
-
_pick("skill", target, assistant, overwrite, term=_resolve_term(term))
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@app.command("pick-agents:filter")
|
|
254
|
-
def pick_agents_filter(
|
|
255
|
-
term: Optional[str] = typer.Argument(None, help="Pre-filter by substring in name."),
|
|
256
|
-
target: Path = typer.Option(
|
|
257
|
-
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
258
|
-
),
|
|
259
|
-
assistant: Optional[str] = typer.Option(
|
|
260
|
-
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
261
|
-
),
|
|
262
|
-
overwrite: bool = typer.Option(
|
|
263
|
-
False,
|
|
264
|
-
"--overwrite",
|
|
265
|
-
help="Replace artifacts that already exist at the destination (skills merge over the folder, overwriting overlapping files; single files are replaced). Without it, existing targets are skipped.",
|
|
266
|
-
),
|
|
267
|
-
) -> None:
|
|
268
|
-
"""Pre-filter indexed subagents by a term, then select and copy them."""
|
|
269
|
-
_pick("agent", target, assistant, overwrite, term=_resolve_term(term))
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
@app.command("pick-rules:filter")
|
|
273
|
-
def pick_rules_filter(
|
|
274
|
-
term: Optional[str] = typer.Argument(None, help="Pre-filter by substring in name."),
|
|
275
|
-
target: Path = typer.Option(
|
|
276
|
-
Path.cwd(), "--target", "-t", resolve_path=True, help="Project root to copy into."
|
|
277
|
-
),
|
|
278
|
-
assistant: Optional[str] = typer.Option(
|
|
279
|
-
None, "--assistant", "-a", help="Skip the prompt and use this assistant root."
|
|
280
|
-
),
|
|
281
|
-
overwrite: bool = typer.Option(
|
|
282
|
-
False,
|
|
283
|
-
"--overwrite",
|
|
284
|
-
help="Replace artifacts that already exist at the destination (skills merge over the folder, overwriting overlapping files; single files are replaced). Without it, existing targets are skipped.",
|
|
285
|
-
),
|
|
286
|
-
) -> None:
|
|
287
|
-
"""Pre-filter indexed rules by a term, then select and copy them."""
|
|
288
|
-
_pick("rule", target, assistant, overwrite, term=_resolve_term(term))
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if __name__ == "__main__":
|
|
292
|
-
app()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|