@softspark/ai-toolkit 2.0.2 → 2.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/llms.txt CHANGED
@@ -27,6 +27,7 @@
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
+ - [AI Toolkit - Codex CLI Compatibility](kb/reference/codex-cli-compatibility.md)
30
31
  - [Plan: Competitive Features — ai-toolkit](kb/reference/competitive-features-implementation.md)
31
32
  - [Distribution Model](kb/reference/distribution-model.md)
32
33
  - [Enterprise Config Inheritance Guide](kb/reference/enterprise-config-guide.md)
@@ -38,6 +39,7 @@
38
39
  - [Language Plugin Packs](kb/reference/language-packs.md)
39
40
  - [Language Rules System](kb/reference/language-rules.md)
40
41
  - [Manifest-Driven Install System](kb/reference/manifest-install.md)
42
+ - [MCP Editor Compatibility](kb/reference/mcp-editor-compatibility.md)
41
43
  - [MCP Server Templates](kb/reference/mcp-templates.md)
42
44
  - [Merge-Friendly Install Model](kb/reference/merge-friendly-install-model.md)
43
45
  - [Plugin Pack Conventions](kb/reference/plugin-pack-conventions.md)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@softspark/ai-toolkit",
3
- "version": "2.0.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.0",
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",
@@ -52,10 +52,11 @@
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/generate_cline.py > .clinerules",
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:all": "npm run generate:agents && 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"
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 cleanup_stale(directory: Path, current_files: set[str]) -> None:
419
- """Remove ai-toolkit-* files that are no longer in the registry."""
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.startswith(PREFIX) and f.name not in current_files:
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 = "") -> None:
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
- cleanup_stale(out_dir, set(rules.keys()))
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"{PREFIX}lang-{lang}.md"
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"{PREFIX}custom-{rule_file.stem}.md"
573
+ filename = f"{CUSTOM_PREFIX}{rule_file.stem}.md"
535
574
  result[filename] = (lambda c: lambda: c)(content)
536
575
 
537
576
  return result
