@softspark/ai-toolkit 1.1.0 → 1.2.0
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.
- package/CHANGELOG.md +20 -0
- package/README.md +20 -6
- package/app/.claude-plugin/plugin.json +2 -2
- package/app/ARCHITECTURE.md +7 -5
- package/app/hooks/pre-compact.sh +44 -10
- package/app/personas/backend-lead.md +27 -0
- package/app/personas/devops-eng.md +27 -0
- package/app/personas/frontend-lead.md +26 -0
- package/app/personas/junior-dev.md +28 -0
- package/app/rules/claude-toolkit-rules.md +1 -1
- package/app/skills/api-patterns/SKILL.md +1 -0
- package/app/skills/app-builder/SKILL.md +1 -0
- package/app/skills/ci-cd-patterns/SKILL.md +1 -0
- package/app/skills/clean-code/SKILL.md +1 -0
- package/app/skills/csharp-patterns/SKILL.md +1 -0
- package/app/skills/database-patterns/SKILL.md +1 -0
- package/app/skills/debugging-tactics/SKILL.md +1 -1
- package/app/skills/design-engineering/SKILL.md +1 -0
- package/app/skills/docker-devops/SKILL.md +1 -0
- package/app/skills/documentation-standards/SKILL.md +1 -0
- package/app/skills/ecommerce-patterns/SKILL.md +1 -0
- package/app/skills/flutter-patterns/SKILL.md +1 -0
- package/app/skills/git-mastery/SKILL.md +1 -1
- package/app/skills/hive-mind/SKILL.md +1 -0
- package/app/skills/java-patterns/SKILL.md +1 -0
- package/app/skills/kotlin-patterns/SKILL.md +1 -0
- package/app/skills/mcp-patterns/SKILL.md +1 -0
- package/app/skills/migration-patterns/SKILL.md +1 -0
- package/app/skills/observability-patterns/SKILL.md +1 -0
- package/app/skills/onboard/SKILL.md +2 -0
- package/app/skills/orchestrate/SKILL.md +1 -1
- package/app/skills/performance-profiling/SKILL.md +1 -1
- package/app/skills/persona/SKILL.md +57 -0
- package/app/skills/plan-writing/SKILL.md +1 -0
- package/app/skills/pr/scripts/pr-summary.py +6 -3
- package/app/skills/rag-patterns/SKILL.md +1 -0
- package/app/skills/research-mastery/SKILL.md +1 -0
- package/app/skills/ruby-patterns/SKILL.md +1 -0
- package/app/skills/rust-patterns/SKILL.md +1 -0
- package/app/skills/security-patterns/SKILL.md +1 -0
- package/app/skills/skill-audit/SKILL.md +137 -0
- package/app/skills/subagent-development/SKILL.md +1 -1
- package/app/skills/swarm/SKILL.md +1 -1
- package/app/skills/swift-patterns/SKILL.md +1 -1
- package/app/skills/teams/SKILL.md +1 -1
- package/app/skills/testing-patterns/SKILL.md +1 -0
- package/app/skills/typescript-patterns/SKILL.md +1 -0
- package/app/skills/verification-before-completion/SKILL.md +1 -0
- package/app/skills/workflow/SKILL.md +1 -1
- package/kb/reference/architecture-overview.md +28 -5
- package/kb/reference/hooks-catalog.md +6 -4
- package/kb/reference/skills-catalog.md +5 -3
- package/llms.txt +2 -2
- package/package.json +3 -2
- package/scripts/audit_skills.py +290 -0
- package/scripts/generate_augment.py +46 -0
- package/scripts/install.py +32 -2
- package/scripts/install_steps/ai_tools.py +10 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Skill & Agent Security Auditor.
|
|
3
|
+
|
|
4
|
+
Deterministic scanner for ai-toolkit skills and agents.
|
|
5
|
+
Detects dangerous code patterns, hardcoded secrets, and permission issues.
|
|
6
|
+
|
|
7
|
+
Stdlib-only. JSON output to stdout. Non-zero exit on HIGH findings.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 scripts/audit_skills.py [toolkit-dir] # scan all
|
|
11
|
+
python3 scripts/audit_skills.py [toolkit-dir] --json # JSON output
|
|
12
|
+
python3 scripts/audit_skills.py [toolkit-dir] --ci # exit 1 on HIGH
|
|
13
|
+
|
|
14
|
+
Exit codes:
|
|
15
|
+
0 no HIGH findings
|
|
16
|
+
1 HIGH findings detected (or errors)
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
26
|
+
from _common import toolkit_dir as default_toolkit_dir
|
|
27
|
+
from frontmatter import frontmatter_field
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Pattern definitions
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
PYTHON_HIGH = [
|
|
34
|
+
(r'\beval\s*\(', "eval() — arbitrary code execution"),
|
|
35
|
+
(r'\bexec\s*\(', "exec() — arbitrary code execution"),
|
|
36
|
+
(r'\bos\.system\s*\(', "os.system() — shell injection risk"),
|
|
37
|
+
(r'subprocess\.[a-z]+\(.*shell\s*=\s*True', "subprocess with shell=True — shell injection"),
|
|
38
|
+
(r'\b__import__\s*\(', "__import__() — dynamic import"),
|
|
39
|
+
(r'\bpickle\.loads?\s*\(', "pickle.load/loads — deserialization attack"),
|
|
40
|
+
(r'\bmarshall?\.loads?\s*\(', "marshal.load/loads — deserialization attack"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
PYTHON_WARN = [
|
|
44
|
+
(r'open\s*\(.*["\']w["\']', "open() with write mode — verify path is validated"),
|
|
45
|
+
(r'\bcompile\s*\(.*\bexec\b', "compile() with exec — dynamic code generation"),
|
|
46
|
+
(r'\bgetattr\s*\(.*input', "getattr with user input — attribute injection risk"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
BASH_HIGH = [
|
|
50
|
+
(r'curl\s+.*\|\s*(ba)?sh', "curl | bash — remote code execution"),
|
|
51
|
+
(r'wget\s+.*\|\s*(ba)?sh', "wget | bash — remote code execution"),
|
|
52
|
+
(r'rm\s+-rf\s+/', "rm -rf / — destructive"),
|
|
53
|
+
(r'rm\s+-rf\s+~', "rm -rf ~ — destructive"),
|
|
54
|
+
(r'rm\s+-rf\s+\$HOME', "rm -rf $HOME — destructive"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
BASH_WARN = [
|
|
58
|
+
(r'chmod\s+(-R\s+)?777', "chmod 777 — overly permissive"),
|
|
59
|
+
(r'eval\s+"\$', "eval with variable — injection risk"),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
SECRET_PATTERNS = [
|
|
63
|
+
(r'AKIA[0-9A-Z]{16}', "HIGH", "AWS access key"),
|
|
64
|
+
(r'sk-[a-zA-Z0-9]{20,}', "HIGH", "API key (sk-* pattern)"),
|
|
65
|
+
(r'ghp_[a-zA-Z0-9]{36}', "HIGH", "GitHub personal access token"),
|
|
66
|
+
(r'gho_[a-zA-Z0-9]{36}', "HIGH", "GitHub OAuth token"),
|
|
67
|
+
(r'-----BEGIN\s+(RSA|DSA|EC|OPENSSH)?\s*PRIVATE\s+KEY', "HIGH", "Private key"),
|
|
68
|
+
(r'password\s*=\s*["\'][^"\']{4,}["\']', "WARN", "Hardcoded password"),
|
|
69
|
+
(r'token\s*=\s*["\'][^"\']{8,}["\']', "WARN", "Hardcoded token"),
|
|
70
|
+
(r'secret\s*=\s*["\'][^"\']{8,}["\']', "WARN", "Hardcoded secret"),
|
|
71
|
+
(r'api_key\s*=\s*["\'][^"\']{8,}["\']', "WARN", "Hardcoded API key"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Scanner
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class Finding:
|
|
79
|
+
__slots__ = ("severity", "file", "line", "pattern", "description")
|
|
80
|
+
|
|
81
|
+
def __init__(self, severity: str, file: str, line: int,
|
|
82
|
+
pattern: str, description: str) -> None:
|
|
83
|
+
self.severity = severity
|
|
84
|
+
self.file = file
|
|
85
|
+
self.line = line
|
|
86
|
+
self.pattern = pattern
|
|
87
|
+
self.description = description
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict:
|
|
90
|
+
return {
|
|
91
|
+
"severity": self.severity,
|
|
92
|
+
"file": self.file,
|
|
93
|
+
"line": self.line,
|
|
94
|
+
"pattern": self.pattern,
|
|
95
|
+
"description": self.description,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def scan_file_patterns(filepath: Path, patterns: list[tuple],
|
|
100
|
+
severity: str, findings: list[Finding]) -> None:
|
|
101
|
+
"""Scan a single file against a list of regex patterns."""
|
|
102
|
+
try:
|
|
103
|
+
text = filepath.read_text(encoding="utf-8", errors="replace")
|
|
104
|
+
except OSError:
|
|
105
|
+
return
|
|
106
|
+
rel = str(filepath)
|
|
107
|
+
for lineno, line in enumerate(text.splitlines(), 1):
|
|
108
|
+
for regex, desc in patterns:
|
|
109
|
+
if re.search(regex, line, re.IGNORECASE):
|
|
110
|
+
findings.append(Finding(severity, rel, lineno, regex, desc))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def scan_secrets(filepath: Path, findings: list[Finding]) -> None:
|
|
114
|
+
"""Scan a file for hardcoded secrets."""
|
|
115
|
+
try:
|
|
116
|
+
text = filepath.read_text(encoding="utf-8", errors="replace")
|
|
117
|
+
except OSError:
|
|
118
|
+
return
|
|
119
|
+
rel = str(filepath)
|
|
120
|
+
for lineno, line in enumerate(text.splitlines(), 1):
|
|
121
|
+
for regex, severity, desc in SECRET_PATTERNS:
|
|
122
|
+
if re.search(regex, line):
|
|
123
|
+
findings.append(Finding(severity, rel, lineno, regex, desc))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def check_frontmatter(skill_dir: Path, findings: list[Finding]) -> None:
|
|
127
|
+
"""Check SKILL.md frontmatter for permission issues."""
|
|
128
|
+
skill_md = skill_dir / "SKILL.md"
|
|
129
|
+
if not skill_md.is_file():
|
|
130
|
+
return
|
|
131
|
+
rel = str(skill_md)
|
|
132
|
+
|
|
133
|
+
allowed = frontmatter_field(skill_md, "allowed-tools")
|
|
134
|
+
user_invocable = frontmatter_field(skill_md, "user-invocable")
|
|
135
|
+
disable_model = frontmatter_field(skill_md, "disable-model-invocation")
|
|
136
|
+
|
|
137
|
+
# Knowledge skill with Bash = suspicious
|
|
138
|
+
if user_invocable == "false" and "Bash" in allowed:
|
|
139
|
+
findings.append(Finding(
|
|
140
|
+
"HIGH", rel, 0,
|
|
141
|
+
"knowledge-skill-with-bash",
|
|
142
|
+
"Knowledge skill (user-invocable: false) has Bash access — "
|
|
143
|
+
"auto-loaded skills should not execute shell commands",
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
# No allowed-tools at all = unrestricted
|
|
147
|
+
if not allowed and not disable_model:
|
|
148
|
+
findings.append(Finding(
|
|
149
|
+
"WARN", rel, 0,
|
|
150
|
+
"missing-allowed-tools",
|
|
151
|
+
"No allowed-tools declared — skill has unrestricted tool access. "
|
|
152
|
+
"Add allowed-tools with principle of least privilege.",
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
# Overly permissive: Bash + Write + Edit without justification
|
|
156
|
+
if allowed:
|
|
157
|
+
tools = {t.strip() for t in allowed.split(",")}
|
|
158
|
+
if {"Bash", "Write", "Edit"}.issubset(tools) and len(tools) >= 5:
|
|
159
|
+
findings.append(Finding(
|
|
160
|
+
"INFO", rel, 0,
|
|
161
|
+
"broad-tool-access",
|
|
162
|
+
f"Skill has broad tool access ({len(tools)} tools including Bash+Write+Edit). "
|
|
163
|
+
"Verify this is necessary.",
|
|
164
|
+
))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def check_agent(agent_md: Path, findings: list[Finding]) -> None:
|
|
168
|
+
"""Check agent definition for issues."""
|
|
169
|
+
rel = str(agent_md)
|
|
170
|
+
tools = frontmatter_field(agent_md, "tools")
|
|
171
|
+
if tools == "*":
|
|
172
|
+
findings.append(Finding(
|
|
173
|
+
"INFO", rel, 0,
|
|
174
|
+
"agent-all-tools",
|
|
175
|
+
"Agent has access to all tools (*). Consider restricting.",
|
|
176
|
+
))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Main
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def audit(toolkit_root: Path) -> list[Finding]:
|
|
184
|
+
"""Run full audit and return findings."""
|
|
185
|
+
findings: list[Finding] = []
|
|
186
|
+
app = toolkit_root / "app"
|
|
187
|
+
skills = app / "skills"
|
|
188
|
+
agents = app / "agents"
|
|
189
|
+
|
|
190
|
+
# Scan skills
|
|
191
|
+
if skills.is_dir():
|
|
192
|
+
for skill_dir in sorted(skills.iterdir()):
|
|
193
|
+
if not skill_dir.is_dir():
|
|
194
|
+
continue
|
|
195
|
+
# Frontmatter checks
|
|
196
|
+
check_frontmatter(skill_dir, findings)
|
|
197
|
+
|
|
198
|
+
# Python scripts
|
|
199
|
+
for py in skill_dir.rglob("*.py"):
|
|
200
|
+
scan_file_patterns(py, PYTHON_HIGH, "HIGH", findings)
|
|
201
|
+
scan_file_patterns(py, PYTHON_WARN, "WARN", findings)
|
|
202
|
+
scan_secrets(py, findings)
|
|
203
|
+
|
|
204
|
+
# Bash scripts
|
|
205
|
+
for sh in skill_dir.rglob("*.sh"):
|
|
206
|
+
scan_file_patterns(sh, BASH_HIGH, "HIGH", findings)
|
|
207
|
+
scan_file_patterns(sh, BASH_WARN, "WARN", findings)
|
|
208
|
+
scan_secrets(sh, findings)
|
|
209
|
+
|
|
210
|
+
# Secrets in any text file
|
|
211
|
+
for md in skill_dir.rglob("*.md"):
|
|
212
|
+
scan_secrets(md, findings)
|
|
213
|
+
|
|
214
|
+
# Scan agents
|
|
215
|
+
if agents.is_dir():
|
|
216
|
+
for agent_md in sorted(agents.glob("*.md")):
|
|
217
|
+
check_agent(agent_md, findings)
|
|
218
|
+
scan_secrets(agent_md, findings)
|
|
219
|
+
|
|
220
|
+
# Sort: HIGH first, then WARN, then INFO
|
|
221
|
+
order = {"HIGH": 0, "WARN": 1, "INFO": 2}
|
|
222
|
+
findings.sort(key=lambda f: (order.get(f.severity, 9), f.file, f.line))
|
|
223
|
+
return findings
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def print_text(findings: list[Finding]) -> None:
|
|
227
|
+
"""Print human-readable report."""
|
|
228
|
+
high = sum(1 for f in findings if f.severity == "HIGH")
|
|
229
|
+
warn = sum(1 for f in findings if f.severity == "WARN")
|
|
230
|
+
info = sum(1 for f in findings if f.severity == "INFO")
|
|
231
|
+
|
|
232
|
+
print("Skill Security Audit")
|
|
233
|
+
print("=" * 40)
|
|
234
|
+
print(f"HIGH: {high} | WARN: {warn} | INFO: {info}")
|
|
235
|
+
print()
|
|
236
|
+
|
|
237
|
+
if not findings:
|
|
238
|
+
print("No findings. All clear.")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
for f in findings:
|
|
242
|
+
loc = f"{f.file}:{f.line}" if f.line else f.file
|
|
243
|
+
print(f"[{f.severity}] {loc}")
|
|
244
|
+
print(f" {f.description}")
|
|
245
|
+
print()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def print_json(findings: list[Finding]) -> None:
|
|
249
|
+
"""Print JSON report."""
|
|
250
|
+
high = sum(1 for f in findings if f.severity == "HIGH")
|
|
251
|
+
report = {
|
|
252
|
+
"summary": {
|
|
253
|
+
"high": high,
|
|
254
|
+
"warn": sum(1 for f in findings if f.severity == "WARN"),
|
|
255
|
+
"info": sum(1 for f in findings if f.severity == "INFO"),
|
|
256
|
+
"total": len(findings),
|
|
257
|
+
},
|
|
258
|
+
"findings": [f.to_dict() for f in findings],
|
|
259
|
+
}
|
|
260
|
+
print(json.dumps(report, indent=2))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main() -> None:
|
|
264
|
+
args = sys.argv[1:]
|
|
265
|
+
toolkit_root = default_toolkit_dir
|
|
266
|
+
json_mode = False
|
|
267
|
+
ci_mode = False
|
|
268
|
+
|
|
269
|
+
for arg in args:
|
|
270
|
+
if arg == "--json":
|
|
271
|
+
json_mode = True
|
|
272
|
+
elif arg == "--ci":
|
|
273
|
+
ci_mode = True
|
|
274
|
+
elif not arg.startswith("-"):
|
|
275
|
+
toolkit_root = Path(arg)
|
|
276
|
+
|
|
277
|
+
findings = audit(toolkit_root)
|
|
278
|
+
|
|
279
|
+
if json_mode:
|
|
280
|
+
print_json(findings)
|
|
281
|
+
else:
|
|
282
|
+
print_text(findings)
|
|
283
|
+
|
|
284
|
+
high_count = sum(1 for f in findings if f.severity == "HIGH")
|
|
285
|
+
if ci_mode and high_count > 0:
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate Augment AI rules from app/agents/*.md and app/skills/*/SKILL.md.
|
|
3
|
+
|
|
4
|
+
Augment uses ~/.augment/rules/*.md (user-level, always_apply) or
|
|
5
|
+
<project>/.augment/rules/*.md (workspace-level) with optional frontmatter.
|
|
6
|
+
|
|
7
|
+
Global install target: ~/.augment/rules/ai-toolkit.md
|
|
8
|
+
Local install target: .augment/rules/ai-toolkit.md
|
|
9
|
+
|
|
10
|
+
Usage: python3 scripts/generate_augment.py > .augment/rules/ai-toolkit.md
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
18
|
+
from generator_base import render_generator
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
render_generator({
|
|
22
|
+
"title": "# AI Toolkit — Augment Rules",
|
|
23
|
+
"header_lines": [
|
|
24
|
+
"---",
|
|
25
|
+
"type: always_apply",
|
|
26
|
+
"description: AI development toolkit with specialized agents and skills",
|
|
27
|
+
"---",
|
|
28
|
+
"",
|
|
29
|
+
"# Auto-generated by ai-toolkit — do not edit manually.",
|
|
30
|
+
"# Regenerate: ai-toolkit update",
|
|
31
|
+
],
|
|
32
|
+
"intro_template": (
|
|
33
|
+
"This repository uses the ai-toolkit — a shared AI development toolkit"
|
|
34
|
+
" with {agents} specialized agent personas and {skills} skills."
|
|
35
|
+
),
|
|
36
|
+
"agents_section": "## Available Agent Personas",
|
|
37
|
+
"agents_intro": "Apply the expertise of these agents when working on relevant tasks:",
|
|
38
|
+
"agents_format": "bullets",
|
|
39
|
+
"agents_level": "##",
|
|
40
|
+
"skills_section": "## Available Skills",
|
|
41
|
+
"skills_intro": "The following skills are available:",
|
|
42
|
+
"skills_format": "bullets",
|
|
43
|
+
"skills_level": "##",
|
|
44
|
+
"trailing_newline_after_skills": True,
|
|
45
|
+
"guidelines": ["general"],
|
|
46
|
+
})
|
package/scripts/install.py
CHANGED
|
@@ -29,9 +29,10 @@ Options:
|
|
|
29
29
|
--list, --dry-run Dry-run: show what would be installed
|
|
30
30
|
--reset Wipe and recreate local configs
|
|
31
31
|
--profile <p> minimal|standard|strict
|
|
32
|
+
--persona <p> backend-lead|frontend-lead|devops-eng|junior-dev
|
|
32
33
|
|
|
33
34
|
Components: agents, skills, hooks, constitution, architecture, rules,
|
|
34
|
-
cursor, windsurf, gemini
|
|
35
|
+
cursor, windsurf, gemini, augment
|
|
35
36
|
"""
|
|
36
37
|
from __future__ import annotations
|
|
37
38
|
|
|
@@ -39,7 +40,7 @@ import sys
|
|
|
39
40
|
from pathlib import Path
|
|
40
41
|
|
|
41
42
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
42
|
-
from _common import toolkit_dir
|
|
43
|
+
from _common import toolkit_dir, app_dir, inject_rule
|
|
43
44
|
from emission import agent_count as count_agents, skill_count as count_skills
|
|
44
45
|
|
|
45
46
|
# Step modules
|
|
@@ -63,6 +64,7 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
63
64
|
"local": False,
|
|
64
65
|
"reset": False,
|
|
65
66
|
"profile": "",
|
|
67
|
+
"persona": "",
|
|
66
68
|
}
|
|
67
69
|
i = 0
|
|
68
70
|
while i < len(argv):
|
|
@@ -88,6 +90,11 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
88
90
|
elif arg == "--profile":
|
|
89
91
|
i += 1
|
|
90
92
|
cfg["profile"] = argv[i] if i < len(argv) else ""
|
|
93
|
+
elif arg.startswith("--persona="):
|
|
94
|
+
cfg["persona"] = arg.split("=", 1)[1]
|
|
95
|
+
elif arg == "--persona":
|
|
96
|
+
i += 1
|
|
97
|
+
cfg["persona"] = argv[i] if i < len(argv) else ""
|
|
91
98
|
elif arg.startswith("-"):
|
|
92
99
|
print(f"Unknown option: {arg}")
|
|
93
100
|
sys.exit(1)
|
|
@@ -188,6 +195,27 @@ def install_claude_code(target_dir: Path, hooks_scripts_dir: Path,
|
|
|
188
195
|
inject_rules(claude_dir, target_dir, rules_dir, only, skip, dry_run)
|
|
189
196
|
|
|
190
197
|
|
|
198
|
+
VALID_PERSONAS = ("backend-lead", "frontend-lead", "devops-eng", "junior-dev")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def install_persona(target_dir: Path, persona: str, dry_run: bool) -> None:
|
|
202
|
+
"""Inject a persona rule into CLAUDE.md."""
|
|
203
|
+
if not persona:
|
|
204
|
+
return
|
|
205
|
+
if persona not in VALID_PERSONAS:
|
|
206
|
+
print(f"Unknown persona: {persona} (valid: {', '.join(VALID_PERSONAS)})")
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
persona_file = app_dir / "personas" / f"{persona}.md"
|
|
209
|
+
if not persona_file.is_file():
|
|
210
|
+
print(f" Persona file not found: {persona_file}")
|
|
211
|
+
return
|
|
212
|
+
if dry_run:
|
|
213
|
+
print(f" Would inject persona: {persona}")
|
|
214
|
+
return
|
|
215
|
+
inject_rule(persona_file, target_dir)
|
|
216
|
+
print(f" Persona applied: {persona}")
|
|
217
|
+
|
|
218
|
+
|
|
191
219
|
def install_strict_git_hooks(profile: str, local: bool, dry_run: bool) -> None:
|
|
192
220
|
if profile == "strict" and not local and not dry_run:
|
|
193
221
|
cwd = Path.cwd()
|
|
@@ -210,6 +238,7 @@ def main() -> None:
|
|
|
210
238
|
local: bool = cfg["local"]
|
|
211
239
|
reset: bool = cfg["reset"]
|
|
212
240
|
profile: str = cfg["profile"]
|
|
241
|
+
persona: str = cfg["persona"]
|
|
213
242
|
|
|
214
243
|
rules_dir = Path.home() / ".ai-toolkit" / "rules"
|
|
215
244
|
hooks_scripts_dir = Path.home() / ".ai-toolkit" / "hooks"
|
|
@@ -228,6 +257,7 @@ def main() -> None:
|
|
|
228
257
|
if local:
|
|
229
258
|
install_local_project(rules_dir, dry_run, reset)
|
|
230
259
|
|
|
260
|
+
install_persona(target_dir, persona, dry_run)
|
|
231
261
|
install_strict_git_hooks(profile, local, dry_run)
|
|
232
262
|
print_summary()
|
|
233
263
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Install AI tool configs (Cursor, Windsurf, Gemini) and local project setup."""
|
|
1
|
+
"""Install AI tool configs (Cursor, Windsurf, Gemini, Augment) and local project setup."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import shutil
|
|
@@ -47,6 +47,15 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
47
47
|
else:
|
|
48
48
|
print(" Skipped: gemini")
|
|
49
49
|
|
|
50
|
+
if should_install("augment", only, skip):
|
|
51
|
+
augment_file = target_dir / ".augment" / "rules" / "ai-toolkit.md"
|
|
52
|
+
if dry_run:
|
|
53
|
+
print(" Would inject: ~/.augment/rules/ai-toolkit.md")
|
|
54
|
+
else:
|
|
55
|
+
inject_with_rules("generate-augment.sh", augment_file, rules_dir)
|
|
56
|
+
else:
|
|
57
|
+
print(" Skipped: augment")
|
|
58
|
+
|
|
50
59
|
print()
|
|
51
60
|
print(" Note: Copilot, Cline, Roo Code, and Aider have no global config -- use 'ai-toolkit install --local' per project")
|
|
52
61
|
|