@softspark/ai-toolkit 4.14.1 → 4.15.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.
Files changed (49) hide show
  1. package/AGENTS.md +117 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +9 -10
  4. package/app/.claude-plugin/plugin.json +1 -1
  5. package/app/CLAUDE.md.template +3 -0
  6. package/app/hooks/_search-capability.sh +3 -2
  7. package/app/hooks/stop-search-check.sh +2 -1
  8. package/benchmarks/ecosystem-doctor-snapshot.json +73 -31
  9. package/kb/procedures/maintenance-sop.md +26 -13
  10. package/kb/procedures/release-verification-sop.md +41 -36
  11. package/kb/reference/architecture-overview.md +23 -7
  12. package/kb/reference/codex-cli-compatibility.md +96 -36
  13. package/kb/reference/extension-api.md +52 -9
  14. package/kb/reference/global-install-model.md +56 -21
  15. package/kb/reference/hooks-catalog.md +44 -8
  16. package/kb/reference/mcp-editor-compatibility.md +27 -6
  17. package/kb/reference/mcp-templates.md +12 -6
  18. package/kb/reference/opencode-compatibility.md +13 -7
  19. package/kb/reference/plugin-pack-conventions.md +7 -7
  20. package/kb/reference/skills-catalog.md +3 -3
  21. package/kb/reference/supported-tools-registry.md +19 -17
  22. package/kb/reference/windows-support.md +27 -3
  23. package/llms-full.txt +447 -180
  24. package/llms.txt +1 -1
  25. package/manifest.json +1 -1
  26. package/package.json +2 -2
  27. package/scripts/codex_skill_adapter.py +448 -198
  28. package/scripts/copilot_legacy_hashes.json +338 -0
  29. package/scripts/dir_rules_shared.py +2 -11
  30. package/scripts/ecosystem_tools.json +29 -8
  31. package/scripts/emission.py +5 -91
  32. package/scripts/generate_agents_md.py +4 -87
  33. package/scripts/generate_codex.py +5 -95
  34. package/scripts/generate_codex_agents.py +242 -0
  35. package/scripts/generate_codex_hooks.py +648 -55
  36. package/scripts/generate_codex_skills.py +15 -6
  37. package/scripts/generate_copilot.py +1187 -97
  38. package/scripts/generate_copilot_hooks.py +723 -0
  39. package/scripts/generate_cursor_hooks.py +453 -121
  40. package/scripts/generate_opencode_commands.py +4 -6
  41. package/scripts/inject_hook_cli.py +770 -205
  42. package/scripts/injection.py +102 -23
  43. package/scripts/install_steps/ai_tools.py +136 -83
  44. package/scripts/instruction_core.py +95 -0
  45. package/scripts/mcp_editors.py +934 -80
  46. package/scripts/mcp_manager.py +46 -26
  47. package/scripts/plugin.py +291 -114
  48. package/scripts/secure_fs.py +538 -0
  49. package/scripts/uninstall.py +1279 -208
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
- """Generate AGENTS.md from app/agents/*.md frontmatter.
2
+ """Generate the shared AGENTS.md instruction core.
3
3
 
4
- Output is compatible with Codex, OpenCode, and Gemini CLI AGENTS.md format.
4
+ Output is compatible with the Codex and GitHub Copilot AGENTS.md format.
5
5
  Usage: ./scripts/generate_agents_md.py > AGENTS.md
