@softspark/ai-toolkit 2.0.2 → 2.1.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 +49 -0
- package/README.md +143 -774
- package/app/ARCHITECTURE.md +1 -1
- package/app/plugins/README.md +6 -2
- package/app/skills/plugin-creator/SKILL.md +3 -4
- package/bin/ai-toolkit.js +34 -10
- package/kb/procedures/maintenance-sop.md +64 -16
- package/kb/procedures/release-preparation-sop.md +4 -2
- package/kb/procedures/release-verification-sop.md +15 -13
- package/kb/reference/architecture-overview.md +44 -5
- package/kb/reference/claude-ecosystem-expansion-foundations.md +4 -4
- package/kb/reference/cli-reference.md +135 -0
- package/kb/reference/codex-cli-compatibility.md +136 -0
- package/kb/reference/comparison.md +29 -0
- package/kb/reference/extension-api.md +23 -6
- package/kb/reference/global-install-model.md +62 -5
- package/kb/reference/mcp-editor-compatibility.md +62 -0
- package/kb/reference/mcp-templates.md +32 -6
- package/kb/reference/plugin-pack-conventions.md +22 -21
- package/kb/reference/skills-catalog.md +27 -5
- package/kb/reference/unique-features.md +213 -0
- package/llms-full.txt +903 -84
- package/llms.txt +5 -0
- package/package.json +6 -5
- package/scripts/codex_skill_adapter.py +295 -0
- package/scripts/dir_rules_shared.py +46 -7
- package/scripts/generate_agents_md.py +13 -0
- package/scripts/generate_antigravity.py +2 -1
- package/scripts/generate_augment_rules.py +2 -1
- package/scripts/generate_cline_rules.py +13 -3
- package/scripts/generate_codex.py +105 -0
- package/scripts/generate_codex_hooks.py +78 -0
- package/scripts/generate_codex_rules.py +52 -0
- package/scripts/generate_cursor_mdc.py +2 -1
- package/scripts/generate_roo_rules.py +2 -1
- package/scripts/generate_windsurf_rules.py +2 -1
- package/scripts/generator_base.py +15 -0
- package/scripts/install_steps/ai_tools.py +83 -4
- package/scripts/mcp_editors.py +340 -0
- package/scripts/mcp_manager.py +125 -13
- package/scripts/plugin.py +745 -301
- package/scripts/plugin_schema.py +16 -1
package/llms.txt
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
- [CI Integration](kb/reference/ci-integration.md)
|
|
28
28
|
- [Claude Ecosystem Benchmark Snapshot](kb/reference/claude-ecosystem-benchmark-snapshot.md)
|
|
29
29
|
- [Claude Ecosystem Expansion Foundations](kb/reference/claude-ecosystem-expansion-foundations.md)
|
|
30
|
+
- [CLI Reference](kb/reference/cli-reference.md)
|
|
31
|
+
- [AI Toolkit - Codex CLI Compatibility](kb/reference/codex-cli-compatibility.md)
|
|
32
|
+
- [Ecosystem Comparison](kb/reference/comparison.md)
|
|
30
33
|
- [Plan: Competitive Features — ai-toolkit](kb/reference/competitive-features-implementation.md)
|
|
31
34
|
- [Distribution Model](kb/reference/distribution-model.md)
|
|
32
35
|
- [Enterprise Config Inheritance Guide](kb/reference/enterprise-config-guide.md)
|
|
@@ -38,6 +41,7 @@
|
|
|
38
41
|
- [Language Plugin Packs](kb/reference/language-packs.md)
|
|
39
42
|
- [Language Rules System](kb/reference/language-rules.md)
|
|
40
43
|
- [Manifest-Driven Install System](kb/reference/manifest-install.md)
|
|
44
|
+
- [MCP Editor Compatibility](kb/reference/mcp-editor-compatibility.md)
|
|
41
45
|
- [MCP Server Templates](kb/reference/mcp-templates.md)
|
|
42
46
|
- [Merge-Friendly Install Model](kb/reference/merge-friendly-install-model.md)
|
|
43
47
|
- [Plugin Pack Conventions](kb/reference/plugin-pack-conventions.md)
|
|
@@ -47,4 +51,5 @@
|
|
|
47
51
|
- [Skills Unification Model](kb/reference/skills-unification.md)
|
|
48
52
|
- [Usage Statistics](kb/reference/stats.md)
|
|
49
53
|
- [Config Sync](kb/reference/sync.md)
|
|
54
|
+
- [Unique Features & Differentiators](kb/reference/unique-features.md)
|
|
50
55
|
- [Troubleshooting](kb/troubleshooting/README.md)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softspark/ai-toolkit",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Professional-grade AI coding toolkit: 92 skills, 44 agents, multi-platform support (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo Code, Aider, Augment, Google Antigravity), machine-enforced safety constitution, persona presets, skill security auditor, expanded lifecycle hooks, 11 plugin packs, and benchmark tooling.",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "Professional-grade AI coding toolkit: 92 skills, 44 agents, multi-platform support (Claude, Cursor, Windsurf, Copilot, Gemini, Cline, Roo Code, Aider, Augment, Google Antigravity, Codex CLI), machine-enforced safety constitution, persona presets, skill security auditor, expanded lifecycle hooks, 11 plugin packs, and benchmark tooling.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -46,16 +46,17 @@
|
|
|
46
46
|
"evaluate": "python3 scripts/evaluate_skills.py",
|
|
47
47
|
"benchmark:ecosystem": "python3 scripts/benchmark_ecosystem.py --offline",
|
|
48
48
|
"benchmark:harvest": "python3 scripts/harvest_ecosystem.py --offline",
|
|
49
|
-
"generate:agents": "python3 scripts/
|
|
49
|
+
"generate:agents": "python3 scripts/generate_codex.py > AGENTS.md",
|
|
50
50
|
"generate:cursor": "python3 scripts/generate_cursor_rules.py > .cursorrules",
|
|
51
51
|
"generate:llms": "python3 scripts/generate_llms_txt.py > llms.txt && python3 scripts/generate_llms_txt.py --full > llms-full.txt",
|
|
52
52
|
"generate:windsurf": "python3 scripts/generate_windsurf.py > .windsurfrules",
|
|
53
53
|
"generate:copilot": "python3 scripts/generate_copilot.py > .github/copilot-instructions.md",
|
|
54
54
|
"generate:gemini": "python3 scripts/generate_gemini.py > GEMINI.md",
|
|
55
|
-
"generate:cline": "python3 scripts/
|
|
55
|
+
"generate:cline": "python3 scripts/generate_cline_rules.py .",
|
|
56
56
|
"generate:roo": "python3 scripts/generate_roo_modes.py > .roomodes",
|
|
57
57
|
"generate:aider": "python3 scripts/generate_aider_conf.py > .aider.conf.yml",
|
|
58
|
-
"generate:
|
|
58
|
+
"generate:codex-rules": "python3 scripts/generate_codex_rules.py .",
|
|
59
|
+
"generate:all": "npm run generate:agents && npm run generate:codex-rules && npm run generate:cursor && npm run generate:windsurf && npm run generate:copilot && npm run generate:gemini && npm run generate:cline && npm run generate:roo && npm run generate:aider && npm run generate:llms"
|
|
59
60
|
},
|
|
60
61
|
"files": [
|
|
61
62
|
"bin/",
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Adapt ai-toolkit skills for Codex CLI.
|
|
2
|
+
|
|
3
|
+
Compatible skills are symlinked as-is. Skills that rely on Claude-only
|
|
4
|
+
delegation primitives are rendered into Codex-specific wrappers that keep the
|
|
5
|
+
same references/assets but rewrite the main SKILL.md guidance to use native
|
|
6
|
+
Codex subagents and plan tracking.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from frontmatter import frontmatter_field
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
19
|
+
from frontmatter import frontmatter_field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CLAUDE_ONLY_TOOLS = frozenset({
|
|
23
|
+
"Agent", "TeamCreate", "TeamDelete", "SendMessage",
|
|
24
|
+
"TaskCreate", "TaskList", "TaskUpdate", "TaskGet", "TaskOutput", "TaskStop",
|
|
25
|
+
"Skill", "EnterPlanMode", "ExitPlanMode",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
CODEX_DELEGATION_TOOLS = (
|
|
29
|
+
"spawn_agent", "send_input", "wait_agent", "close_agent", "update_plan",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
ADAPTED_MARKER = ".ai-toolkit-codex-adapted"
|
|
33
|
+
|
|
34
|
+
_FRONTMATTER_RE = re.compile(r"\A---\n(?P<frontmatter>.*?)\n---\n?(?P<body>.*)\Z", re.S)
|
|
35
|
+
_SINGLE_AGENT_CALL_RE = re.compile(
|
|
36
|
+
r'Agent\(subagent_type="([^"]+)",\s*prompt="([^"]+)"[^)]*\)'
|
|
37
|
+
)
|
|
38
|
+
_MULTILINE_AGENT_CALL_RE = re.compile(
|
|
39
|
+
r'Agent\(\s*\n'
|
|
40
|
+
r'\s*subagent_type="([^"]+)"\s*,\s*\n'
|
|
41
|
+
r'\s*description="([^"]+)"\s*,\s*\n'
|
|
42
|
+
r'\s*prompt="([^"]+)"\s*\n'
|
|
43
|
+
r'\)',
|
|
44
|
+
re.S,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_CODEX_NOTE = """
|
|
48
|
+
## Codex Translation Layer
|
|
49
|
+
|
|
50
|
+
This generated Codex variant preserves the original workflow while translating
|
|
51
|
+
Claude-only delegation primitives into Codex-native ones:
|
|
52
|
+
|
|
53
|
+
- Use `spawn_agent(..., fork_context=True, ...)` instead of Claude `Agent(...)`.
|
|
54
|
+
- Pick `explorer` for read-only discovery, `worker` for edits, `default` for
|
|
55
|
+
synthesis, planning, and mixed execution.
|
|
56
|
+
- Use `send_input` to redirect or clarify an active subagent.
|
|
57
|
+
- Use `wait_agent` only when the next critical-path step is blocked on a
|
|
58
|
+
delegated result.
|
|
59
|
+
- Use `close_agent` after integrating finished subagents.
|
|
60
|
+
- Track progress with `update_plan` or a local checklist instead of Claude
|
|
61
|
+
task/team APIs.
|
|
62
|
+
- Treat a "team" as a coordinated set of spawned subagents with explicit file
|
|
63
|
+
ownership. No extra Codex feature flag is required.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def skill_tools(skill_file: Path) -> list[str]:
|
|
68
|
+
"""Return ordered allowed-tools entries from a skill frontmatter block."""
|
|
69
|
+
tools_str = frontmatter_field(skill_file, "allowed-tools") or ""
|
|
70
|
+
return [tool.strip() for tool in tools_str.split(",") if tool.strip()]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_codex_adapted_skill(skill_file: Path) -> bool:
|
|
74
|
+
"""Return True if a skill needs Claude→Codex delegation adaptation."""
|
|
75
|
+
return bool(set(skill_tools(skill_file)) & CLAUDE_ONLY_TOOLS)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def codex_skill_description(skill_file: Path) -> str:
|
|
79
|
+
"""Return the skill description shown in Codex-facing generators."""
|
|
80
|
+
description = frontmatter_field(skill_file, "description")
|
|
81
|
+
if not description:
|
|
82
|
+
return ""
|
|
83
|
+
if is_codex_adapted_skill(skill_file):
|
|
84
|
+
return f"{description} Codex-adapted: uses native subagents and plan tracking."
|
|
85
|
+
return description
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_codex_skill_text(skill_file: Path) -> str:
|
|
89
|
+
"""Render the Codex-facing SKILL.md contents for a source skill."""
|
|
90
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
91
|
+
match = _FRONTMATTER_RE.match(text)
|
|
92
|
+
if not match:
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
frontmatter = _parse_frontmatter(match.group("frontmatter"))
|
|
96
|
+
body = match.group("body")
|
|
97
|
+
adapted = is_codex_adapted_skill(skill_file)
|
|
98
|
+
|
|
99
|
+
if adapted:
|
|
100
|
+
frontmatter = _adapt_frontmatter(frontmatter)
|
|
101
|
+
body = _adapt_body(skill_file.parent.name, body)
|
|
102
|
+
|
|
103
|
+
rendered_frontmatter = _render_frontmatter(frontmatter)
|
|
104
|
+
return f"---\n{rendered_frontmatter}\n---\n{body.rstrip()}\n"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def sync_codex_skill(skill_dir: Path, skills_dst: Path) -> str:
|
|
108
|
+
"""Install one skill into `.agents/skills/` and return its mode."""
|
|
109
|
+
skill_file = skill_dir / "SKILL.md"
|
|
110
|
+
target = skills_dst / skill_dir.name
|
|
111
|
+
adapted = is_codex_adapted_skill(skill_file)
|
|
112
|
+
marker = target / ADAPTED_MARKER
|
|
113
|
+
|
|
114
|
+
if adapted:
|
|
115
|
+
if target.is_symlink():
|
|
116
|
+
target.unlink()
|
|
117
|
+
elif target.is_dir() and not marker.is_file():
|
|
118
|
+
return "skipped"
|
|
119
|
+
elif target.is_dir():
|
|
120
|
+
shutil.rmtree(target)
|
|
121
|
+
elif target.exists():
|
|
122
|
+
return "skipped"
|
|
123
|
+
|
|
124
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
marker.write_text("generated by ai-toolkit for Codex\n", encoding="utf-8")
|
|
126
|
+
|
|
127
|
+
for child in sorted(skill_dir.iterdir()):
|
|
128
|
+
dest = target / child.name
|
|
129
|
+
if child.name == "SKILL.md":
|
|
130
|
+
continue
|
|
131
|
+
if dest.is_symlink() or dest.is_file():
|
|
132
|
+
dest.unlink()
|
|
133
|
+
elif dest.is_dir():
|
|
134
|
+
shutil.rmtree(dest)
|
|
135
|
+
dest.symlink_to(child)
|
|
136
|
+
|
|
137
|
+
(target / "SKILL.md").write_text(build_codex_skill_text(skill_file), encoding="utf-8")
|
|
138
|
+
return "adapted"
|
|
139
|
+
|
|
140
|
+
if target.is_symlink():
|
|
141
|
+
if target.resolve() == skill_dir.resolve():
|
|
142
|
+
return "linked"
|
|
143
|
+
target.unlink()
|
|
144
|
+
elif target.is_dir():
|
|
145
|
+
if marker.is_file():
|
|
146
|
+
shutil.rmtree(target)
|
|
147
|
+
else:
|
|
148
|
+
return "skipped"
|
|
149
|
+
elif target.exists():
|
|
150
|
+
return "skipped"
|
|
151
|
+
|
|
152
|
+
target.symlink_to(skill_dir)
|
|
153
|
+
return "linked"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cleanup_codex_skills(skills_dst: Path, skills_src: Path) -> None:
|
|
157
|
+
"""Remove broken symlinks and stale generated Codex skill wrappers."""
|
|
158
|
+
for item in skills_dst.iterdir():
|
|
159
|
+
src = skills_src / item.name
|
|
160
|
+
if item.is_symlink() and not item.exists():
|
|
161
|
+
item.unlink()
|
|
162
|
+
continue
|
|
163
|
+
if item.is_dir() and (item / ADAPTED_MARKER).is_file() and not src.is_dir():
|
|
164
|
+
shutil.rmtree(item)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _parse_frontmatter(frontmatter_text: str) -> list[tuple[str, str]]:
|
|
168
|
+
entries: list[tuple[str, str]] = []
|
|
169
|
+
for line in frontmatter_text.splitlines():
|
|
170
|
+
if ":" not in line:
|
|
171
|
+
continue
|
|
172
|
+
key, value = line.split(":", 1)
|
|
173
|
+
entries.append((key.strip(), value.strip()))
|
|
174
|
+
return entries
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _render_frontmatter(entries: list[tuple[str, str]]) -> str:
|
|
178
|
+
return "\n".join(f"{key}: {value}" for key, value in entries)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _adapt_frontmatter(entries: list[tuple[str, str]]) -> list[tuple[str, str]]:
|
|
182
|
+
adapted: list[tuple[str, str]] = []
|
|
183
|
+
for key, value in entries:
|
|
184
|
+
if key in {"context", "agent", "model"}:
|
|
185
|
+
continue
|
|
186
|
+
if key == "description":
|
|
187
|
+
stripped = _strip_quotes(value)
|
|
188
|
+
value = f'"{stripped} Codex-adapted: uses native subagents and plan tracking."'
|
|
189
|
+
elif key == "allowed-tools":
|
|
190
|
+
value = ", ".join(_adapt_allowed_tools(value))
|
|
191
|
+
adapted.append((key, value))
|
|
192
|
+
return adapted
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _adapt_allowed_tools(value: str) -> list[str]:
|
|
196
|
+
tools = []
|
|
197
|
+
for tool in [item.strip() for item in value.split(",") if item.strip()]:
|
|
198
|
+
if tool in CLAUDE_ONLY_TOOLS:
|
|
199
|
+
continue
|
|
200
|
+
if tool not in tools:
|
|
201
|
+
tools.append(tool)
|
|
202
|
+
for tool in CODEX_DELEGATION_TOOLS:
|
|
203
|
+
if tool not in tools:
|
|
204
|
+
tools.append(tool)
|
|
205
|
+
return tools
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _adapt_body(skill_name: str, body: str) -> str:
|
|
209
|
+
body = body.replace(
|
|
210
|
+
"## MANDATORY: You MUST use the Agent tool",
|
|
211
|
+
"## MANDATORY: Use Codex subagents for delegation",
|
|
212
|
+
)
|
|
213
|
+
body = body.replace("`Agent` tool", "Codex subagent tools")
|
|
214
|
+
body = body.replace("the `Agent` tool", "Codex subagent tools")
|
|
215
|
+
body = body.replace("Agent tool", "Codex subagent tools")
|
|
216
|
+
body = body.replace("Agent Teams", "parallel Codex subagents")
|
|
217
|
+
body = body.replace(
|
|
218
|
+
"Requires: `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`",
|
|
219
|
+
"Requires: native Codex subagent support (`spawn_agent`, `send_input`, `wait_agent`, `close_agent`)",
|
|
220
|
+
)
|
|
221
|
+
body = body.replace(
|
|
222
|
+
"Check `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS` is set; warn if not",
|
|
223
|
+
"Confirm the user explicitly wants delegated/subagent execution before launching workers",
|
|
224
|
+
)
|
|
225
|
+
body = body.replace(
|
|
226
|
+
"export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1",
|
|
227
|
+
"# No extra environment variable is required in Codex",
|
|
228
|
+
)
|
|
229
|
+
body = body.replace("TaskCreate", "update_plan")
|
|
230
|
+
body = body.replace("TaskList", "update_plan")
|
|
231
|
+
body = body.replace("TaskUpdate", "update_plan")
|
|
232
|
+
body = body.replace("TaskGet", "wait_agent")
|
|
233
|
+
body = body.replace("TaskOutput", "wait_agent")
|
|
234
|
+
body = body.replace("TaskStop", "close_agent")
|
|
235
|
+
body = body.replace("SendMessage", "send_input")
|
|
236
|
+
body = body.replace("TeamCreate", "spawn_agent")
|
|
237
|
+
body = body.replace("TeamDelete", "close_agent")
|
|
238
|
+
|
|
239
|
+
body = _MULTILINE_AGENT_CALL_RE.sub(_replace_multiline_agent_call, body)
|
|
240
|
+
body = _SINGLE_AGENT_CALL_RE.sub(_replace_single_agent_call, body)
|
|
241
|
+
|
|
242
|
+
if "$ARGUMENTS" in body:
|
|
243
|
+
body = body.replace("$ARGUMENTS", f"$ARGUMENTS\n{_CODEX_NOTE.rstrip()}", 1)
|
|
244
|
+
|
|
245
|
+
if skill_name == "teams":
|
|
246
|
+
body = body.replace(
|
|
247
|
+
"Launches a pre-configured Agent Teams composition for your task.",
|
|
248
|
+
"Launches a pre-configured Codex subagent composition for your task.",
|
|
249
|
+
)
|
|
250
|
+
return body
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _replace_single_agent_call(match: re.Match[str]) -> str:
|
|
254
|
+
role = match.group(1)
|
|
255
|
+
prompt = match.group(2)
|
|
256
|
+
agent_type = _codex_agent_type(role, prompt)
|
|
257
|
+
escaped_prompt = prompt.replace('"', "'")
|
|
258
|
+
return (
|
|
259
|
+
f'spawn_agent(agent_type="{agent_type}", fork_context=True, '
|
|
260
|
+
f'message="Act as {role}. {escaped_prompt}")'
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _replace_multiline_agent_call(match: re.Match[str]) -> str:
|
|
265
|
+
role = match.group(1)
|
|
266
|
+
prompt = match.group(3).replace('"', "'")
|
|
267
|
+
return (
|
|
268
|
+
"spawn_agent(\n"
|
|
269
|
+
f' agent_type="{_codex_agent_type(role, prompt)}",\n'
|
|
270
|
+
" fork_context=True,\n"
|
|
271
|
+
f' message="Act as {role}. {prompt}"\n'
|
|
272
|
+
")"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _codex_agent_type(role: str, prompt: str) -> str:
|
|
277
|
+
role_l = role.lower()
|
|
278
|
+
prompt_l = prompt.lower()
|
|
279
|
+
if role_l == "explorer-agent":
|
|
280
|
+
return "explorer"
|
|
281
|
+
if any(token in prompt_l for token in ("read-only", "review", "audit", "trace", "map ", "analyze")):
|
|
282
|
+
return "default"
|
|
283
|
+
if any(token in prompt_l for token in (
|
|
284
|
+
"own files:", "implement", "write ", "apply", "build ", "update ",
|
|
285
|
+
"create ", "execute ", "fix", "document", "deploy",
|
|
286
|
+
)):
|
|
287
|
+
return "worker"
|
|
288
|
+
return "default"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _strip_quotes(value: str) -> str:
|
|
292
|
+
value = value.strip()
|
|
293
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
|
|
294
|
+
return value[1:-1]
|
|
295
|
+
return value
|
|
@@ -20,6 +20,11 @@ from emission import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
PREFIX = "ai-toolkit-"
|
|
23
|
+
LANG_PREFIX = f"{PREFIX}lang-"
|
|
24
|
+
CUSTOM_PREFIX = f"{PREFIX}custom-"
|
|
25
|
+
STANDARD_SCOPE = "standard"
|
|
26
|
+
LANG_SCOPE = "lang"
|
|
27
|
+
CUSTOM_SCOPE = "custom"
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
# ---------------------------------------------------------------------------
|
|
@@ -415,25 +420,59 @@ STANDARD_WORKFLOWS: dict[str, callable] = {
|
|
|
415
420
|
# Shared file generation helpers
|
|
416
421
|
# ---------------------------------------------------------------------------
|
|
417
422
|
|
|
418
|
-
def
|
|
419
|
-
"""
|
|
423
|
+
def rule_scope(filename: str) -> str | None:
|
|
424
|
+
"""Classify a generated rule file by ownership scope."""
|
|
425
|
+
if filename.startswith(LANG_PREFIX):
|
|
426
|
+
return LANG_SCOPE
|
|
427
|
+
if filename.startswith(CUSTOM_PREFIX):
|
|
428
|
+
return CUSTOM_SCOPE
|
|
429
|
+
if filename.startswith(PREFIX):
|
|
430
|
+
return STANDARD_SCOPE
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def cleanup_stale(
|
|
435
|
+
directory: Path,
|
|
436
|
+
current_files: set[str],
|
|
437
|
+
*,
|
|
438
|
+
managed_scopes: tuple[str, ...] = (STANDARD_SCOPE, LANG_SCOPE, CUSTOM_SCOPE),
|
|
439
|
+
extra_managed_names: set[str] | None = None,
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Remove managed files that are no longer in the active registry."""
|
|
420
442
|
if not directory.is_dir():
|
|
421
443
|
return
|
|
444
|
+
scope_set = set(managed_scopes)
|
|
445
|
+
extra_names = extra_managed_names or set()
|
|
422
446
|
for f in directory.iterdir():
|
|
423
|
-
if f.name
|
|
447
|
+
if f.name in current_files:
|
|
448
|
+
continue
|
|
449
|
+
if f.name in extra_names:
|
|
450
|
+
f.unlink()
|
|
451
|
+
print(f" Removed stale: {f.relative_to(directory.parent.parent)}")
|
|
452
|
+
continue
|
|
453
|
+
if rule_scope(f.name) in scope_set:
|
|
424
454
|
f.unlink()
|
|
425
455
|
print(f" Removed stale: {f.relative_to(directory.parent.parent)}")
|
|
426
456
|
|
|
427
457
|
|
|
428
458
|
def write_rules(target_dir: Path, rules: dict[str, callable],
|
|
429
|
-
subdir: str = "rules", label: str = ""
|
|
459
|
+
subdir: str = "rules", label: str = "",
|
|
460
|
+
cleanup: bool = True,
|
|
461
|
+
managed_scopes: tuple[str, ...] = (STANDARD_SCOPE, LANG_SCOPE, CUSTOM_SCOPE),
|
|
462
|
+
extra_managed_names: set[str] | None = None) -> None:
|
|
430
463
|
"""Write rule files to target_dir/<subdir>/.
|
|
431
464
|
|
|
432
465
|
Only writes ai-toolkit-* files. User files are never touched.
|
|
433
466
|
"""
|
|
434
467
|
out_dir = target_dir / subdir
|
|
435
468
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
436
|
-
|
|
469
|
+
if cleanup:
|
|
470
|
+
cleanup_stale(
|
|
471
|
+
out_dir,
|
|
472
|
+
set(rules.keys()),
|
|
473
|
+
managed_scopes=managed_scopes,
|
|
474
|
+
extra_managed_names=extra_managed_names,
|
|
475
|
+
)
|
|
437
476
|
|
|
438
477
|
for filename, content_fn in rules.items():
|
|
439
478
|
(out_dir / filename).write_text(content_fn(), encoding="utf-8")
|
|
@@ -511,7 +550,7 @@ def build_language_rules(
|
|
|
511
550
|
|
|
512
551
|
if parts:
|
|
513
552
|
combined = "\n\n".join(parts) + "\n"
|
|
514
|
-
filename = f"{
|
|
553
|
+
filename = f"{LANG_PREFIX}{lang}.md"
|
|
515
554
|
# Capture value via default arg to avoid late-binding closure
|
|
516
555
|
result[filename] = (lambda c: lambda: c)(combined)
|
|
517
556
|
|
|
@@ -531,7 +570,7 @@ def build_registered_rules(
|
|
|
531
570
|
result: dict[str, callable] = {}
|
|
532
571
|
for rule_file in sorted(rules_dir.glob("*.md")):
|
|
533
572
|
content = rule_file.read_text(encoding="utf-8")
|
|
534
|
-
filename = f"{
|
|
573
|
+
filename = f"{CUSTOM_PREFIX}{rule_file.stem}.md"
|
|
535
574
|
result[filename] = (lambda c: lambda: c)(content)
|
|
536
575
|
|
|
537
576
|
return result
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
13
13
|
from _common import agents_dir, frontmatter_field
|
|
14
|
+
from paths import RULES_DIR
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def main() -> None:
|
|
@@ -86,6 +87,18 @@ def main() -> None:
|
|
|
86
87
|
print("---")
|
|
87
88
|
print()
|
|
88
89
|
|
|
90
|
+
# Registered custom rules from ~/.softspark/ai-toolkit/rules/
|
|
91
|
+
if RULES_DIR.is_dir():
|
|
92
|
+
for rule_file in sorted(RULES_DIR.glob("*.md")):
|
|
93
|
+
rule_name = rule_file.stem
|
|
94
|
+
print(f"<!-- TOOLKIT:{rule_name} START -->")
|
|
95
|
+
print("<!-- Auto-injected by ai-toolkit. Re-run to update. -->")
|
|
96
|
+
print()
|
|
97
|
+
print(rule_file.read_text(encoding="utf-8").rstrip())
|
|
98
|
+
print()
|
|
99
|
+
print(f"<!-- TOOLKIT:{rule_name} END -->")
|
|
100
|
+
print()
|
|
101
|
+
|
|
89
102
|
|
|
90
103
|
if __name__ == "__main__":
|
|
91
104
|
main()
|
|
@@ -134,7 +134,8 @@ def generate(target_dir: Path, *,
|
|
|
134
134
|
|
|
135
135
|
def main() -> None:
|
|
136
136
|
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
137
|
-
|
|
137
|
+
from paths import RULES_DIR
|
|
138
|
+
generate(target, rules_dir=RULES_DIR)
|
|
138
139
|
|
|
139
140
|
|
|
140
141
|
if __name__ == "__main__":
|
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
17
|
from dir_rules_shared import (
|
|
18
18
|
STANDARD_RULES,
|
|
19
|
+
STANDARD_SCOPE,
|
|
19
20
|
build_language_rules,
|
|
20
21
|
build_registered_rules,
|
|
21
22
|
write_rules,
|
|
@@ -24,7 +25,9 @@ from dir_rules_shared import (
|
|
|
24
25
|
|
|
25
26
|
def generate(target_dir: Path, *,
|
|
26
27
|
language_modules: list[str] | None = None,
|
|
27
|
-
rules_dir: Path | None = None
|
|
28
|
+
rules_dir: Path | None = None,
|
|
29
|
+
cleanup: bool = True,
|
|
30
|
+
managed_scopes: tuple[str, ...] = (STANDARD_SCOPE,)) -> None:
|
|
28
31
|
"""Write .clinerules/*.md files to target_dir."""
|
|
29
32
|
# Migrate: if .clinerules exists as a single file, remove it
|
|
30
33
|
# so the directory can be created (Cline 3.7+ uses directory format)
|
|
@@ -34,12 +37,19 @@ def generate(target_dir: Path, *,
|
|
|
34
37
|
rules = dict(STANDARD_RULES)
|
|
35
38
|
rules.update(build_language_rules(language_modules))
|
|
36
39
|
rules.update(build_registered_rules(rules_dir))
|
|
37
|
-
write_rules(
|
|
40
|
+
write_rules(
|
|
41
|
+
target_dir,
|
|
42
|
+
rules,
|
|
43
|
+
".clinerules",
|
|
44
|
+
cleanup=cleanup,
|
|
45
|
+
managed_scopes=managed_scopes,
|
|
46
|
+
)
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
def main() -> None:
|
|
41
50
|
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
42
|
-
|
|
51
|
+
from paths import RULES_DIR
|
|
52
|
+
generate(target, rules_dir=RULES_DIR)
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate AGENTS.md content for OpenAI Codex CLI.
|
|
3
|
+
|
|
4
|
+
Adapts Claude-oriented skills to Codex-native delegation guidance so the full
|
|
5
|
+
skill catalog can be surfaced in Codex installs.
|
|
6
|
+
|
|
7
|
+
Usage: ./scripts/generate_codex.py > AGENTS.md
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
|
+
from codex_skill_adapter import codex_skill_description
|
|
16
|
+
from emission import (
|
|
17
|
+
agents_dir,
|
|
18
|
+
skills_dir,
|
|
19
|
+
generate_quality_standards,
|
|
20
|
+
generate_workflow_guidelines,
|
|
21
|
+
print_toolkit_end,
|
|
22
|
+
print_toolkit_start,
|
|
23
|
+
)
|
|
24
|
+
from frontmatter import frontmatter_field
|
|
25
|
+
from paths import RULES_DIR
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _emit_agents() -> str:
|
|
29
|
+
"""Emit agents as bullets."""
|
|
30
|
+
lines: list[str] = []
|
|
31
|
+
for agent_file in sorted(agents_dir.glob("*.md")):
|
|
32
|
+
name = frontmatter_field(agent_file, "name")
|
|
33
|
+
description = frontmatter_field(agent_file, "description")
|
|
34
|
+
if not name or not description:
|
|
35
|
+
continue
|
|
36
|
+
lines.append(f"- **{name}**: {description}")
|
|
37
|
+
return "\n".join(lines)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _emit_skills() -> str:
|
|
41
|
+
"""Emit skills as bullets, adapting Claude-native descriptions for Codex."""
|
|
42
|
+
lines: list[str] = []
|
|
43
|
+
for skill_dir in sorted(skills_dir.iterdir()):
|
|
44
|
+
if skill_dir.name.startswith("_"):
|
|
45
|
+
continue
|
|
46
|
+
skill_file = skill_dir / "SKILL.md"
|
|
47
|
+
if not skill_file.is_file():
|
|
48
|
+
continue
|
|
49
|
+
name = frontmatter_field(skill_file, "name")
|
|
50
|
+
description = codex_skill_description(skill_file)
|
|
51
|
+
if not name or not description:
|
|
52
|
+
continue
|
|
53
|
+
lines.append(f"- **{name}**: {description}")
|
|
54
|
+
return "\n".join(lines)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
print_toolkit_start()
|
|
59
|
+
|
|
60
|
+
print("# AI Toolkit — Codex CLI Configuration")
|
|
61
|
+
print()
|
|
62
|
+
print(
|
|
63
|
+
"Shared AI development toolkit with specialized agents,"
|
|
64
|
+
" Codex-compatible skills, quality hooks, and a safety constitution."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Agents (all agents are informational — safe to list)
|
|
68
|
+
print()
|
|
69
|
+
print("## Available Agents")
|
|
70
|
+
print()
|
|
71
|
+
print("Specialized agent personas — apply their expertise for relevant tasks:")
|
|
72
|
+
print()
|
|
73
|
+
print(_emit_agents())
|
|
74
|
+
|
|
75
|
+
# Skills
|
|
76
|
+
print()
|
|
77
|
+
print("## Available Skills")
|
|
78
|
+
print()
|
|
79
|
+
print("Skills are invocable commands or auto-loaded knowledge sources:")
|
|
80
|
+
print()
|
|
81
|
+
print(_emit_skills())
|
|
82
|
+
|
|
83
|
+
# Guidelines
|
|
84
|
+
print()
|
|
85
|
+
print(generate_quality_standards())
|
|
86
|
+
print()
|
|
87
|
+
print(generate_workflow_guidelines())
|
|
88
|
+
|
|
89
|
+
print_toolkit_end()
|
|
90
|
+
|
|
91
|
+
# Registered custom rules from ~/.softspark/ai-toolkit/rules/
|
|
92
|
+
if RULES_DIR.is_dir():
|
|
93
|
+
for rule_file in sorted(RULES_DIR.glob("*.md")):
|
|
94
|
+
rule_name = rule_file.stem
|
|
95
|
+
print()
|
|
96
|
+
print(f"<!-- TOOLKIT:{rule_name} START -->")
|
|
97
|
+
print("<!-- Auto-injected by ai-toolkit. Re-run to update. -->")
|
|
98
|
+
print()
|
|
99
|
+
print(rule_file.read_text(encoding="utf-8").rstrip())
|
|
100
|
+
print()
|
|
101
|
+
print(f"<!-- TOOLKIT:{rule_name} END -->")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|