auto-context-cli 1.0.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.
- auto_context/__init__.py +3 -0
- auto_context/commands/__init__.py +2 -0
- auto_context/commands/import_cmd.py +144 -0
- auto_context/commands/init_cmd.py +309 -0
- auto_context/commands/mcp.py +304 -0
- auto_context/commands/memory.py +236 -0
- auto_context/commands/share.py +122 -0
- auto_context/config.py +65 -0
- auto_context/main.py +121 -0
- auto_context/security.py +162 -0
- auto_context/templates/AGENTS.template.md +49 -0
- auto_context/templates/GEMINI.template.md +49 -0
- auto_context/templates/context.local.template.md +52 -0
- auto_context/templates/context.template.md +94 -0
- auto_context/templates/maintain_context.template.md +36 -0
- auto_context/templates/mcp_config.template.json +13 -0
- auto_context/templates/profile.template.md +20 -0
- auto_context/templates/rule.template.md +19 -0
- auto_context/templates/rules/code-standards.md +24 -0
- auto_context/templates/rules/security-boundaries.md +21 -0
- auto_context/templates/skill.template.md +39 -0
- auto_context/templates/skills/browser-testing/SKILL.md +59 -0
- auto_context/templates/skills/docker-ops/SKILL.md +79 -0
- auto_context/templates/skills/git-workflow/SKILL.md +59 -0
- auto_context/templates/skills/server-diagnostics/SKILL.md +55 -0
- auto_context_cli-1.0.0.dist-info/METADATA +577 -0
- auto_context_cli-1.0.0.dist-info/RECORD +30 -0
- auto_context_cli-1.0.0.dist-info/WHEEL +4 -0
- auto_context_cli-1.0.0.dist-info/entry_points.txt +2 -0
- auto_context_cli-1.0.0.dist-info/licenses/LICENSE +0 -0
auto_context/__init__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
commands/import_cmd.py — Fetch and install a skill or rule from a Gist or URL.
|
|
3
|
+
|
|
4
|
+
1. Resolves GitHub Gist HTML URLs to raw content URLs.
|
|
5
|
+
2. Downloads content (httpx with urllib fallback).
|
|
6
|
+
3. Runs security audit on downloaded content.
|
|
7
|
+
4. Detects skill vs rule from frontmatter.
|
|
8
|
+
5. Handles file collisions with interactive overwrite / rename / cancel prompt.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
import urllib.request
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Annotated, Optional
|
|
18
|
+
from urllib.error import URLError
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from auto_context import config, security
|
|
24
|
+
from auto_context.config import AGENTS_SUBDIR
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
# Regex: https://gist.github.com/<user>/<id>
|
|
29
|
+
_GIST_HTML_RE = re.compile(r"^https?://gist\.github\.com/([^/]+)/([a-f0-9]+)$")
|
|
30
|
+
# Frontmatter field matchers
|
|
31
|
+
_NAME_RE = re.compile(r"(?m)^name:\s*[\"']?([a-zA-Z0-9_\-]+)")
|
|
32
|
+
_SKILL_RE = re.compile(r"(?m)^triggers\s*:")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve_url(url: str) -> str:
|
|
36
|
+
"""Convert a Gist HTML URL to its raw content URL."""
|
|
37
|
+
m = _GIST_HTML_RE.match(url.strip())
|
|
38
|
+
if m:
|
|
39
|
+
return f"https://gist.githubusercontent.com/{m.group(1)}/{m.group(2)}/raw"
|
|
40
|
+
return url
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _fetch(url: str) -> str:
|
|
44
|
+
"""Download URL content as a string. Tries httpx first, falls back to urllib."""
|
|
45
|
+
try:
|
|
46
|
+
import httpx
|
|
47
|
+
resp = httpx.get(url, follow_redirects=True, timeout=30)
|
|
48
|
+
resp.raise_for_status()
|
|
49
|
+
return resp.text
|
|
50
|
+
except (ImportError, Exception):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with urllib.request.urlopen(url, timeout=30) as resp: # noqa: S310
|
|
55
|
+
return resp.read().decode("utf-8")
|
|
56
|
+
except URLError as exc:
|
|
57
|
+
raise typer.BadParameter(f"Failed to download content from: {url}\n{exc}") from exc
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _detect_type(content: str) -> str:
|
|
61
|
+
"""Return 'skill' or 'rule' based on frontmatter inspection."""
|
|
62
|
+
if _SKILL_RE.search(content) or "# Skill:" in content:
|
|
63
|
+
return "skill"
|
|
64
|
+
return "rule"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _dest_path(clean_name: str, item_type: str, project_dir: Path, global_: bool) -> Path:
|
|
68
|
+
if global_:
|
|
69
|
+
if item_type == "skill":
|
|
70
|
+
return config.GLOBAL_PLUGIN_DIR / "skills" / clean_name / "SKILL.md"
|
|
71
|
+
return config.GLOBAL_PLUGIN_DIR / "rules" / f"{clean_name}.md"
|
|
72
|
+
else:
|
|
73
|
+
if item_type == "skill":
|
|
74
|
+
return project_dir / AGENTS_SUBDIR / "skills" / clean_name / "SKILL.md"
|
|
75
|
+
return project_dir / AGENTS_SUBDIR / "rules" / f"{clean_name}.md"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def import_item(
|
|
79
|
+
url: Annotated[str, typer.Argument(help="Gist URL or raw content URL to import.")],
|
|
80
|
+
target: Annotated[Optional[Path], typer.Option("--target", "-t")] = None,
|
|
81
|
+
as_name: Annotated[Optional[str], typer.Option("--as", help="Override imported item name.")] = None,
|
|
82
|
+
global_: Annotated[bool, typer.Option("--global", "-g")] = False,
|
|
83
|
+
force: Annotated[bool, typer.Option("--force", "-f")] = False,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Audit and import a skill or rule from a GitHub Gist or URL."""
|
|
86
|
+
project_dir = (target or Path.cwd()).resolve()
|
|
87
|
+
raw_url = _resolve_url(url)
|
|
88
|
+
|
|
89
|
+
console.print(f"\n[bold cyan]📥 Importing External Instruction[/]")
|
|
90
|
+
console.print(f" [cyan][*] Fetching:[/] {raw_url}")
|
|
91
|
+
content = _fetch(raw_url)
|
|
92
|
+
|
|
93
|
+
# Write to a temp file so the security scanner can work on it
|
|
94
|
+
with tempfile.NamedTemporaryFile(
|
|
95
|
+
mode="w", suffix=".md", encoding="utf-8", delete=False
|
|
96
|
+
) as tmp:
|
|
97
|
+
tmp.write(content)
|
|
98
|
+
tmp_path = Path(tmp.name)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
console.print(" [cyan][*] Running pre-import security audit...[/]")
|
|
102
|
+
violations = security.audit_path(tmp_path, single_file=True)
|
|
103
|
+
security.abort_if_violations(violations)
|
|
104
|
+
|
|
105
|
+
# Detect metadata
|
|
106
|
+
name_match = _NAME_RE.search(content)
|
|
107
|
+
detected_name = name_match.group(1).strip() if name_match else ""
|
|
108
|
+
item_type = _detect_type(content)
|
|
109
|
+
ts_fallback = f"imported-{int(time.time())}"
|
|
110
|
+
final_name = (as_name or detected_name or ts_fallback).lower().replace(" ", "-").replace("_", "-").removesuffix(".md")
|
|
111
|
+
|
|
112
|
+
scope = "GLOBAL Plugin" if global_ else "LOCAL Project"
|
|
113
|
+
dest = _dest_path(final_name, item_type, project_dir, global_)
|
|
114
|
+
|
|
115
|
+
# Collision guard
|
|
116
|
+
if dest.exists() and not force:
|
|
117
|
+
console.print(f"\n[yellow][!] Collision:[/] '{final_name}' already exists at:\n {dest}")
|
|
118
|
+
choice = typer.prompt("Action? [O]verwrite / [R]ename / [C]ancel", default="C")
|
|
119
|
+
|
|
120
|
+
if choice.strip().upper().startswith("O"):
|
|
121
|
+
console.print(" [yellow]Overwriting...[/]")
|
|
122
|
+
elif choice.strip().upper().startswith("R"):
|
|
123
|
+
new_name = typer.prompt("New name").strip()
|
|
124
|
+
if not new_name:
|
|
125
|
+
console.print("[red][✗] Name cannot be empty. Import cancelled.[/]")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
final_name = new_name.lower().replace(" ", "-").replace("_", "-").removesuffix(".md")
|
|
128
|
+
dest = _dest_path(final_name, item_type, project_dir, global_)
|
|
129
|
+
else:
|
|
130
|
+
console.print("[cyan][*] Import cancelled.[/]\n")
|
|
131
|
+
raise typer.Exit(0)
|
|
132
|
+
|
|
133
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
dest.write_text(content, encoding="utf-8")
|
|
135
|
+
|
|
136
|
+
finally:
|
|
137
|
+
tmp_path.unlink(missing_ok=True)
|
|
138
|
+
|
|
139
|
+
console.print(f"\n[bold green]✨ Imported Successfully![/]")
|
|
140
|
+
console.print(f" [yellow]Type: [/] {item_type.upper()}")
|
|
141
|
+
console.print(f" [yellow]Name: [/] {final_name}")
|
|
142
|
+
console.print(f" [cyan]Scope:[/] {scope}")
|
|
143
|
+
console.print(f" [green]File: [/] {dest}\n")
|
|
144
|
+
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
commands/init_cmd.py — Workspace scaffolding, stack detection,
|
|
3
|
+
rule and skill scaffolding commands.
|
|
4
|
+
|
|
5
|
+
Commands exposed:
|
|
6
|
+
init — scaffold the current (or target) project directory
|
|
7
|
+
rule — scaffold a new agent rule file
|
|
8
|
+
skill — scaffold a new agent skill directory
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import shutil
|
|
14
|
+
from datetime import date
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Annotated, Optional
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
|
|
22
|
+
from auto_context import config
|
|
23
|
+
from auto_context.config import AGENTS_SUBDIR, GEMINI_SUBDIR, get_templates_dir
|
|
24
|
+
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
# ── Template variable substitution ────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
def _render(template_path: Path, variables: dict[str, str]) -> str:
|
|
32
|
+
"""Read a template file and replace {{KEY}} placeholders."""
|
|
33
|
+
text = template_path.read_text(encoding="utf-8")
|
|
34
|
+
for key, val in variables.items():
|
|
35
|
+
text = text.replace("{{" + key + "}}", val)
|
|
36
|
+
return text
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _write_if_missing(dest: Path, content: str, *, force: bool, dry_run: bool) -> bool:
|
|
40
|
+
"""Write *content* to *dest*. Return True if a file was written."""
|
|
41
|
+
if dest.exists() and not force:
|
|
42
|
+
console.print(f" [yellow][*] Preserved:[/] {dest.relative_to(dest.anchor)} (use --force to overwrite)")
|
|
43
|
+
return False
|
|
44
|
+
if dry_run:
|
|
45
|
+
console.print(f" [yellow][!] Dry-run:[/] Would write → {dest}")
|
|
46
|
+
return False
|
|
47
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
dest.write_text(content, encoding="utf-8")
|
|
49
|
+
console.print(f" [green][✓] Generated:[/] {dest}")
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _copy_if_missing(src: Path, dest: Path, *, force: bool, dry_run: bool) -> bool:
|
|
54
|
+
"""Copy *src* to *dest*. Return True if a file was written."""
|
|
55
|
+
if dest.exists() and not force:
|
|
56
|
+
console.print(f" [yellow][*] Preserved:[/] {dest}")
|
|
57
|
+
return False
|
|
58
|
+
if dry_run:
|
|
59
|
+
console.print(f" [yellow][!] Dry-run:[/] Would copy {src.name} → {dest}")
|
|
60
|
+
return False
|
|
61
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
shutil.copy2(src, dest)
|
|
63
|
+
console.print(f" [green][✓] Copied:[/] {dest}")
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Stack detection ───────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
_JS_FRAMEWORKS = {
|
|
70
|
+
"next": "Next.js", "react": "React", "vue": "Vue.js",
|
|
71
|
+
"express": "Express.js", "fastify": "Fastify", "nest": "NestJS",
|
|
72
|
+
}
|
|
73
|
+
_PY_FRAMEWORKS = {
|
|
74
|
+
"fastapi": "FastAPI", "flask": "Flask", "django": "Django",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_stack(project_dir: Path) -> tuple[str, str]:
|
|
79
|
+
"""Return (stack_label, framework_label) for the project directory."""
|
|
80
|
+
for filename, stack, kind in config.STACK_INDICATORS:
|
|
81
|
+
indicator = project_dir / filename
|
|
82
|
+
if not indicator.exists():
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if kind == "js":
|
|
86
|
+
content = indicator.read_text(encoding="utf-8", errors="ignore").lower()
|
|
87
|
+
fw = next((fw for key, fw in _JS_FRAMEWORKS.items() if key in content), "Not detected")
|
|
88
|
+
return stack, fw
|
|
89
|
+
|
|
90
|
+
if kind == "py":
|
|
91
|
+
# Check manage.py for Django first
|
|
92
|
+
if (project_dir / "manage.py").exists():
|
|
93
|
+
return stack, "Django"
|
|
94
|
+
# scan all py indicator files
|
|
95
|
+
combined = ""
|
|
96
|
+
for py_file in ("requirements.txt", "pyproject.toml", "Pipfile"):
|
|
97
|
+
p = project_dir / py_file
|
|
98
|
+
if p.exists():
|
|
99
|
+
combined += p.read_text(encoding="utf-8", errors="ignore").lower()
|
|
100
|
+
fw = next((fw for key, fw in _PY_FRAMEWORKS.items() if key in combined), "Not detected")
|
|
101
|
+
return stack, fw
|
|
102
|
+
|
|
103
|
+
return stack, "Standard"
|
|
104
|
+
|
|
105
|
+
return "General / Multi-stack", "Not detected"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── .gitignore helper ─────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def _ensure_gitignore_entries(project_dir: Path, entries: list[str]) -> None:
|
|
111
|
+
"""Append *entries* to .gitignore if not already present."""
|
|
112
|
+
gitignore = project_dir / ".gitignore"
|
|
113
|
+
existing = gitignore.read_text(encoding="utf-8") if gitignore.exists() else ""
|
|
114
|
+
missing = [e for e in entries if e not in existing]
|
|
115
|
+
if missing:
|
|
116
|
+
with gitignore.open("a", encoding="utf-8") as fh:
|
|
117
|
+
fh.write("\n# auto-context: protected files\n")
|
|
118
|
+
fh.write("\n".join(missing) + "\n")
|
|
119
|
+
console.print(f" [green][✓] Updated:[/] .gitignore ({', '.join(missing)})")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── init command ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def init(
|
|
125
|
+
target: Annotated[Optional[Path], typer.Argument(help="Target project directory.")] = None,
|
|
126
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Overwrite existing files.")] = False,
|
|
127
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", "-d", help="Preview without writing.")] = False,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Scaffold autonomous context files into a project directory."""
|
|
130
|
+
project_dir = (target or Path.cwd()).resolve()
|
|
131
|
+
project_name = project_dir.name
|
|
132
|
+
stack, framework = detect_stack(project_dir)
|
|
133
|
+
date_now = date.today().isoformat()
|
|
134
|
+
templates = get_templates_dir() # importlib.resources-safe path
|
|
135
|
+
|
|
136
|
+
console.print(Panel(
|
|
137
|
+
f"[bold cyan]🚀 Auto-Context: Initializing Autonomous Context Manager[/]\n"
|
|
138
|
+
f"[yellow] Target:[/] {project_name} ({project_dir})\n"
|
|
139
|
+
f"[cyan] Stack: [/] {stack} ([dim]{framework}[/])",
|
|
140
|
+
border_style="cyan",
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
if dry_run:
|
|
144
|
+
console.print("[yellow][!] Dry-run mode: No files will be written.\n")
|
|
145
|
+
|
|
146
|
+
variables = {
|
|
147
|
+
"PROJECT_NAME": project_name,
|
|
148
|
+
"PROJECT_DIR": str(project_dir),
|
|
149
|
+
"STACK": stack,
|
|
150
|
+
"FRAMEWORK": framework,
|
|
151
|
+
"DATE_NOW": date_now,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ── Rules ────────────────────────────────────────────────────────────────
|
|
155
|
+
rules_dest = project_dir / AGENTS_SUBDIR / "rules"
|
|
156
|
+
for rule_file in (templates / "rules").glob("*.md"):
|
|
157
|
+
_copy_if_missing(rule_file, rules_dest / rule_file.name, force=force, dry_run=dry_run)
|
|
158
|
+
|
|
159
|
+
maintain_tmpl = templates / "maintain_context.template.md"
|
|
160
|
+
if maintain_tmpl.exists():
|
|
161
|
+
content = _render(maintain_tmpl, variables)
|
|
162
|
+
_write_if_missing(rules_dest / "maintain_context.md", content, force=force, dry_run=dry_run)
|
|
163
|
+
|
|
164
|
+
# ── Skills ───────────────────────────────────────────────────────────────
|
|
165
|
+
skills_dest = project_dir / AGENTS_SUBDIR / "skills"
|
|
166
|
+
for skill_dir in (templates / "skills").iterdir():
|
|
167
|
+
if skill_dir.is_dir():
|
|
168
|
+
skill_md = skill_dir / "SKILL.md"
|
|
169
|
+
if skill_md.exists():
|
|
170
|
+
_copy_if_missing(
|
|
171
|
+
skill_md,
|
|
172
|
+
skills_dest / skill_dir.name / "SKILL.md",
|
|
173
|
+
force=force, dry_run=dry_run,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# ── Root context files ────────────────────────────────────────────────────
|
|
177
|
+
for tmpl_name, dest_name in [
|
|
178
|
+
("context.template.md", "context.md"),
|
|
179
|
+
("context.local.template.md", "context.local.md"),
|
|
180
|
+
("GEMINI.template.md", "GEMINI.md"),
|
|
181
|
+
("AGENTS.template.md", "AGENTS.md"),
|
|
182
|
+
]:
|
|
183
|
+
tmpl = templates / tmpl_name
|
|
184
|
+
if tmpl.exists():
|
|
185
|
+
content = _render(tmpl, variables)
|
|
186
|
+
_write_if_missing(project_dir / dest_name, content, force=force, dry_run=dry_run)
|
|
187
|
+
|
|
188
|
+
# ── MCP config stub ───────────────────────────────────────────────────────
|
|
189
|
+
mcp_tmpl = templates / "mcp_config.template.json"
|
|
190
|
+
if mcp_tmpl.exists():
|
|
191
|
+
for mcp_dir in [
|
|
192
|
+
project_dir / GEMINI_SUBDIR,
|
|
193
|
+
project_dir / AGENTS_SUBDIR,
|
|
194
|
+
]:
|
|
195
|
+
_copy_if_missing(
|
|
196
|
+
mcp_tmpl,
|
|
197
|
+
mcp_dir / "mcp_config.json",
|
|
198
|
+
force=force, dry_run=dry_run,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# ── .gitignore ────────────────────────────────────────────────────────────
|
|
202
|
+
if not dry_run:
|
|
203
|
+
_ensure_gitignore_entries(project_dir, [
|
|
204
|
+
"context.local.md",
|
|
205
|
+
".env",
|
|
206
|
+
".env.local",
|
|
207
|
+
".env.*.local",
|
|
208
|
+
])
|
|
209
|
+
|
|
210
|
+
console.print(Panel(
|
|
211
|
+
"[bold green]✨ Project scaffold complete![/]\n"
|
|
212
|
+
"Edit [cyan]context.md[/] and [cyan]GEMINI.md[/] to customise your AI workspace.",
|
|
213
|
+
border_style="green",
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── rule command ──────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def new_rule(
|
|
220
|
+
name: Annotated[str, typer.Argument(help="Rule name (e.g. code-standards).")],
|
|
221
|
+
target: Annotated[Optional[Path], typer.Option("--target", "-t")] = None,
|
|
222
|
+
global_: Annotated[bool, typer.Option("--global", "-g", help="Install into global plugin.")] = False,
|
|
223
|
+
force: Annotated[bool, typer.Option("--force", "-f")] = False,
|
|
224
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", "-d")] = False,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Scaffold a new agent rule file."""
|
|
227
|
+
clean_name = name.lower().replace(" ", "-").replace("_", "-").removesuffix(".md")
|
|
228
|
+
title = clean_name.replace("-", " ").title()
|
|
229
|
+
|
|
230
|
+
dest_dir = (
|
|
231
|
+
config.GLOBAL_PLUGIN_DIR / "rules"
|
|
232
|
+
if global_
|
|
233
|
+
else (target or Path.cwd()).resolve() / AGENTS_SUBDIR / "rules"
|
|
234
|
+
)
|
|
235
|
+
dest_file = dest_dir / f"{clean_name}.md"
|
|
236
|
+
|
|
237
|
+
if dest_file.exists() and not force:
|
|
238
|
+
console.print(f"[yellow][!] Rule already exists:[/] {dest_file}\n Use --force to overwrite.")
|
|
239
|
+
raise typer.Exit(1)
|
|
240
|
+
|
|
241
|
+
tmpl = get_templates_dir() / "rule.template.md"
|
|
242
|
+
if tmpl.exists():
|
|
243
|
+
content = _render(tmpl, {"RULE_NAME": clean_name, "RULE_TITLE": title})
|
|
244
|
+
else:
|
|
245
|
+
content = (
|
|
246
|
+
f"---\nname: {clean_name}\ndescription: Guidelines for {title}.\ntrigger: always_on\n---\n\n"
|
|
247
|
+
f"# Rule: {title}\n\n## Objective\n\n## Guidelines & Constraints\n"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
_write_if_missing(dest_file, content, force=force, dry_run=dry_run)
|
|
251
|
+
if global_ and config.MEMORY_DIR.exists():
|
|
252
|
+
mem_dest = config.MEMORY_DIR / "rules" / f"{clean_name}.md"
|
|
253
|
+
_write_if_missing(mem_dest, content, force=force, dry_run=dry_run)
|
|
254
|
+
scope = "GLOBAL Plugin & Central Memory" if global_ else f"LOCAL ({dest_dir})"
|
|
255
|
+
console.print(Panel(
|
|
256
|
+
f"[bold green]✨ New Rule Created![/]\n"
|
|
257
|
+
f"[yellow] Name:[/] {clean_name}\n"
|
|
258
|
+
f"[cyan] Scope:[/] {scope}\n"
|
|
259
|
+
f"[green] File: [/] {dest_file}",
|
|
260
|
+
border_style="cyan",
|
|
261
|
+
))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ── skill command ─────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
def new_skill(
|
|
267
|
+
name: Annotated[str, typer.Argument(help="Skill name (e.g. deploy-docker).")],
|
|
268
|
+
target: Annotated[Optional[Path], typer.Option("--target", "-t")] = None,
|
|
269
|
+
global_: Annotated[bool, typer.Option("--global", "-g")] = False,
|
|
270
|
+
force: Annotated[bool, typer.Option("--force", "-f")] = False,
|
|
271
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", "-d")] = False,
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Scaffold a new agent skill directory."""
|
|
274
|
+
clean_name = name.lower().replace(" ", "-").replace("_", "-")
|
|
275
|
+
title = clean_name.replace("-", " ").title()
|
|
276
|
+
|
|
277
|
+
dest_dir = (
|
|
278
|
+
config.GLOBAL_PLUGIN_DIR / "skills" / clean_name
|
|
279
|
+
if global_
|
|
280
|
+
else (target or Path.cwd()).resolve() / AGENTS_SUBDIR / "skills" / clean_name
|
|
281
|
+
)
|
|
282
|
+
dest_file = dest_dir / "SKILL.md"
|
|
283
|
+
|
|
284
|
+
if dest_file.exists() and not force:
|
|
285
|
+
console.print(f"[yellow][!] Skill already exists:[/] {dest_file}\n Use --force to overwrite.")
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
tmpl = get_templates_dir() / "skill.template.md"
|
|
289
|
+
if tmpl.exists():
|
|
290
|
+
content = _render(tmpl, {"SKILL_NAME": clean_name, "SKILL_TITLE": title})
|
|
291
|
+
else:
|
|
292
|
+
content = (
|
|
293
|
+
f"---\nname: {clean_name}\nversion: 1.0.0\n"
|
|
294
|
+
f"description: Skill for {title}.\ntriggers: [\"{clean_name}\"]\n---\n\n"
|
|
295
|
+
f"# Skill: {title}\n\n## Overview\n\n## When to Activate\n\n## Workflow\n"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
_write_if_missing(dest_file, content, force=force, dry_run=dry_run)
|
|
299
|
+
if global_ and config.MEMORY_DIR.exists():
|
|
300
|
+
mem_dest = config.MEMORY_DIR / "skills" / clean_name / "SKILL.md"
|
|
301
|
+
_write_if_missing(mem_dest, content, force=force, dry_run=dry_run)
|
|
302
|
+
scope = "GLOBAL Plugin & Central Memory" if global_ else f"LOCAL ({dest_dir})"
|
|
303
|
+
console.print(Panel(
|
|
304
|
+
f"[bold green]🎯 New Skill Created![/]\n"
|
|
305
|
+
f"[yellow] Name:[/] {clean_name}\n"
|
|
306
|
+
f"[cyan] Scope:[/] {scope}\n"
|
|
307
|
+
f"[green] File: [/] {dest_file}",
|
|
308
|
+
border_style="cyan",
|
|
309
|
+
))
|