6
6
  """
7
7
  from __future__ import annotations
@@ -10,94 +10,11 @@ import sys
10
10
  from pathlib import Path
11
11
 
12
12
  sys.path.insert(0, str(Path(__file__).resolve().parent))
13
- import subprocess
14
-
15
- from _common import agents_dir, frontmatter_field
13
+ from instruction_core import render_instruction_core
16
14
 
17
15
 
18
16
  def main() -> None:
19
- print("# AGENTS.md")
20
- print()
21
- print(
22
- "This file describes the specialized AI agents bundled with ai-toolkit."
23
- )
24
- print(
25
- "It is auto-generated from `app/agents/*.md` frontmatter"
26
- " — do not edit manually."
27
- )
28
- print()
29
- print("To regenerate: `python3 scripts/generate_agents_md.py > AGENTS.md`")
30
- print()
31
- print("Compatible with: Claude Code, Codex, OpenCode, Gemini CLI.")
32
- print()
33
- print("---")
34
- print()
35
- print("## Usage")
36
- print()
37
- print("### Claude Code")
38
- print(
39
- "Agents are loaded automatically from `.claude/agents/`"
40
- " after running `install.sh`."
41
- )
42
- print("Invoke via the Agent tool:")
43
- print("```")
44
- print(
45
- 'Use subagent_type: "backend-specialist" to implement the API endpoint.'
46
- )
47
- print("```")
48
- print()
49
- print("### Codex / OpenCode")
50
- print("Reference agents by name in your prompts:")
51
- print("```")
52
- print("@backend-specialist implement the payment API")
53
- print("```")
54
- print()
55
- print("### Gemini CLI")
56
- print("Use agent descriptions as system context:")
57
- print("```")
58
- print(
59
- 'gemini --system "$(cat .claude/agents/backend-specialist.md)"'
60
- ' "implement the API"'
61
- )
62
- print("```")
63
- print()
64
- print("---")
65
- print()
66
- print("## Agents")
67
- print()
68
-
69
- for agent_file in sorted(agents_dir.glob("*.md")):
70
- if not agent_file.is_file():
71
- continue
72
-
73
- name = frontmatter_field(agent_file, "name")
74
- description = frontmatter_field(agent_file, "description")
75
- tools = frontmatter_field(agent_file, "tools")
76
-
77
- if not name:
78
- continue
79
-
80
- print(f"### `{name}`")
81
- print()
82
- if description:
83
- print(description)
84
- print()
85
- if tools:
86
- print(f"**Tools:** `{tools}`")
87
- print()
88
- print("---")
89
- print()
90
-
91
- # Codex CLI configuration block (agents, skills, guidelines)
92
- codex_script = Path(__file__).resolve().parent / "generate_codex.py"
93
- result = subprocess.run(
94
- ["python3", str(codex_script)],
95
- capture_output=True, text=True,
96
- )
97
- if result.returncode == 0 and result.stdout.strip():
98
- print(result.stdout.rstrip())
99
-
100
- # Note: custom rules are included via generate_codex.py output above
17
+ print(render_instruction_core(), end="")
101
18
 
102
19
 
103
20
  if __name__ == "__main__":
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """Generate AGENTS.md content for OpenAI Codex CLI.
2
+ """Generate the compact shared AGENTS.md policy for OpenAI Codex CLI.
3
3
 
4
- Adapts Claude-oriented skills to Codex-native delegation guidance so the full
5
- skill catalog can be surfaced in Codex installs.
4
+ Agent and skill discovery is installed separately in native directories, so
5
+ the always-on instruction file contains policy rather than duplicated catalogs.
6
6
 
7
7
  Usage: ./scripts/generate_codex.py > AGENTS.md
8
8
  """
@@ -13,108 +13,18 @@ import sys
13
13
  from pathlib import Path
14
14
 
15
15
  sys.path.insert(0, str(Path(__file__).resolve().parent))
16
- from codex_skill_adapter import codex_skill_description
17
- from dir_rules_shared import (
18
- rule_code_style,
19
- rule_output_mode,
20
- rule_security,
21
- rule_testing,
22
- )
23
16
  from emission import (
24
- agents_dir,
25
- skills_dir,
26
- generate_quality_standards,
27
- generate_workflow_guidelines,
28
17
  print_toolkit_end,
29
18
  print_toolkit_start,
30
19
  )
31
- from frontmatter import frontmatter_field
20
+ from instruction_core import render_instruction_core
32
21
  from paths import RULES_DIR
33
22
 
34
23
 
