@softspark/ai-toolkit 3.0.2 → 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 +23 -0
- package/README.md +30 -23
- 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/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/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/global-install-model.md +29 -6
- 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/llms-full.txt +133 -58
- package/manifest.json +3 -3
- package/package.json +10 -3
- 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
|
@@ -155,15 +155,31 @@ def sync_codex_skill(skill_dir: Path, skills_dst: Path) -> str:
|
|
|
155
155
|
|
|
156
156
|
def cleanup_codex_skills(skills_dst: Path, skills_src: Path) -> None:
|
|
157
157
|
"""Remove broken symlinks and stale generated Codex skill wrappers."""
|
|
158
|
+
skills_src_resolved = skills_src.resolve()
|
|
158
159
|
for item in skills_dst.iterdir():
|
|
159
160
|
src = skills_src / item.name
|
|
160
|
-
if item.is_symlink()
|
|
161
|
-
item.
|
|
162
|
-
|
|
161
|
+
if item.is_symlink():
|
|
162
|
+
if not item.exists():
|
|
163
|
+
item.unlink()
|
|
164
|
+
continue
|
|
165
|
+
target = item.resolve()
|
|
166
|
+
if src.is_dir() and target == src.resolve():
|
|
167
|
+
continue
|
|
168
|
+
if _is_relative_to(target, skills_src_resolved):
|
|
169
|
+
item.unlink()
|
|
170
|
+
continue
|
|
163
171
|
if item.is_dir() and (item / ADAPTED_MARKER).is_file() and not src.is_dir():
|
|
164
172
|
shutil.rmtree(item)
|
|
165
173
|
|
|
166
174
|
|
|
175
|
+
def _is_relative_to(path: Path, parent: Path) -> bool:
|
|
176
|
+
try:
|
|
177
|
+
path.relative_to(parent)
|
|
178
|
+
return True
|
|
179
|
+
except ValueError:
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
167
183
|
def _parse_frontmatter(frontmatter_text: str) -> list[tuple[str, str]]:
|
|
168
184
|
entries: list[tuple[str, str]] = []
|
|
169
185
|
for line in frontmatter_text.splitlines():
|
|
@@ -81,8 +81,7 @@
|
|
|
81
81
|
"~/.cursor/mcp.json",
|
|
82
82
|
".cursor/skills/*/SKILL.md",
|
|
83
83
|
".cursor/agents/*.md",
|
|
84
|
-
".cursor/hooks.json"
|
|
85
|
-
"~/.cursor/hooks.json"
|
|
84
|
+
".cursor/hooks.json"
|
|
86
85
|
],
|
|
87
86
|
"our_generators": [
|
|
88
87
|
"scripts/generate_cursor_rules.py",
|
|
@@ -264,8 +263,8 @@
|
|
|
264
263
|
".roo/rules-{slug}/*.md",
|
|
265
264
|
".roo/mcp.json",
|
|
266
265
|
"~/.roo/rules/",
|
|
267
|
-
"~/.roo/
|
|
268
|
-
"
|
|
266
|
+
"~/.roo/custom_modes.yaml",
|
|
267
|
+
"mcp_settings.json"
|
|
269
268
|
],
|
|
270
269
|
"our_generators": [
|
|
271
270
|
"scripts/generate_roo_modes.py",
|
|
@@ -413,14 +412,15 @@
|
|
|
413
412
|
"config_paths": [
|
|
414
413
|
"AGENTS.md",
|
|
415
414
|
".agents/rules/*.md",
|
|
415
|
+
".agents/skills/*/SKILL.md",
|
|
416
416
|
".codex/hooks.json",
|
|
417
|
-
".codex/skills/*/SKILL.md",
|
|
418
417
|
"~/.codex/config.toml"
|
|
419
418
|
],
|
|
420
419
|
"our_generators": [
|
|
421
420
|
"scripts/generate_codex.py",
|
|
422
421
|
"scripts/generate_codex_rules.py",
|
|
423
|
-
"scripts/generate_codex_hooks.py"
|
|
422
|
+
"scripts/generate_codex_hooks.py",
|
|
423
|
+
"scripts/generate_codex_skills.py"
|
|
424
424
|
],
|
|
425
425
|
"capability_markers": [
|
|
426
426
|
"AGENTS.md",
|
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
"hook handler: command",
|
|
438
438
|
"hook handler: prompt",
|
|
439
439
|
"hook handler: agent",
|
|
440
|
-
".
|
|
440
|
+
".agents/skills"
|
|
441
441
|
],
|
|
442
442
|
"version_probe": {
|
|
443
443
|
"kind": "command",
|
|
@@ -79,13 +79,20 @@ def generate(target_dir: Path, *,
|
|
|
79
79
|
rules_dir: Path | None = None,
|
|
80
80
|
cleanup: bool = True,
|
|
81
81
|
emit_workflows: bool = True,
|
|
82
|
-
managed_scopes: tuple[str, ...] = (STANDARD_SCOPE,)
|
|
83
|
-
|
|
82
|
+
managed_scopes: tuple[str, ...] = (STANDARD_SCOPE,),
|
|
83
|
+
output_root: Path | None = None) -> None:
|
|
84
|
+
"""Write Cline rule files.
|
|
85
|
+
|
|
86
|
+
By default writes project-local ``target_dir/.clinerules/*.md``. When
|
|
87
|
+
``output_root`` is provided, writes directly into that directory; this is
|
|
88
|
+
used for Cline's documented global rules directory.
|
|
89
|
+
"""
|
|
84
90
|
# Migrate: if .clinerules exists as a single file, remove it so the
|
|
85
91
|
# directory can be created (Cline 3.7+ uses directory format).
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
clinerules.
|
|
92
|
+
if output_root is None:
|
|
93
|
+
clinerules = target_dir / ".clinerules"
|
|
94
|
+
if clinerules.is_file():
|
|
95
|
+
clinerules.unlink()
|
|
89
96
|
|
|
90
97
|
rules: dict[str, callable] = dict(STANDARD_RULES)
|
|
91
98
|
# Replace the testing rule with a conditional variant so it only
|
|
@@ -107,15 +114,17 @@ def generate(target_dir: Path, *,
|
|
|
107
114
|
|
|
108
115
|
rules.update(build_registered_rules(rules_dir))
|
|
109
116
|
|
|
117
|
+
root = output_root.parent if output_root is not None else target_dir
|
|
118
|
+
subdir = output_root.name if output_root is not None else ".clinerules"
|
|
110
119
|
write_rules(
|
|
111
|
-
|
|
120
|
+
root,
|
|
112
121
|
rules,
|
|
113
|
-
|
|
122
|
+
subdir,
|
|
114
123
|
cleanup=cleanup,
|
|
115
124
|
managed_scopes=managed_scopes,
|
|
116
125
|
)
|
|
117
126
|
|
|
118
|
-
if emit_workflows:
|
|
127
|
+
if emit_workflows and output_root is None:
|
|
119
128
|
_write_workflows(target_dir, cleanup=cleanup)
|
|
120
129
|
|
|
121
130
|
|
|
@@ -1,26 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Mirror the
|
|
2
|
+
"""Mirror the ai-toolkit skill catalogue into Codex ``.agents/skills/``.
|
|
3
3
|
|
|
4
|
-
OpenAI Codex CLI
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
skill in ``app/skills/`` into ``<target-dir>/.codex/skills/<name>/``.
|
|
4
|
+
OpenAI Codex CLI discovers Agent Skills from ``.agents/skills/`` in the
|
|
5
|
+
repository tree, plus user/admin/system skill locations. Unlike the Augment
|
|
6
|
+
and Gemini pointer pattern, Codex benefits from having the full skill catalog
|
|
7
|
+
on disk, so this generator syncs every skill in ``app/skills/`` into
|
|
8
|
+
``<target-dir>/.agents/skills/<name>/``.
|
|
10
9
|
|
|
11
10
|
The mirror is **opt-in**: ``enable_codex_skills=False`` is the default.
|
|
12
11
|
Bucket 4 wires a ``--codex-skills`` CLI flag that toggles this on.
|
|
13
12
|
|
|
14
13
|
Implementation:
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
without developer mode, hostile filesystems, etc.).
|
|
14
|
+
* Native Codex-compatible skills are symlinked to canonical ``app/skills``.
|
|
15
|
+
* Skills that use Claude-only delegation tools are rendered as Codex
|
|
16
|
+
wrappers through ``codex_skill_adapter.sync_codex_skill``.
|
|
19
17
|
* Skip ``_lib`` and any dotfile directories under ``app/skills/``.
|
|
20
|
-
* Remove stale
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
match a source skill name and are not our managed targets.
|
|
18
|
+
* Remove stale generated wrappers and broken managed symlinks.
|
|
19
|
+
* Preserve user-authored entries in ``.agents/skills/`` that are not managed
|
|
20
|
+
by ai-toolkit.
|
|
24
21
|
|
|
25
22
|
Idempotent on rerun.
|
|
26
23
|
|
|
@@ -32,12 +29,11 @@ keeping the opt-in default enforced even when invoked directly.
|
|
|
32
29
|
"""
|
|
33
30
|
from __future__ import annotations
|
|
34
31
|
|
|
35
|
-
import os
|
|
36
|
-
import shutil
|
|
37
32
|
import sys
|
|
38
33
|
from pathlib import Path
|
|
39
34
|
|
|
40
35
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
36
|
+
from codex_skill_adapter import cleanup_codex_skills, sync_codex_skill
|
|
41
37
|
from emission import skills_dir
|
|
42
38
|
|
|
43
39
|
|
|
@@ -69,69 +65,11 @@ def _iter_source_skills() -> list[Path]:
|
|
|
69
65
|
# Mirror operations
|
|
70
66
|
# ---------------------------------------------------------------------------
|
|
71
67
|
|
|
72
|
-
def
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
Uses ``lstat`` so symlinks are unlinked without following them.
|
|
76
|
-
"""
|
|
77
|
-
if not target.exists() and not target.is_symlink():
|
|
78
|
-
return
|
|
79
|
-
if target.is_symlink() or target.is_file():
|
|
80
|
-
target.unlink()
|
|
81
|
-
return
|
|
82
|
-
shutil.rmtree(target)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _symlink_or_copy(source: Path, target: Path) -> str:
|
|
86
|
-
"""Create ``target`` as a symlink to ``source``; fall back to a copy.
|
|
87
|
-
|
|
88
|
-
Returns ``"symlink"`` or ``"copy"`` indicating which strategy was used.
|
|
89
|
-
"""
|
|
90
|
-
_remove_existing(target)
|
|
91
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
-
try:
|
|
93
|
-
os.symlink(source, target, target_is_directory=True)
|
|
94
|
-
return "symlink"
|
|
95
|
-
except (OSError, NotImplementedError):
|
|
96
|
-
shutil.copytree(source, target, symlinks=False)
|
|
97
|
-
return "copy"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _cleanup_stale(codex_skills_dir: Path, live_names: set[str]) -> list[str]:
|
|
101
|
-
"""Remove managed entries under ``.codex/skills/`` that no longer map
|
|
102
|
-
to a source skill. Returns the names removed.
|
|
103
|
-
|
|
104
|
-
A managed entry is one whose directory name matches a historical
|
|
105
|
-
source skill name pattern: it contains a ``SKILL.md`` either directly
|
|
106
|
-
(copy) or via a symlink back into ``app/skills/``. User-authored
|
|
107
|
-
entries that do not look managed are left alone.
|
|
108
|
-
"""
|
|
68
|
+
def _count_entries(codex_skills_dir: Path) -> int:
|
|
69
|
+
"""Count top-level skill entries after sync."""
|
|
109
70
|
if not codex_skills_dir.is_dir():
|
|
110
|
-
return
|
|
111
|
-
|
|
112
|
-
for entry in sorted(codex_skills_dir.iterdir()):
|
|
113
|
-
if not entry.is_dir() and not entry.is_symlink():
|
|
114
|
-
continue
|
|
115
|
-
if entry.name in live_names:
|
|
116
|
-
continue
|
|
117
|
-
if entry.is_symlink():
|
|
118
|
-
# Only remove symlinks that point inside our app/skills/ tree.
|
|
119
|
-
try:
|
|
120
|
-
resolved = entry.resolve()
|
|
121
|
-
except OSError:
|
|
122
|
-
continue
|
|
123
|
-
try:
|
|
124
|
-
resolved.relative_to(skills_dir.resolve())
|
|
125
|
-
except ValueError:
|
|
126
|
-
continue
|
|
127
|
-
entry.unlink()
|
|
128
|
-
removed.append(entry.name)
|
|
129
|
-
continue
|
|
130
|
-
# Copy mode: treat as managed only if a SKILL.md is present.
|
|
131
|
-
if (entry / "SKILL.md").is_file():
|
|
132
|
-
shutil.rmtree(entry)
|
|
133
|
-
removed.append(entry.name)
|
|
134
|
-
return removed
|
|
71
|
+
return 0
|
|
72
|
+
return sum(1 for entry in codex_skills_dir.iterdir() if entry.is_dir() or entry.is_symlink())
|
|
135
73
|
|
|
136
74
|
|
|
137
75
|
# ---------------------------------------------------------------------------
|
|
@@ -139,10 +77,10 @@ def _cleanup_stale(codex_skills_dir: Path, live_names: set[str]) -> list[str]:
|
|
|
139
77
|
# ---------------------------------------------------------------------------
|
|
140
78
|
|
|
141
79
|
def generate(target_dir: Path, enable_codex_skills: bool = False) -> None:
|
|
142
|
-
"""Mirror ``app/skills/`` into ``<target_dir>/.
|
|
80
|
+
"""Mirror ``app/skills/`` into ``<target_dir>/.agents/skills/``.
|
|
143
81
|
|
|
144
82
|
Args:
|
|
145
|
-
target_dir: Project root where ``.
|
|
83
|
+
target_dir: Project root where ``.agents/skills/`` is written.
|
|
146
84
|
enable_codex_skills: Opt-in flag. Defaults to ``False`` (no-op).
|
|
147
85
|
|
|
148
86
|
Contract for Bucket 4 wiring::
|
|
@@ -156,31 +94,30 @@ def generate(target_dir: Path, enable_codex_skills: bool = False) -> None:
|
|
|
156
94
|
if not enable_codex_skills:
|
|
157
95
|
return
|
|
158
96
|
|
|
159
|
-
codex_skills_dir = target_dir / ".
|
|
97
|
+
codex_skills_dir = target_dir / ".agents" / "skills"
|
|
160
98
|
codex_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
161
99
|
|
|
162
100
|
sources = _iter_source_skills()
|
|
163
|
-
live_names: set[str] = {s.name for s in sources}
|
|
164
101
|
|
|
165
|
-
|
|
166
|
-
|
|
102
|
+
linked = 0
|
|
103
|
+
adapted = 0
|
|
104
|
+
skipped = 0
|
|
167
105
|
for skill in sources:
|
|
168
|
-
|
|
169
|
-
mode
|
|
170
|
-
|
|
171
|
-
|
|
106
|
+
mode = sync_codex_skill(skill, codex_skills_dir)
|
|
107
|
+
if mode == "linked":
|
|
108
|
+
linked += 1
|
|
109
|
+
elif mode == "adapted":
|
|
110
|
+
adapted += 1
|
|
172
111
|
else:
|
|
173
|
-
|
|
112
|
+
skipped += 1
|
|
174
113
|
|
|
175
|
-
|
|
114
|
+
cleanup_codex_skills(codex_skills_dir, skills_dir)
|
|
176
115
|
|
|
177
|
-
total = symlink_count + copy_count
|
|
178
116
|
print(
|
|
179
|
-
f" Codex skill mirror: {
|
|
180
|
-
f"({
|
|
117
|
+
f" Codex skill mirror: {_count_entries(codex_skills_dir)} skills "
|
|
118
|
+
f"to .agents/skills/ ({linked} linked, {adapted} adapted, "
|
|
119
|
+
f"{skipped} skipped)"
|
|
181
120
|
)
|
|
182
|
-
if removed:
|
|
183
|
-
print(f" Codex skill mirror: removed {len(removed)} stale entries")
|
|
184
121
|
|
|
185
122
|
|
|
186
123
|
def main() -> None:
|
|
@@ -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>
|