@softspark/ai-toolkit 2.12.0 → 3.0.1
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 +50 -0
- package/README.md +25 -8
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/hook-creator/SKILL.md +84 -8
- package/app/skills/skill-creator/SKILL.md +8 -4
- package/benchmarks/ecosystem-doctor-snapshot.json +395 -0
- package/kb/history/completed/deep-coverage-v3-20260423.md +160 -0
- package/kb/history/completed/ecosystem-deep-sweep-20260423.md +273 -0
- package/kb/procedures/ecosystem-sync-sop.md +255 -0
- package/kb/procedures/maintenance-sop.md +13 -2
- package/kb/procedures/release-preparation-sop.md +94 -12
- package/kb/procedures/release-verification-sop.md +112 -8
- package/kb/reference/global-install-model.md +15 -2
- package/kb/reference/supported-tools-registry.md +229 -0
- package/llms-full.txt +1175 -24
- package/llms.txt +4 -0
- package/manifest.json +1 -1
- package/package.json +4 -1
- package/scripts/ecosystem_doctor.py +348 -0
- package/scripts/ecosystem_tools.json +500 -0
- package/scripts/generate_aider_conf.py +26 -1
- package/scripts/generate_antigravity.py +77 -8
- package/scripts/generate_augment_agents.py +161 -0
- package/scripts/generate_augment_commands.py +160 -0
- package/scripts/generate_augment_hooks.py +162 -0
- package/scripts/generate_augment_skills.py +98 -0
- package/scripts/generate_cline_rules.py +96 -9
- package/scripts/generate_codex_hooks.py +13 -2
- package/scripts/generate_codex_skills.py +195 -0
- package/scripts/generate_copilot.py +296 -18
- package/scripts/generate_cursor_agents.py +144 -0
- package/scripts/generate_cursor_hooks.py +155 -0
- package/scripts/generate_cursor_mdc.py +20 -8
- package/scripts/generate_gemini_commands.py +158 -0
- package/scripts/generate_gemini_hooks.py +159 -0
- package/scripts/generate_gemini_skills.py +98 -0
- package/scripts/generate_roo_modes.py +42 -1
- package/scripts/generate_windsurf_hooks.py +143 -0
- package/scripts/generate_windsurf_rules.py +162 -10
- package/scripts/install.py +11 -2
- package/scripts/install_steps/ai_tools.py +120 -5
- package/scripts/validate.py +20 -3
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate
|
|
2
|
+
"""Generate ``.clinerules/*.md`` files and companion workflows for Cline.
|
|
3
3
|
|
|
4
|
-
Cline reads rules from the
|
|
5
|
-
Each
|
|
6
|
-
|
|
4
|
+
Cline reads rules from the ``.clinerules/`` directory (since Cline 3.7).
|
|
5
|
+
Each ``.md`` file inside is automatically loaded. The legacy single-file
|
|
6
|
+
``.clinerules`` format is replaced by this directory.
|
|
7
|
+
|
|
8
|
+
This generator also produces:
|
|
9
|
+
* ``.clinerules/workflows/*.md`` — project-local workflow files that
|
|
10
|
+
users invoke with ``/name.md`` in Cline chat. Emitted by default and
|
|
11
|
+
mirrors the same workflow catalogue used by Antigravity and Codex.
|
|
12
|
+
* Conditional rules (YAML ``paths:`` frontmatter) for file-type-scoped
|
|
13
|
+
rules such as testing and language-specific guidance, so Cline only
|
|
14
|
+
loads them when the user is editing matching files. See Cline docs:
|
|
15
|
+
customization/cline-rules#conditional-rules.
|
|
7
16
|
|
|
8
17
|
Usage:
|
|
9
18
|
python3 scripts/generate_cline_rules.py [target-dir]
|
|
@@ -15,28 +24,89 @@ from pathlib import Path
|
|
|
15
24
|
|
|
16
25
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
26
|
from dir_rules_shared import (
|
|
27
|
+
LANG_GLOBS,
|
|
28
|
+
LANG_PREFIX,
|
|
29
|
+
PREFIX,
|
|
18
30
|
STANDARD_RULES,
|
|
19
31
|
STANDARD_SCOPE,
|
|
32
|
+
STANDARD_WORKFLOWS,
|
|
20
33
|
build_language_rules,
|
|
21
34
|
build_registered_rules,
|
|
35
|
+
cleanup_stale,
|
|
36
|
+
rule_testing,
|
|
22
37
|
write_rules,
|
|
23
38
|
)
|
|
24
39
|
|
|
25
40
|
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Conditional-rule helper
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def _conditional(content: str, paths: list[str]) -> str:
|
|
46
|
+
"""Prepend a Cline ``paths:`` YAML frontmatter block.
|
|
47
|
+
|
|
48
|
+
Cline activates the rule only when the current working files match
|
|
49
|
+
one of the globs. See docs.cline.bot → customization/cline-rules.
|
|
50
|
+
"""
|
|
51
|
+
lines = ["---", "paths:"]
|
|
52
|
+
for p in paths:
|
|
53
|
+
lines.append(f' - "{p}"')
|
|
54
|
+
lines.append("---")
|
|
55
|
+
lines.append("")
|
|
56
|
+
lines.append(content.rstrip("\n"))
|
|
57
|
+
lines.append("")
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _conditional_testing_rule() -> str:
|
|
62
|
+
"""Scope the testing rule to test files only (reduces context use)."""
|
|
63
|
+
return _conditional(
|
|
64
|
+
rule_testing(),
|
|
65
|
+
["**/*.test.*", "**/*.spec.*", "**/test_*", "**/tests/**"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _wrap_language_rule(raw: str, lang: str) -> str:
|
|
70
|
+
"""Scope a language rule to its file extensions via conditional ``paths``."""
|
|
71
|
+
globs = LANG_GLOBS.get(lang)
|
|
72
|
+
if not globs:
|
|
73
|
+
return raw
|
|
74
|
+
return _conditional(raw, globs)
|
|
75
|
+
|
|
76
|
+
|
|
26
77
|
def generate(target_dir: Path, *,
|
|
27
78
|
language_modules: list[str] | None = None,
|
|
28
79
|
rules_dir: Path | None = None,
|
|
29
80
|
cleanup: bool = True,
|
|
81
|
+
emit_workflows: bool = True,
|
|
30
82
|
managed_scopes: tuple[str, ...] = (STANDARD_SCOPE,)) -> None:
|
|
31
|
-
"""Write
|
|
32
|
-
# Migrate: if .clinerules exists as a single file, remove it
|
|
33
|
-
#
|
|
83
|
+
"""Write ``.clinerules/*.md`` (and workflow) files to target_dir."""
|
|
84
|
+
# Migrate: if .clinerules exists as a single file, remove it so the
|
|
85
|
+
# directory can be created (Cline 3.7+ uses directory format).
|
|
34
86
|
clinerules = target_dir / ".clinerules"
|
|
35
87
|
if clinerules.is_file():
|
|
36
88
|
clinerules.unlink()
|
|
37
|
-
|
|
38
|
-
rules
|
|
89
|
+
|
|
90
|
+
rules: dict[str, callable] = dict(STANDARD_RULES)
|
|
91
|
+
# Replace the testing rule with a conditional variant so it only
|
|
92
|
+
# loads when the user is editing tests.
|
|
93
|
+
rules[f"{PREFIX}testing.md"] = _conditional_testing_rule
|
|
94
|
+
|
|
95
|
+
# Language rules — wrap each in conditional frontmatter scoped to
|
|
96
|
+
# the language's file globs so the language-specific guidance only
|
|
97
|
+
# loads for matching files.
|
|
98
|
+
for filename, content_fn in build_language_rules(language_modules).items():
|
|
99
|
+
lang = filename.removeprefix(LANG_PREFIX).removesuffix(".md")
|
|
100
|
+
if lang == "common":
|
|
101
|
+
# "common" spans all languages — apply unconditionally.
|
|
102
|
+
rules[filename] = content_fn
|
|
103
|
+
continue
|
|
104
|
+
rules[filename] = (lambda fn, l: lambda: _wrap_language_rule(fn(), l))(
|
|
105
|
+
content_fn, lang,
|
|
106
|
+
)
|
|
107
|
+
|
|
39
108
|
rules.update(build_registered_rules(rules_dir))
|
|
109
|
+
|
|
40
110
|
write_rules(
|
|
41
111
|
target_dir,
|
|
42
112
|
rules,
|
|
@@ -45,6 +115,23 @@ def generate(target_dir: Path, *,
|
|
|
45
115
|
managed_scopes=managed_scopes,
|
|
46
116
|
)
|
|
47
117
|
|
|
118
|
+
if emit_workflows:
|
|
119
|
+
_write_workflows(target_dir, cleanup=cleanup)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _write_workflows(target_dir: Path, *, cleanup: bool = True) -> None:
|
|
123
|
+
"""Write ``.clinerules/workflows/*.md`` files (Cline slash-invocable)."""
|
|
124
|
+
workflows_dir = target_dir / ".clinerules" / "workflows"
|
|
125
|
+
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
if cleanup:
|
|
128
|
+
# Only touch ai-toolkit-* files; never remove user workflows.
|
|
129
|
+
cleanup_stale(workflows_dir, set(STANDARD_WORKFLOWS.keys()))
|
|
130
|
+
|
|
131
|
+
for filename, content_fn in STANDARD_WORKFLOWS.items():
|
|
132
|
+
(workflows_dir / filename).write_text(content_fn(), encoding="utf-8")
|
|
133
|
+
print(f" Generated: .clinerules/workflows/{filename}")
|
|
134
|
+
|
|
48
135
|
|
|
49
136
|
def main() -> None:
|
|
50
137
|
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
Maps compatible ai-toolkit hooks to Codex lifecycle events.
|
|
5
5
|
Hook scripts are shared with Claude Code (stored in ~/.softspark/ai-toolkit/hooks/).
|
|
6
6
|
|
|
7
|
-
Codex supports
|
|
8
|
-
|
|
7
|
+
Codex supports 6 events (PascalCase in config.toml / hooks.json):
|
|
8
|
+
``PreToolUse``, ``PostToolUse``, ``PermissionRequest``, ``SessionStart``,
|
|
9
|
+
``UserPromptSubmit``, ``Stop``. PreToolUse/PostToolUse only support
|
|
10
|
+
the ``Bash`` matcher.
|
|
11
|
+
|
|
12
|
+
Handler types in Codex: ``command`` (what we emit), ``prompt``, and ``agent``.
|
|
13
|
+
Reference: codex-rs/config/src/hook_config.rs.
|
|
9
14
|
|
|
10
15
|
Usage:
|
|
11
16
|
python3 scripts/generate_codex_hooks.py [target-dir]
|
|
@@ -33,6 +38,12 @@ CODEX_HOOKS: dict[str, list[tuple[str, str]]] = {
|
|
|
33
38
|
("Bash", "guard-destructive.sh"),
|
|
34
39
|
("Bash", "commit-quality.sh"),
|
|
35
40
|
],
|
|
41
|
+
"PermissionRequest": [
|
|
42
|
+
# Fires when Codex asks the user to approve a tool call. Our guard
|
|
43
|
+
# reviews the tool input and can veto destructive patterns before the
|
|
44
|
+
# approval prompt reaches the human.
|
|
45
|
+
("", "guard-destructive.sh"),
|
|
46
|
+
],
|
|
36
47
|
"UserPromptSubmit": [
|
|
37
48
|
("", "user-prompt-submit.sh"),
|
|
38
49
|
("", "track-usage.sh"),
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Mirror the full ai-toolkit skill catalogue into ``.codex/skills/``.
|
|
3
|
+
|
|
4
|
+
OpenAI Codex CLI supports the Agent Skills standard at
|
|
5
|
+
``.codex/skills/<skill-name>/SKILL.md`` with optional supporting files
|
|
6
|
+
(scripts, references, assets) in the skill directory. Unlike the Augment
|
|
7
|
+
and Gemini pointer pattern, Codex benefits from having the full skill
|
|
8
|
+
content on disk, so this generator performs a **full mirror** of every
|
|
9
|
+
skill in ``app/skills/`` into ``<target-dir>/.codex/skills/<name>/``.
|
|
10
|
+
|
|
11
|
+
The mirror is **opt-in**: ``enable_codex_skills=False`` is the default.
|
|
12
|
+
Bucket 4 wires a ``--codex-skills`` CLI flag that toggles this on.
|
|
13
|
+
|
|
14
|
+
Implementation:
|
|
15
|
+
* Prefer symlinks from ``.codex/skills/<name>`` to the canonical
|
|
16
|
+
``app/skills/<name>`` directory (atomic and cheap).
|
|
17
|
+
* Fall back to a recursive copy when symlinks are unavailable (Windows
|
|
18
|
+
without developer mode, hostile filesystems, etc.).
|
|
19
|
+
* Skip ``_lib`` and any dotfile directories under ``app/skills/``.
|
|
20
|
+
* Remove stale entries under ``.codex/skills/`` that no longer
|
|
21
|
+
correspond to a source skill (cleanup on rerun).
|
|
22
|
+
* Never touch user-authored entries in ``.codex/skills/`` that do not
|
|
23
|
+
match a source skill name and are not our managed targets.
|
|
24
|
+
|
|
25
|
+
Idempotent on rerun.
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
python3 scripts/generate_codex_skills.py [target-dir]
|
|
29
|
+
|
|
30
|
+
By default the CLI entrypoint also requires ``--enable`` to write anything,
|
|
31
|
+
keeping the opt-in default enforced even when invoked directly.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import shutil
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
41
|
+
from emission import skills_dir
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Source skill discovery
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
def _iter_source_skills() -> list[Path]:
|
|
49
|
+
"""Return sorted list of source skill directories in ``app/skills/``.
|
|
50
|
+
|
|
51
|
+
Skills starting with ``_`` (library internals) or ``.`` are skipped.
|
|
52
|
+
Only directories containing a top-level ``SKILL.md`` are returned.
|
|
53
|
+
"""
|
|
54
|
+
if not skills_dir.is_dir():
|
|
55
|
+
return []
|
|
56
|
+
entries: list[Path] = []
|
|
57
|
+
for entry in sorted(skills_dir.iterdir()):
|
|
58
|
+
if not entry.is_dir():
|
|
59
|
+
continue
|
|
60
|
+
if entry.name.startswith("_") or entry.name.startswith("."):
|
|
61
|
+
continue
|
|
62
|
+
if not (entry / "SKILL.md").is_file():
|
|
63
|
+
continue
|
|
64
|
+
entries.append(entry)
|
|
65
|
+
return entries
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Mirror operations
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def _remove_existing(target: Path) -> None:
|
|
73
|
+
"""Remove an existing file, symlink, or directory at ``target``.
|
|
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
|
+
"""
|
|
109
|
+
if not codex_skills_dir.is_dir():
|
|
110
|
+
return []
|
|
111
|
+
removed: list[str] = []
|
|
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
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Main
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def generate(target_dir: Path, enable_codex_skills: bool = False) -> None:
|
|
142
|
+
"""Mirror ``app/skills/`` into ``<target_dir>/.codex/skills/``.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
target_dir: Project root where ``.codex/skills/`` is written.
|
|
146
|
+
enable_codex_skills: Opt-in flag. Defaults to ``False`` (no-op).
|
|
147
|
+
|
|
148
|
+
Contract for Bucket 4 wiring::
|
|
149
|
+
|
|
150
|
+
from scripts.generate_codex_skills import generate as gen_codex_skills
|
|
151
|
+
gen_codex_skills(target_dir, enable_codex_skills=cli_flag)
|
|
152
|
+
|
|
153
|
+
The ``--codex-skills`` CLI flag should propagate straight into
|
|
154
|
+
``enable_codex_skills``.
|
|
155
|
+
"""
|
|
156
|
+
if not enable_codex_skills:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
codex_skills_dir = target_dir / ".codex" / "skills"
|
|
160
|
+
codex_skills_dir.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
sources = _iter_source_skills()
|
|
163
|
+
live_names: set[str] = {s.name for s in sources}
|
|
164
|
+
|
|
165
|
+
symlink_count = 0
|
|
166
|
+
copy_count = 0
|
|
167
|
+
for skill in sources:
|
|
168
|
+
target = codex_skills_dir / skill.name
|
|
169
|
+
mode = _symlink_or_copy(skill, target)
|
|
170
|
+
if mode == "symlink":
|
|
171
|
+
symlink_count += 1
|
|
172
|
+
else:
|
|
173
|
+
copy_count += 1
|
|
174
|
+
|
|
175
|
+
removed = _cleanup_stale(codex_skills_dir, live_names)
|
|
176
|
+
|
|
177
|
+
total = symlink_count + copy_count
|
|
178
|
+
print(
|
|
179
|
+
f" Codex skill mirror: {total} skills "
|
|
180
|
+
f"({symlink_count} symlink, {copy_count} copy)"
|
|
181
|
+
)
|
|
182
|
+
if removed:
|
|
183
|
+
print(f" Codex skill mirror: removed {len(removed)} stale entries")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main() -> None:
|
|
187
|
+
args = sys.argv[1:]
|
|
188
|
+
enable = "--enable" in args
|
|
189
|
+
positional = [a for a in args if not a.startswith("--")]
|
|
190
|
+
target = Path(positional[0]) if positional else Path.cwd()
|
|
191
|
+
generate(target, enable_codex_skills=enable)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
main()
|