35
- def _emit_agents() -> str:
36
- """Emit agents as bullets."""
37
- lines: list[str] = []
38
- for agent_file in sorted(agents_dir.glob("*.md")):
39
- name = frontmatter_field(agent_file, "name")
40
- description = frontmatter_field(agent_file, "description")
41
- if not name or not description:
42
- continue
43
- lines.append(f"- **{name}**: {description}")
44
- return "\n".join(lines)
45
-
46
-
47
- def _emit_skills() -> str:
48
- """Emit skills as bullets, adapting Claude-native descriptions for Codex."""
49
- lines: list[str] = []
50
- for skill_dir in sorted(skills_dir.iterdir()):
51
- if skill_dir.name.startswith("_"):
52
- continue
53
- skill_file = skill_dir / "SKILL.md"
54
- if not skill_file.is_file():
55
- continue
56
- name = frontmatter_field(skill_file, "name")
57
- description = codex_skill_description(skill_file)
58
- if not name or not description:
59
- continue
60
- lines.append(f"- **{name}**: {description}")
61
- return "\n".join(lines)
62
-
63
-
64
- def _emit_coding_rules() -> str:
65
- """Inline the universal coding-rule bodies so Codex receives them.
66
-
67
- Codex reads project instructions only from AGENTS.md (and AGENTS.override.md);
68
- it does not read a ``.agents/rules/`` directory. Workflow + quality standards
69
- are already emitted above, so this adds code-style, testing, security, and
70
- output-mode under a single ``## Coding Rules`` section (H1 demoted to H3).
71
- """
72
- sections: list[str] = []
73
- for rule_fn in (rule_code_style, rule_testing, rule_security, rule_output_mode):
74
- body = rule_fn().rstrip()
75
- if body.startswith("# "):
76
- body = "### " + body[2:]
77
- sections.append(body)
78
- return "## Coding Rules\n\n" + "\n\n".join(sections)
79
-
80
-
81
24
  def main() -> None:
82
25
  print_toolkit_start()
83
-
84
- print("# AI Toolkit — Codex CLI Configuration")
85
- print()
86
- print(
87
- "Shared AI development toolkit with specialized agents,"
88
- " Codex-compatible skills, quality hooks, and a safety constitution."
89
- )
90
-
91
- # Agents (all agents are informational — safe to list)
92
- print()
93
- print("## Available Agents")
26
+ print(render_instruction_core().rstrip())
94
27
  print()
95
- print("Specialized agent personas — apply their expertise for relevant tasks:")
96
- print()
97
- print(_emit_agents())
98
-
99
- # Skills
100
- print()
101
- print("## Available Skills")
102
- print()
103
- print("Skills are invocable commands or auto-loaded knowledge sources:")
104
- print()
105
- print(_emit_skills())
106
-
107
- # Guidelines
108
- print()
109
- print(generate_quality_standards())
110
- print()
111
- print(generate_workflow_guidelines())
112
-
113
- # Universal coding rules — Codex reads instructions only from AGENTS.md,
114
- # so inline them here (previously emitted to the unread .agents/rules/).
115
- print()
116
- print(_emit_coding_rules())
117
-
118
28
  print_toolkit_end()
119
29
 