@@ -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) -> 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,7 +37,13 @@ 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(target_dir, rules, ".clinerules")
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:
@@ -0,0 +1,92 @@
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
+
26
+
27
+ def _emit_agents() -> str:
28
+ """Emit agents as bullets."""
29
+ lines: list[str] = []
30
+ for agent_file in sorted(agents_dir.glob("*.md")):
31
+ name = frontmatter_field(agent_file, "name")
32
+ description = frontmatter_field(agent_file, "description")
33
+ if not name or not description:
34
+ continue
35
+ lines.append(f"- **{name}**: {description}")
36
+ return "\n".join(lines)
37
+
38
+
39
+ def _emit_skills() -> str:
40
+ """Emit skills as bullets, adapting Claude-native descriptions for Codex."""
41
+ lines: list[str] = []
42
+ for skill_dir in sorted(skills_dir.iterdir()):
43
+ if skill_dir.name.startswith("_"):
44
+ continue
45
+ skill_file = skill_dir / "SKILL.md"
46
+ if not skill_file.is_file():
47
+ continue
48
+ name = frontmatter_field(skill_file, "name")
49
+ description = codex_skill_description(skill_file)
50
+ if not name or not description:
51
+ continue
52
+ lines.append(f"- **{name}**: {description}")
53
+ return "\n".join(lines)
54
+
55
+
56
+ def main() -> None:
57
+ print_toolkit_start()
58
+
59
+ print("# AI Toolkit — Codex CLI Configuration")
60
+ print()
61
+ print(
62
+ "Shared AI development toolkit with specialized agents,"
63
+ " Codex-compatible skills, quality hooks, and a safety constitution."
64
+ )
65
+
66
+ # Agents (all agents are informational — safe to list)
67
+ print()
68
+ print("## Available Agents")
69
+ print()
70
+ print("Specialized agent personas — apply their expertise for relevant tasks:")
71
+ print()
72
+ print(_emit_agents())
73
+
74
+ # Skills
75
+ print()
76
+ print("## Available Skills")
77
+ print()
78
+ print("Skills are invocable commands or auto-loaded knowledge sources:")
79
+ print()
80
+ print(_emit_skills())
81
+
82
+ # Guidelines
83
+ print()
84
+ print(generate_quality_standards())
85
+ print()
86
+ print(generate_workflow_guidelines())
87
+
88
+ print_toolkit_end()
89
+
90
+
91
+ if __name__ == "__main__":
92
+ main()
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Generate .codex/hooks.json for OpenAI Codex CLI.
3
+
4
+ Maps compatible ai-toolkit hooks to Codex lifecycle events.
5
+ Hook scripts are shared with Claude Code (stored in ~/.softspark/ai-toolkit/hooks/).
6
+
7
+ Codex supports 5 events: SessionStart, PreToolUse, PostToolUse,
8
+ UserPromptSubmit, Stop. PreToolUse/PostToolUse only support Bash matcher.
9
+
10
+ Usage:
11
+ python3 scripts/generate_codex_hooks.py [target-dir]
12
+
13
+ Writes .codex/hooks.json to target-dir.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+
22
+ HOOKS_PREFIX = '"$HOME/.softspark/ai-toolkit/hooks/'
23
+
24
+ # Hooks compatible with Codex, grouped by event.
25
+ # Format: (matcher, script_name)
26
+ CODEX_HOOKS: dict[str, list[tuple[str, str]]] = {
27
+ "SessionStart": [
28
+ ("startup|resume", "session-start.sh"),
29
+ ("startup|resume", "mcp-health.sh"),
30
+ ("startup|resume", "session-context.sh"),
31
+ ],
32
+ "PreToolUse": [
33
+ ("Bash", "guard-destructive.sh"),
34
+ ("Bash", "commit-quality.sh"),
35
+ ],
36
+ "UserPromptSubmit": [
37
+ ("", "user-prompt-submit.sh"),
38
+ ("", "track-usage.sh"),
39
+ ],
40
+ "Stop": [
41
+ ("", "quality-check.sh"),
42
+ ("", "save-session.sh"),
43
+ ],
44
+ }
45
+
46
+
47
+ def build_hooks_json() -> dict:
48
+ """Build the hooks.json structure for Codex."""
49
+ hooks: dict[str, list] = {}
50
+ for event, entries in CODEX_HOOKS.items():
51
+ hooks[event] = []
52
+ for matcher, script in entries:
53
+ entry: dict = {"hooks": [{"type": "command", "command": f"{HOOKS_PREFIX}{script}\""}]}
54
+ if matcher:
55
+ entry["matcher"] = matcher
56
+ hooks[event].append(entry)
57
+ return {"hooks": hooks}
58
+
59
+
60
+ def generate(target_dir: Path) -> None:
61
+ """Write .codex/hooks.json to target_dir."""
62
+ codex_dir = target_dir / ".codex"
63
+ codex_dir.mkdir(parents=True, exist_ok=True)
64
+ hooks_path = codex_dir / "hooks.json"
65
+ data = build_hooks_json()
66
+ with open(hooks_path, "w", encoding="utf-8") as f:
67
+ json.dump(data, f, indent=4, ensure_ascii=False)
68
+ f.write("\n")
69
+
70
+
71
+ def main() -> None:
72
+ target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
73
+ generate(target)
74
+ print(f"Generated: .codex/hooks.json ({sum(len(v) for v in CODEX_HOOKS.values())} hooks)")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env python3
2
+ """Generate Codex CLI .agents/rules/ files.
3
+
4
+ Codex discovers rules in .agents/rules/ at the project root.
5
+ This generator follows the same pattern as generate_antigravity.py.
6
+
7
+ Usage:
8
+ python3 scripts/generate_codex_rules.py [target-dir]
9
+
10
+ Writes files directly to target-dir/.agents/rules/.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+ from dir_rules_shared import (
19
+ STANDARD_RULES,
20
+ STANDARD_SCOPE,
21
+ build_language_rules,
22
+ build_registered_rules,
23
+ write_rules,
24
+ )
25
+
26
+
27
+ def generate(target_dir: Path, *,
28
+ language_modules: list[str] | None = None,
29
+ rules_dir: Path | None = None,
30
+ cleanup: bool = True,
31
+ managed_scopes: tuple[str, ...] = (STANDARD_SCOPE,)) -> None:
32
+ """Write .agents/rules/ files to target_dir."""
33
+ rules = dict(STANDARD_RULES)
34
+ rules.update(build_language_rules(language_modules))
35
+ rules.update(build_registered_rules(rules_dir))
36
+ write_rules(
37
+ target_dir,
38
+ rules,
39
+ ".agents/rules",
40
+ cleanup=cleanup,
41
+ managed_scopes=managed_scopes,
42
+ )
43
+
44
+
45
+ def main() -> None:
46
+ target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
47
+ generate(target)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()