@softspark/ai-toolkit 3.0.1 → 3.1.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/AGENTS.md +13 -0
- package/CHANGELOG.md +46 -0
- package/README.md +34 -20
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/cpp-rules/SKILL.md +275 -0
- package/app/skills/csharp-rules/SKILL.md +282 -0
- package/app/skills/dart-rules/SKILL.md +299 -0
- package/app/skills/golang-rules/SKILL.md +262 -0
- package/app/skills/hook-creator/SKILL.md +7 -3
- package/app/skills/introspect/SKILL.md +1 -1
- package/app/skills/java-rules/SKILL.md +273 -0
- package/app/skills/kotlin-rules/SKILL.md +271 -0
- package/app/skills/medplum-rules/SKILL.md +271 -0
- package/app/skills/php-rules/SKILL.md +292 -0
- package/app/skills/python-rules/SKILL.md +257 -0
- package/app/skills/ruby-rules/SKILL.md +286 -0
- package/app/skills/rust-rules/SKILL.md +276 -0
- package/app/skills/swift-rules/SKILL.md +293 -0
- package/app/skills/typescript-rules/SKILL.md +249 -0
- package/benchmarks/ecosystem-doctor-snapshot.json +14 -14
- package/bin/ai-toolkit.js +5 -1
- package/kb/history/completed/deep-coverage-v3-20260423.md +3 -3
- package/kb/history/completed/ecosystem-deep-sweep-20260423.md +1 -1
- package/kb/procedures/release-preparation-sop.md +4 -4
- package/kb/procedures/release-verification-sop.md +11 -12
- package/kb/reference/architecture-overview.md +1 -1
- package/kb/reference/cli-reference.md +14 -3
- package/kb/reference/competitive-features-implementation.md +9 -9
- package/kb/reference/global-install-model.md +29 -6
- package/kb/reference/hooks-catalog.md +8 -2
- package/kb/reference/language-rules.md +54 -18
- package/kb/reference/mcp-editor-compatibility.md +4 -3
- package/kb/reference/mcp-templates.md +3 -2
- package/kb/reference/supported-tools-registry.md +10 -8
- package/kb/reference/windows-support.md +50 -0
- package/llms-full.txt +220 -72
- package/llms.txt +1 -0
- package/manifest.json +3 -3
- package/package.json +14 -3
- package/scripts/_common.py +21 -0
- package/scripts/check_deps.py +14 -0
- package/scripts/codex_skill_adapter.py +19 -3
- package/scripts/ecosystem_tools.json +7 -7
- package/scripts/generate_cline_rules.py +17 -8
- package/scripts/generate_codex_skills.py +33 -96
- package/scripts/generate_language_rules_skills.py +232 -0
- package/scripts/generate_roo_rules.py +11 -3
- package/scripts/install.py +6 -1
- package/scripts/install_steps/ai_tools.py +154 -51
- package/scripts/install_steps/install_state.py +14 -2
- package/scripts/mcp_editors.py +7 -0
- package/scripts/stats.py +126 -39
- package/scripts/validate.py +160 -4
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate language-rules knowledge skills from app/rules/<lang>/*.md.
|
|
3
|
+
|
|
4
|
+
Each language directory under ``app/rules/`` (except ``common/``) is compiled
|
|
5
|
+
into a single ``app/skills/<lang>-rules/SKILL.md`` knowledge skill. The skill
|
|
6
|
+
is ``user-invocable: false`` so Claude loads it contextually when the
|
|
7
|
+
description triggers match (file extensions, framework names).
|
|
8
|
+
|
|
9
|
+
This is the proper progressive-disclosure replacement for the v1.3.8 pointer
|
|
10
|
+
block in ``.claude/CLAUDE.md``: instead of nudging Claude to Read absolute
|
|
11
|
+
nvm-pinned paths on demand, the rules ride on the Agent Skills mechanism.
|
|
12
|
+
|
|
13
|
+
Common rules (``app/rules/common/``) stay inlined in ``CLAUDE.md`` because
|
|
14
|
+
they are language-agnostic and should be visible regardless of context.
|
|
15
|
+
|
|
16
|
+
Idempotent: rerunning overwrites generated SKILL.md but leaves any other
|
|
17
|
+
files in the skill directory alone.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python3 scripts/generate_language_rules_skills.py # write all
|
|
21
|
+
python3 scripts/generate_language_rules_skills.py --check # dry-run
|
|
22
|
+
python3 scripts/generate_language_rules_skills.py --langs python,rust
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from textwrap import dedent
|
|
30
|
+
|
|
31
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
32
|
+
RULES_DIR = ROOT / "app" / "rules"
|
|
33
|
+
SKILLS_DIR = ROOT / "app" / "skills"
|
|
34
|
+
|
|
35
|
+
# Per-language description triggers. Concrete file extensions and framework
|
|
36
|
+
# names give Claude a high-signal match against user prompts and file paths,
|
|
37
|
+
# so the skill activates reliably when the user is actually working in that
|
|
38
|
+
# language.
|
|
39
|
+
TRIGGERS: dict[str, dict[str, str]] = {
|
|
40
|
+
"python": {
|
|
41
|
+
"label": "Python",
|
|
42
|
+
"triggers": ".py, .pyi, pyproject.toml, requirements.txt, Pipfile, FastAPI, Django, Flask, pytest, SQLAlchemy, ruff, mypy",
|
|
43
|
+
},
|
|
44
|
+
"typescript": {
|
|
45
|
+
"label": "TypeScript/JavaScript",
|
|
46
|
+
"triggers": ".ts, .tsx, .js, .jsx, package.json, tsconfig.json, React, Next.js, Vue, Vite, Vitest, Jest, ESLint",
|
|
47
|
+
},
|
|
48
|
+
"golang": {
|
|
49
|
+
"label": "Go",
|
|
50
|
+
"triggers": ".go, go.mod, go.sum, Gin, Echo, Gorilla, testing, gofmt",
|
|
51
|
+
},
|
|
52
|
+
"rust": {
|
|
53
|
+
"label": "Rust",
|
|
54
|
+
"triggers": ".rs, Cargo.toml, Cargo.lock, Tokio, Axum, Serde, clippy, cargo test",
|
|
55
|
+
},
|
|
56
|
+
"java": {
|
|
57
|
+
"label": "Java",
|
|
58
|
+
"triggers": ".java, pom.xml, build.gradle, Spring, Spring Boot, JPA, Hibernate, JUnit, Maven, Gradle",
|
|
59
|
+
},
|
|
60
|
+
"kotlin": {
|
|
61
|
+
"label": "Kotlin",
|
|
62
|
+
"triggers": ".kt, .kts, build.gradle.kts, Ktor, Jetpack Compose, coroutines, kotlinx",
|
|
63
|
+
},
|
|
64
|
+
"swift": {
|
|
65
|
+
"label": "Swift",
|
|
66
|
+
"triggers": ".swift, Package.swift, .xcodeproj, SwiftUI, Combine, async/await, XCTest",
|
|
67
|
+
},
|
|
68
|
+
"dart": {
|
|
69
|
+
"label": "Dart/Flutter",
|
|
70
|
+
"triggers": ".dart, pubspec.yaml, Flutter, Riverpod, Bloc, widget, StatelessWidget, StatefulWidget",
|
|
71
|
+
},
|
|
72
|
+
"csharp": {
|
|
73
|
+
"label": "C#/.NET",
|
|
74
|
+
"triggers": ".cs, .csproj, .sln, ASP.NET, ASP.NET Core, EF Core, LINQ, NUnit, xUnit, dotnet",
|
|
75
|
+
},
|
|
76
|
+
"php": {
|
|
77
|
+
"label": "PHP",
|
|
78
|
+
"triggers": ".php, composer.json, Laravel, Symfony, PHPUnit, PSR-12, Composer",
|
|
79
|
+
},
|
|
80
|
+
"cpp": {
|
|
81
|
+
"label": "C++",
|
|
82
|
+
"triggers": ".cpp, .cc, .cxx, .hpp, .h, CMakeLists.txt, Makefile, GoogleTest, clang-tidy",
|
|
83
|
+
},
|
|
84
|
+
"ruby": {
|
|
85
|
+
"label": "Ruby",
|
|
86
|
+
"triggers": ".rb, Gemfile, .gemspec, Rails, ActiveRecord, Sidekiq, RSpec, Sorbet, rubocop",
|
|
87
|
+
},
|
|
88
|
+
"medplum": {
|
|
89
|
+
"label": "Medplum (FHIR healthcare)",
|
|
90
|
+
"triggers": "medplum.config.mts, medplum.config.ts, FHIR, Medplum, Bot, Subscription, Questionnaire",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _strip_frontmatter(text: str) -> str:
|
|
96
|
+
"""Remove YAML frontmatter (--- ... ---) if present."""
|
|
97
|
+
if not text.startswith("---"):
|
|
98
|
+
return text.lstrip("\n")
|
|
99
|
+
end = text.find("\n---", 3)
|
|
100
|
+
if end == -1:
|
|
101
|
+
return text.lstrip("\n")
|
|
102
|
+
return text[end + 4:].lstrip("\n")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _category_title(stem: str) -> str:
|
|
106
|
+
"""Convert filename stem (e.g. ``coding-style``) to a section title."""
|
|
107
|
+
return " ".join(part.capitalize() for part in stem.split("-"))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _build_skill_body(lang_dir: Path) -> str:
|
|
111
|
+
"""Concatenate all rule category files into a skill body."""
|
|
112
|
+
parts: list[str] = []
|
|
113
|
+
for f in sorted(lang_dir.glob("*.md")):
|
|
114
|
+
body = _strip_frontmatter(f.read_text(encoding="utf-8")).rstrip()
|
|
115
|
+
# If the source file already starts with a top-level "# Title", keep
|
|
116
|
+
# it. Otherwise, prepend a "## Category" header so the skill body
|
|
117
|
+
# has structure.
|
|
118
|
+
if body.lstrip().startswith("#"):
|
|
119
|
+
parts.append(body)
|
|
120
|
+
else:
|
|
121
|
+
parts.append(f"## {_category_title(f.stem)}\n\n{body}")
|
|
122
|
+
return "\n\n".join(parts) + "\n"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_description(lang: str) -> str:
|
|
126
|
+
meta = TRIGGERS.get(lang)
|
|
127
|
+
if not meta:
|
|
128
|
+
return (
|
|
129
|
+
f"{lang.capitalize()} coding rules: coding-style, frameworks, "
|
|
130
|
+
f"patterns, security, testing. Load when writing or reviewing "
|
|
131
|
+
f"{lang.capitalize()} code."
|
|
132
|
+
)
|
|
133
|
+
label = meta["label"]
|
|
134
|
+
triggers = meta["triggers"]
|
|
135
|
+
return (
|
|
136
|
+
f"{label} coding rules from ai-toolkit: coding-style, frameworks, "
|
|
137
|
+
f"patterns, security, testing. "
|
|
138
|
+
f"Triggers: {triggers}. "
|
|
139
|
+
f"Load when writing, reviewing, or editing {label} code."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _build_skill_md(lang: str, lang_dir: Path) -> str:
|
|
144
|
+
description = _build_description(lang)
|
|
145
|
+
body = _build_skill_body(lang_dir)
|
|
146
|
+
label = TRIGGERS.get(lang, {}).get("label", lang.capitalize())
|
|
147
|
+
frontmatter = dedent(
|
|
148
|
+
f"""\
|
|
149
|
+
---
|
|
150
|
+
name: {lang}-rules
|
|
151
|
+
description: "{description}"
|
|
152
|
+
effort: medium
|
|
153
|
+
user-invocable: false
|
|
154
|
+
allowed-tools: Read
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
# {label} Rules
|
|
158
|
+
|
|
159
|
+
These rules come from `app/rules/{lang}/` in ai-toolkit. They cover
|
|
160
|
+
the project's standards for coding style, frameworks, patterns,
|
|
161
|
+
security, and testing in {label}. Apply them when writing or
|
|
162
|
+
reviewing {label} code.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
)
|
|
166
|
+
return frontmatter + body
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def discover_languages() -> list[str]:
|
|
170
|
+
"""List language directories under app/rules/ excluding ``common``."""
|
|
171
|
+
if not RULES_DIR.is_dir():
|
|
172
|
+
return []
|
|
173
|
+
out: list[str] = []
|
|
174
|
+
for d in sorted(RULES_DIR.iterdir()):
|
|
175
|
+
if not d.is_dir() or d.name == "common":
|
|
176
|
+
continue
|
|
177
|
+
if any(d.glob("*.md")):
|
|
178
|
+
out.append(d.name)
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def generate(langs: list[str] | None = None, check: bool = False) -> int:
|
|
183
|
+
"""Generate skills. Returns count of skills written (or that would be)."""
|
|
184
|
+
languages = langs if langs else discover_languages()
|
|
185
|
+
written = 0
|
|
186
|
+
for lang in languages:
|
|
187
|
+
lang_dir = RULES_DIR / lang
|
|
188
|
+
if not lang_dir.is_dir():
|
|
189
|
+
print(f" SKIP: {lang} (directory missing)", file=sys.stderr)
|
|
190
|
+
continue
|
|
191
|
+
skill_dir = SKILLS_DIR / f"{lang}-rules"
|
|
192
|
+
skill_md = skill_dir / "SKILL.md"
|
|
193
|
+
content = _build_skill_md(lang, lang_dir)
|
|
194
|
+
|
|
195
|
+
if check:
|
|
196
|
+
existing = skill_md.read_text(encoding="utf-8") if skill_md.is_file() else ""
|
|
197
|
+
status = "OK" if existing == content else "DIFF"
|
|
198
|
+
print(f" [{status}] {skill_md.relative_to(ROOT)}")
|
|
199
|
+
if existing != content:
|
|
200
|
+
written += 1
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
skill_md.write_text(content, encoding="utf-8")
|
|
205
|
+
print(f" Wrote: {skill_md.relative_to(ROOT)}")
|
|
206
|
+
written += 1
|
|
207
|
+
return written
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main() -> int:
|
|
211
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
212
|
+
ap.add_argument(
|
|
213
|
+
"--langs",
|
|
214
|
+
default="",
|
|
215
|
+
help="Comma-separated language list (default: all)",
|
|
216
|
+
)
|
|
217
|
+
ap.add_argument(
|
|
218
|
+
"--check",
|
|
219
|
+
action="store_true",
|
|
220
|
+
help="Dry-run: report which skills would change without writing",
|
|
221
|
+
)
|
|
222
|
+
args = ap.parse_args()
|
|
223
|
+
langs = [s.strip() for s in args.langs.split(",") if s.strip()] or None
|
|
224
|
+
n = generate(langs=langs, check=args.check)
|
|
225
|
+
if args.check and n > 0:
|
|
226
|
+
print(f"\n{n} skill(s) out of date. Re-run without --check.", file=sys.stderr)
|
|
227
|
+
return 1
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
sys.exit(main())
|
|
@@ -23,12 +23,20 @@ from dir_rules_shared import (
|
|
|
23
23
|
|
|
24
24
|
def generate(target_dir: Path, *,
|
|
25
25
|
language_modules: list[str] | None = None,
|
|
26
|
-
rules_dir: Path | None = None
|
|
27
|
-
|
|
26
|
+
rules_dir: Path | None = None,
|
|
27
|
+
output_root: Path | None = None) -> None:
|
|
28
|
+
"""Write Roo Code rule files.
|
|
29
|
+
|
|
30
|
+
By default writes project-local ``target_dir/.roo/rules/*.md``. When
|
|
31
|
+
``output_root`` is provided, writes directly into that directory for
|
|
32
|
+
documented global rules such as ``~/.roo/rules``.
|
|
33
|
+
"""
|
|
28
34
|
rules = dict(STANDARD_RULES)
|
|
29
35
|
rules.update(build_language_rules(language_modules))
|
|
30
36
|
rules.update(build_registered_rules(rules_dir))
|
|
31
|
-
|
|
37
|
+
root = output_root.parent if output_root is not None else target_dir
|
|
38
|
+
subdir = output_root.name if output_root is not None else ".roo/rules"
|
|
39
|
+
write_rules(root, rules, subdir)
|
|
32
40
|
|
|
33
41
|
|
|
34
42
|
def main() -> None:
|
package/scripts/install.py
CHANGED
|
@@ -12,9 +12,14 @@ Claude Code (~/.claude/):
|
|
|
12
12
|
- Rules injected into ~/.claude/CLAUDE.md
|
|
13
13
|
|
|
14
14
|
Other tools (global config locations):
|
|
15
|
-
- Cursor: ~/.cursor/rules
|
|
16
15
|
- Windsurf: ~/.codeium/windsurf/memories/global_rules.md
|
|
17
16
|
- Gemini: ~/.gemini/GEMINI.md
|
|
17
|
+
- Cline: ~/Documents/Cline/Rules/
|
|
18
|
+
- Roo Code: ~/.roo/rules/
|
|
19
|
+
- Aider: ~/.aider.conf.yml (created only if absent)
|
|
20
|
+
- Augment: ~/.augment/rules/ai-toolkit.md
|
|
21
|
+
- Codex: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json
|
|
22
|
+
- opencode: ~/.config/opencode/
|
|
18
23
|
|
|
19
24
|
Registered rules (~/.softspark/ai-toolkit/rules/*.md) are also injected into
|
|
20
25
|
all of the above. Add rules with: ai-toolkit add-rule <rule.md>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Install
|
|
1
|
+
"""Install global and project-local AI tool configs."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import shutil
|
|
@@ -53,14 +53,6 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
53
53
|
# control Claude components like agents, hooks, rules). If an editor is
|
|
54
54
|
# in the requested set, install it unconditionally.
|
|
55
55
|
|
|
56
|
-
if "cursor" in eds:
|
|
57
|
-
cursor_file = target_dir / ".cursor" / "rules"
|
|
58
|
-
if dry_run:
|
|
59
|
-
print(" Would inject: ~/.cursor/rules")
|
|
60
|
-
else:
|
|
61
|
-
inject_with_rules("generate-cursor-rules.sh", cursor_file, rules_dir)
|
|
62
|
-
installed.append("cursor")
|
|
63
|
-
|
|
64
56
|
if "windsurf" in eds:
|
|
65
57
|
windsurf_file = target_dir / ".codeium" / "windsurf" / "memories" / "global_rules.md"
|
|
66
58
|
if dry_run:
|
|
@@ -85,6 +77,27 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
85
77
|
inject_with_rules("generate-augment.sh", augment_file, rules_dir)
|
|
86
78
|
installed.append("augment")
|
|
87
79
|
|
|
80
|
+
if "cline" in eds:
|
|
81
|
+
if dry_run:
|
|
82
|
+
print(" Would generate: ~/Documents/Cline/Rules/ai-toolkit-*.md")
|
|
83
|
+
else:
|
|
84
|
+
_install_cline_global(target_dir, rules_dir)
|
|
85
|
+
installed.append("cline")
|
|
86
|
+
|
|
87
|
+
if "roo" in eds:
|
|
88
|
+
if dry_run:
|
|
89
|
+
print(" Would generate: ~/.roo/rules/ai-toolkit-*.md")
|
|
90
|
+
else:
|
|
91
|
+
_install_roo_global(target_dir, rules_dir)
|
|
92
|
+
installed.append("roo")
|
|
93
|
+
|
|
94
|
+
if "aider" in eds:
|
|
95
|
+
if dry_run:
|
|
96
|
+
print(" Would create if missing: ~/.aider.conf.yml + ~/.aider-ai-toolkit-CONVENTIONS.md")
|
|
97
|
+
else:
|
|
98
|
+
_install_aider_global(target_dir, rules_dir)
|
|
99
|
+
installed.append("aider")
|
|
100
|
+
|
|
88
101
|
if "codex" in eds:
|
|
89
102
|
if dry_run:
|
|
90
103
|
print(" Would inject: ~/AGENTS.md, ~/.agents/, ~/.codex/hooks.json")
|
|
@@ -102,7 +115,7 @@ def install_ai_tools(target_dir: Path, rules_dir: Path,
|
|
|
102
115
|
|
|
103
116
|
print()
|
|
104
117
|
print(f" Available: {', '.join(GLOBAL_CAPABLE_EDITORS)}")
|
|
105
|
-
print(" Note:
|
|
118
|
+
print(" Note: Cursor, Copilot, and Antigravity rule installs are project-local; use 'ai-toolkit install --local' for those.")
|
|
106
119
|
|
|
107
120
|
return installed
|
|
108
121
|
|
|
@@ -136,6 +149,77 @@ def _install_codex_global(target_dir: Path, rules_dir: Path) -> None:
|
|
|
136
149
|
_install_codex_skills(target_dir)
|
|
137
150
|
|
|
138
151
|
|
|
152
|
+
def _install_cline_global(target_dir: Path, rules_dir: Path) -> None:
|
|
153
|
+
"""Install Cline global rules in the documented user rules directory."""
|
|
154
|
+
from generate_cline_rules import generate as gen_cline_rules
|
|
155
|
+
|
|
156
|
+
rules_root = target_dir / "Documents" / "Cline" / "Rules"
|
|
157
|
+
gen_cline_rules(
|
|
158
|
+
target_dir,
|
|
159
|
+
rules_dir=rules_dir,
|
|
160
|
+
output_root=rules_root,
|
|
161
|
+
emit_workflows=False,
|
|
162
|
+
managed_scopes=("standard", "custom"),
|
|
163
|
+
)
|
|
164
|
+
print(" Created: ~/Documents/Cline/Rules/ai-toolkit-*.md")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _install_roo_global(target_dir: Path, rules_dir: Path) -> None:
|
|
168
|
+
"""Install Roo Code global rules in ~/.roo/rules."""
|
|
169
|
+
from generate_roo_rules import generate as gen_roo_rules
|
|
170
|
+
|
|
171
|
+
gen_roo_rules(target_dir, rules_dir=rules_dir, output_root=target_dir / ".roo" / "rules")
|
|
172
|
+
print(" Created: ~/.roo/rules/ai-toolkit-*.md")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _install_aider_global(target_dir: Path, rules_dir: Path) -> None:
|
|
176
|
+
"""Install an Aider global config only when it can be created safely.
|
|
177
|
+
|
|
178
|
+
Aider supports ~/.aider.conf.yml, but YAML merging without a parser risks
|
|
179
|
+
clobbering user settings. For existing files we leave the user's config
|
|
180
|
+
untouched and print an explicit next step.
|
|
181
|
+
"""
|
|
182
|
+
conventions_file = target_dir / ".aider-ai-toolkit-CONVENTIONS.md"
|
|
183
|
+
inject_with_rules("generate_conventions.py", conventions_file, rules_dir)
|
|
184
|
+
|
|
185
|
+
config_file = target_dir / ".aider.conf.yml"
|
|
186
|
+
if config_file.exists():
|
|
187
|
+
print(" Kept: ~/.aider.conf.yml (already exists; not merging YAML automatically)")
|
|
188
|
+
print(f" Available: {conventions_file} for manual read: entry")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
default_model = _aider_default_model()
|
|
192
|
+
config_file.write_text(
|
|
193
|
+
"\n".join([
|
|
194
|
+
"# Aider configuration generated by ai-toolkit",
|
|
195
|
+
"# Aider docs: https://aider.chat/docs/config/aider_conf.html",
|
|
196
|
+
"",
|
|
197
|
+
"architect: true",
|
|
198
|
+
"auto-accept-architect: true",
|
|
199
|
+
"",
|
|
200
|
+
"read:",
|
|
201
|
+
f' - "{conventions_file}"',
|
|
202
|
+
"",
|
|
203
|
+
f"model: {default_model}",
|
|
204
|
+
f"editor-model: {default_model}",
|
|
205
|
+
"",
|
|
206
|
+
'commit-prompt: "Write a short, concise commit message following Conventional Commits (feat/fix/chore/docs/test/refactor)."',
|
|
207
|
+
"attribute-co-authored-by: false",
|
|
208
|
+
"attribute-commit-message-author: false",
|
|
209
|
+
"attribute-commit-message-committer: false",
|
|
210
|
+
"",
|
|
211
|
+
]),
|
|
212
|
+
encoding="utf-8",
|
|
213
|
+
)
|
|
214
|
+
print(" Created: ~/.aider.conf.yml")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _aider_default_model() -> str:
|
|
218
|
+
from _common import DEFAULT_CLAUDE_MODELS
|
|
219
|
+
|
|
220
|
+
return DEFAULT_CLAUDE_MODELS["sonnet"]
|
|
221
|
+
|
|
222
|
+
|
|
139
223
|
def _install_opencode_global(target_dir: Path, rules_dir: Path) -> None:
|
|
140
224
|
"""Install opencode at the global level (~/.config/opencode/).
|
|
141
225
|
|
|
@@ -331,10 +415,10 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
331
415
|
- ``full``: adds every native surface an editor can host (subagents,
|
|
332
416
|
custom commands, hooks, skill-catalogue pointers).
|
|
333
417
|
|
|
334
|
-
``codex_skills`` (opt-in, off by default)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
418
|
+
``codex_skills`` (opt-in, off by default) explicitly refreshes the full
|
|
419
|
+
Codex skill catalog under ``.agents/skills/``. ``--editors codex`` already
|
|
420
|
+
installs this catalog for normal local installs; the flag is kept as a
|
|
421
|
+
direct generator contract for scripts and dry-run verification.
|
|
338
422
|
|
|
339
423
|
If ``merged_config`` is provided (from .softspark-toolkit.json extends resolution),
|
|
340
424
|
additional rules and constitution amendments from the base config are injected.
|
|
@@ -491,53 +575,67 @@ def _apply_extends_config(cwd: Path, merged: dict) -> None:
|
|
|
491
575
|
|
|
492
576
|
|
|
493
577
|
def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
|
|
494
|
-
"""Inject
|
|
578
|
+
"""Inject ``app/rules/common/*.md`` content into project's ``.claude/CLAUDE.md``.
|
|
579
|
+
|
|
580
|
+
Per-language rules (``app/rules/<lang>/``) are NOT injected here -- they
|
|
581
|
+
ship as ``<lang>-rules`` knowledge skills under ``app/skills/`` and load
|
|
582
|
+
contextually via the Agent Skills progressive-disclosure mechanism. This
|
|
583
|
+
keeps ``CLAUDE.md`` small while ensuring language-specific guidance still
|
|
584
|
+
reaches Claude when relevant.
|
|
495
585
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
586
|
+
Common rules are language-agnostic (security, git workflow, testing,
|
|
587
|
+
coding-style, performance) and stay inlined so they remain in scope for
|
|
588
|
+
every prompt.
|
|
499
589
|
"""
|
|
500
590
|
if not language_modules:
|
|
501
591
|
return
|
|
502
592
|
|
|
503
593
|
rules_src = app_dir / "rules"
|
|
504
|
-
|
|
594
|
+
common_dir = rules_src / "common"
|
|
595
|
+
if not common_dir.is_dir():
|
|
505
596
|
return
|
|
506
597
|
|
|
507
|
-
# Detect language
|
|
598
|
+
# Detect requested per-language modules so we can name the linked skills
|
|
599
|
+
# in the marker block. The modules themselves are not inlined.
|
|
508
600
|
langs: list[str] = []
|
|
509
601
|
for mod in language_modules:
|
|
510
602
|
if mod.startswith("rules-"):
|
|
511
|
-
|
|
603
|
+
name = mod[6:]
|
|
604
|
+
if name != "common":
|
|
605
|
+
langs.append(name)
|
|
606
|
+
|
|
607
|
+
# Inline full content of every common rule file, stripping YAML
|
|
608
|
+
# frontmatter so the resulting block reads as plain Markdown.
|
|
609
|
+
inlined: list[str] = []
|
|
610
|
+
for f in sorted(common_dir.glob("*.md")):
|
|
611
|
+
body = f.read_text(encoding="utf-8")
|
|
612
|
+
if body.startswith("---"):
|
|
613
|
+
end = body.find("\n---", 3)
|
|
614
|
+
if end != -1:
|
|
615
|
+
body = body[end + 4:].lstrip("\n")
|
|
616
|
+
inlined.append(body.rstrip())
|
|
512
617
|
|
|
513
|
-
if not langs:
|
|
514
|
-
return
|
|
515
|
-
|
|
516
|
-
# Build a lightweight reference pointer — NOT the full rules content.
|
|
517
|
-
# Full rules are available as knowledge skills (auto-loaded by Claude)
|
|
518
|
-
# and as files Claude can Read on demand.
|
|
519
|
-
toolkit_pkg = app_dir.parent
|
|
520
618
|
lines: list[str] = ["# Language Rules", ""]
|
|
521
|
-
lines.append(
|
|
619
|
+
lines.append(
|
|
620
|
+
"Common (language-agnostic) rules apply to every change in this "
|
|
621
|
+
"project. Language-specific rules live in `<lang>-rules` knowledge "
|
|
622
|
+
"skills (e.g. `python-rules`, `typescript-rules`) and load "
|
|
623
|
+
"automatically when their triggers match -- you do not need to "
|
|
624
|
+
"Read them manually."
|
|
625
|
+
)
|
|
626
|
+
if langs:
|
|
627
|
+
skill_names = ", ".join(f"`{l}-rules`" for l in langs)
|
|
628
|
+
lines.append("")
|
|
629
|
+
lines.append(f"Detected languages: {skill_names}.")
|
|
522
630
|
lines.append("")
|
|
523
|
-
lines.append("
|
|
524
|
-
# Resolve actual installed path for the rules
|
|
525
|
-
rules_resolved = str(rules_src.resolve())
|
|
526
|
-
all_dirs: list[str] = ["common"]
|
|
527
|
-
for l in langs:
|
|
528
|
-
if l not in all_dirs:
|
|
529
|
-
all_dirs.append(l)
|
|
530
|
-
for lang in all_dirs:
|
|
531
|
-
lang_path = rules_src / lang
|
|
532
|
-
if lang_path.is_dir():
|
|
533
|
-
categories = ", ".join(f.stem for f in sorted(lang_path.glob("*.md")))
|
|
534
|
-
lines.append(f"- `{lang_path.resolve()}/` ({categories})")
|
|
631
|
+
lines.append("---")
|
|
535
632
|
lines.append("")
|
|
536
|
-
lines.
|
|
633
|
+
lines.extend(inlined)
|
|
537
634
|
|
|
538
|
-
# Write
|
|
635
|
+
# Write to temp file, then inject as a single named section so reruns are
|
|
636
|
+
# idempotent (existing block is replaced, not duplicated).
|
|
539
637
|
import tempfile
|
|
540
|
-
combined = "\n".join(lines)
|
|
638
|
+
combined = "\n\n".join(lines).rstrip() + "\n"
|
|
541
639
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False,
|
|
542
640
|
encoding="utf-8") as tmp:
|
|
543
641
|
tmp.write(combined)
|
|
@@ -545,8 +643,11 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
|
|
|
545
643
|
|
|
546
644
|
try:
|
|
547
645
|
inject_section(tmp_path, cwd / ".claude" / "CLAUDE.md", "language-rules")
|
|
548
|
-
|
|
549
|
-
|
|
646
|
+
if langs:
|
|
647
|
+
print(f" Injected: common rules + {len(langs)} language skill(s) "
|
|
648
|
+
f"({', '.join(langs)})")
|
|
649
|
+
else:
|
|
650
|
+
print(" Injected: common rules")
|
|
550
651
|
finally:
|
|
551
652
|
tmp_path.unlink(missing_ok=True)
|
|
552
653
|
|
|
@@ -600,8 +701,10 @@ def _install_local_dry_run(reset: bool, editors: list[str] | None = None,
|
|
|
600
701
|
"$HOME/.augment/settings.json + .augment/skills/ (profile=full)")
|
|
601
702
|
if "gemini" in eds:
|
|
602
703
|
print(" Would generate: .gemini/commands/ + .gemini/skills/ (profile=full)")
|
|
603
|
-
if "codex" in eds
|
|
604
|
-
print(" Would generate: .
|
|
704
|
+
if "codex" in eds:
|
|
705
|
+
print(" Would generate: .agents/skills/ Codex skills")
|
|
706
|
+
if codex_skills:
|
|
707
|
+
print(" Would refresh: .agents/skills/ via --codex-skills")
|
|
605
708
|
|
|
606
709
|
if not eds:
|
|
607
710
|
print(" No editors selected (use --editors <list> or --editors all)")
|
|
@@ -881,10 +984,10 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
881
984
|
from generate_codex_hooks import generate as gen_codex_hooks
|
|
882
985
|
gen_codex_hooks(cwd)
|
|
883
986
|
print(" Created: .codex/hooks.json")
|
|
884
|
-
# .agents/skills/ —
|
|
987
|
+
# .agents/skills/ — Codex discovery path for repo-local skills
|
|
885
988
|
_install_codex_skills(cwd)
|
|
886
|
-
#
|
|
887
|
-
#
|
|
989
|
+
# --codex-skills explicitly re-runs the same Codex skill sync path.
|
|
990
|
+
# Codex upstream discovers skills from .agents/skills/, not .codex/skills/.
|
|
888
991
|
if codex_skills:
|
|
889
992
|
_try_generator("generate_codex_skills", cwd,
|
|
890
993
|
enable_codex_skills=True)
|
|
@@ -88,8 +88,20 @@ def remove_mcp_template(name: str) -> None:
|
|
|
88
88
|
# Default global install: Claude only — no other editors unless --editors is used
|
|
89
89
|
DEFAULT_GLOBAL_EDITORS: list[str] = []
|
|
90
90
|
|
|
91
|
-
# All editors that support global install (opt-in via --editors)
|
|
92
|
-
|
|
91
|
+
# All editors that support global install (opt-in via --editors).
|
|
92
|
+
#
|
|
93
|
+
# Cursor intentionally stays out of this list: its documented global rules
|
|
94
|
+
# surface is the Settings UI, not a stable file path we can merge safely.
|
|
95
|
+
GLOBAL_CAPABLE_EDITORS = [
|
|
96
|
+
"aider",
|
|
97
|
+
"augment",
|
|
98
|
+
"cline",
|
|
99
|
+
"codex",
|
|
100
|
+
"gemini",
|
|
101
|
+
"opencode",
|
|
102
|
+
"roo",
|
|
103
|
+
"windsurf",
|
|
104
|
+
]
|
|
93
105
|
|
|
94
106
|
|
|
95
107
|
def get_global_editors() -> list[str]:
|
package/scripts/mcp_editors.py
CHANGED
|
@@ -41,6 +41,13 @@ EDITOR_SPECS: dict[str, dict[str, str | None]] = {
|
|
|
41
41
|
"format": "json",
|
|
42
42
|
"doc_scope": "project + global",
|
|
43
43
|
},
|
|
44
|
+
"roo": {
|
|
45
|
+
"label": "Roo Code",
|
|
46
|
+
"project_path": ".roo/mcp.json",
|
|
47
|
+
"global_path": None,
|
|
48
|
+
"format": "json",
|
|
49
|
+
"doc_scope": "project",
|
|
50
|
+
},
|
|
44
51
|
"windsurf": {
|
|
45
52
|
"label": "Windsurf",
|
|
46
53
|
"project_path": None,
|