120
30
  # Registered custom rules from ~/.softspark/ai-toolkit/rules/.
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ """Generate native Codex custom-agent TOML files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import sys
9
+ import tempfile
10
+ import tomllib
11
+ from pathlib import Path
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
14
+ from emission import agents_dir
15
+ from frontmatter import frontmatter_field
16
+
17
+ AGENT_PREFIX = "ai-toolkit-"
18
+ MANAGED_MARKER = "# ai-toolkit-managed: codex-agent"
19
+ REQUIRED_FIELDS = {"name", "description", "developer_instructions"}
20
+
21
+
22
+ def _agent_body(agent_file: Path) -> str:
23
+ text = agent_file.read_text(encoding="utf-8")
24
+ lines = text.splitlines(keepends=True)
25
+ body = text
26
+ if lines and lines[0].rstrip("\r\n") == "---":
27
+ for index, line in enumerate(lines[1:], start=1):
28
+ if line.rstrip("\r\n") == "---":
29
+ body = "".join(lines[index + 1:])
30
+ break
31
+ return body.lstrip("\r\n").rstrip() + "\n"
32
+
33
+
34
+ def _toml_string(value: str) -> str:
35
+ return json.dumps(value, ensure_ascii=False)
36
+
37
+
38
+ def _render_agent(agent_file: Path) -> str:
39
+ name = frontmatter_field(agent_file, "name")
40
+ description = frontmatter_field(agent_file, "description")
41
+ body = _agent_body(agent_file)
42
+ return "\n".join(
43
+ [
44
+ "# Generated by ai-toolkit. Do not edit.",
45
+ MANAGED_MARKER,
46
+ f"name = {_toml_string(name)}",
47
+ f"description = {_toml_string(description)}",
48
+ f"developer_instructions = {_toml_string(body)}",
49
+ "",
50
+ ]
51
+ )
52
+
53
+
54
+ def _is_managed(path: Path) -> bool:
55
+ if path.is_symlink() or not path.is_file():
56
+ return False
57
+ try:
58
+ lines = path.read_text(encoding="utf-8").splitlines()
59
+ except (OSError, UnicodeError):
60
+ return False
61
+ return MANAGED_MARKER in lines[:3]
62
+
63
+
64
+ def _warn_preserved(path: Path, reason: str) -> None:
65
+ print(
66
+ f"Warning: preserving user Codex agent '{path}': {reason}",
67
+ file=sys.stderr,
68
+ )
69
+
70
+
71
+ def _unmanaged_agent_names(output_dir: Path) -> set[str]:
72
+ names: set[str] = set()
73
+ for path in sorted(output_dir.glob("*.toml")):
74
+ if path.is_symlink():
75
+ _warn_preserved(path, "path is a symlink")
76
+ continue
77
+ if _is_managed(path):
78
+ continue
79
+ try:
80
+ data = tomllib.loads(path.read_text(encoding="utf-8"))
81
+ except (OSError, UnicodeError, tomllib.TOMLDecodeError) as error:
82
+ _warn_preserved(path, f"cannot parse TOML ({error})")
83
+ continue
84
+ name = data.get("name")
85
+ if isinstance(name, str) and name.strip():
86
+ names.add(name)
87
+ return names
88
+
89
+
90
+ def _cleanup_stale(output_dir: Path, expected: set[str]) -> int:
91
+ removed = 0
92
+ for output in sorted(output_dir.glob(f"{AGENT_PREFIX}*.toml")):
93
+ if output.is_symlink():
94
+ continue
95
+ if output.name in expected or not _is_managed(output):
96
+ continue
97
+ output.unlink()
98
+ removed += 1
99
+ return removed
100
+
101
+
102
+ def _assert_safe_output_paths(base: Path, output_dir: Path) -> None:
103
+ if base.is_symlink():
104
+ raise RuntimeError(f"Refusing symlinked Codex config directory: {base}")
105
+ if output_dir.is_symlink():
106
+ raise RuntimeError(f"Refusing symlinked Codex agents directory: {output_dir}")
107
+
108
+
109
+ def _prepare_output_dir(base: Path) -> Path:
110
+ output_dir = base / "agents"
111
+ _assert_safe_output_paths(base, output_dir)
112
+ output_dir.mkdir(parents=True, exist_ok=True)
113
+ _assert_safe_output_paths(base, output_dir)
114
+ return output_dir
115
+
116
+
117
+ def _validate_agent_toml(rendered: str, source: Path) -> None:
118
+ data = tomllib.loads(rendered)
119
+ if set(data) != REQUIRED_FIELDS:
120
+ raise ValueError(f"Invalid Codex agent fields rendered from {source}")
121
+ if any(not isinstance(data[field], str) or not data[field] for field in REQUIRED_FIELDS):
122
+ raise ValueError(f"Empty Codex agent field rendered from {source}")
123
+
124
+
125
+ def _build_write_plan(
126
+ output_dir: Path,
127
+ unmanaged_names: set[str],
128
+ ) -> tuple[list[tuple[Path, str]], set[str]]:
129
+ plan: list[tuple[Path, str]] = []
130
+ expected: set[str] = set()
131
+
132
+ for agent_file in sorted(agents_dir.glob("*.md")):
133
+ name = frontmatter_field(agent_file, "name")
134
+ description = frontmatter_field(agent_file, "description")
135
+ if not name or not description:
136
+ continue
137
+ if name in unmanaged_names:
138
+ continue
139
+ filename = f"{AGENT_PREFIX}{name}.toml"
140
+ expected.add(filename)
141
+ output = output_dir / filename
142
+ if output.is_symlink():
143
+ _warn_preserved(output, "destination is a symlink")
144
+ continue
145
+ if output.exists() and not _is_managed(output):
146
+ continue
147
+ rendered = _render_agent(agent_file)
148
+ _validate_agent_toml(rendered, agent_file)
149
+ plan.append((output, rendered))
150
+ return plan, expected
151
+
152
+
153
+ def _discard_temp(path: Path, output_dir: Path) -> None:
154
+ if path.parent != output_dir:
155
+ return
156
+ try:
157
+ path.unlink(missing_ok=True)
158
+ except OSError:
159
+ pass
160
+
161
+
162
+ def _stage_agent(output_dir: Path, output: Path, rendered: str) -> Path:
163
+ fd, temp_name = tempfile.mkstemp(
164
+ dir=output_dir,
165
+ prefix=f".{output.name}.",
166
+ suffix=".tmp",
167
+ )
168
+ temp_path = Path(temp_name)
169
+ try:
170
+ handle = os.fdopen(fd, "w", encoding="utf-8")
171
+ fd = -1
172
+ with handle:
173
+ handle.write(rendered)
174
+ handle.flush()
175
+ os.fsync(handle.fileno())
176
+ _validate_agent_toml(temp_path.read_text(encoding="utf-8"), output)
177
+ return temp_path
178
+ except Exception:
179
+ if fd >= 0:
180
+ os.close(fd)
181
+ _discard_temp(temp_path, output_dir)
182
+ raise
183
+
184
+
185
+ def _stage_all(
186
+ output_dir: Path,
187
+ plan: list[tuple[Path, str]],
188
+ ) -> list[tuple[Path, Path]]:
189
+ staged: list[tuple[Path, Path]] = []
190
+ try:
191
+ for output, rendered in plan:
192
+ staged.append((_stage_agent(output_dir, output, rendered), output))
193
+ return staged
194
+ except Exception:
195
+ for temp_path, _ in staged:
196
+ _discard_temp(temp_path, output_dir)
197
+ raise
198
+
199
+
200
+ def _replace_all(
201
+ base: Path,
202
+ output_dir: Path,
203
+ staged: list[tuple[Path, Path]],
204
+ ) -> int:
205
+ written = 0
206
+ _assert_safe_output_paths(base, output_dir)
207
+ for temp_path, output in staged:
208
+ if output.is_symlink():
209
+ _warn_preserved(output, "destination became a symlink during generation")
210
+ continue
211
+ if output.exists() and not _is_managed(output):
212
+ _warn_preserved(output, "destination became user-owned during generation")
213
+ continue
214
+ os.replace(temp_path, output)
215
+ written += 1
216
+ return written
217
+
218
+
219
+ def generate(target_dir: Path, config_root: Path | None = None) -> tuple[int, int]:
220
+ base = config_root if config_root is not None else target_dir / ".codex"
221
+ output_dir = _prepare_output_dir(base)
222
+ unmanaged_names = _unmanaged_agent_names(output_dir)
223
+ plan, expected = _build_write_plan(output_dir, unmanaged_names)
224
+ staged = _stage_all(output_dir, plan)
225
+ try:
226
+ written = _replace_all(base, output_dir, staged)
227
+ _assert_safe_output_paths(base, output_dir)
228
+ finally:
229
+ for temp_path, _ in staged:
230
+ _discard_temp(temp_path, output_dir)
231
+ removed = _cleanup_stale(output_dir, expected)
232
+ return written, removed
233
+
234
+
235
+ def main() -> None:
236
+ target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
237
+ written, removed = generate(target)
238
+ print(f"Generated: .codex/agents/ ({written} agents, {removed} stale removed)")
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()