@softspark/ai-toolkit 4.14.1 → 4.15.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/CHANGELOG.md +28 -0
- package/README.md +11 -10
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/CLAUDE.md.template +3 -0
- package/app/hooks/_search-capability.sh +3 -2
- package/app/hooks/stop-search-check.sh +2 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +73 -31
- package/kb/procedures/maintenance-sop.md +26 -13
- package/kb/procedures/release-verification-sop.md +41 -36
- package/kb/reference/architecture-overview.md +23 -7
- package/kb/reference/codex-cli-compatibility.md +96 -36
- package/kb/reference/extension-api.md +52 -9
- package/kb/reference/global-install-model.md +53 -21
- package/kb/reference/hooks-catalog.md +44 -8
- package/kb/reference/mcp-editor-compatibility.md +27 -6
- package/kb/reference/mcp-templates.md +12 -6
- package/kb/reference/opencode-compatibility.md +13 -7
- package/kb/reference/plugin-pack-conventions.md +7 -7
- package/kb/reference/skills-catalog.md +3 -3
- package/kb/reference/supported-tools-registry.md +19 -17
- package/kb/reference/windows-support.md +26 -3
- package/llms-full.txt +443 -180
- package/llms.txt +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/codex_skill_adapter.py +448 -198
- package/scripts/dir_rules_shared.py +2 -11
- package/scripts/ecosystem_tools.json +29 -8
- package/scripts/emission.py +5 -91
- package/scripts/generate_agents_md.py +4 -87
- package/scripts/generate_codex.py +5 -95
- package/scripts/generate_codex_agents.py +242 -0
- package/scripts/generate_codex_hooks.py +648 -55
- package/scripts/generate_codex_skills.py +15 -6
- package/scripts/generate_copilot.py +771 -74
- package/scripts/generate_copilot_hooks.py +606 -0
- package/scripts/generate_cursor_hooks.py +453 -121
- package/scripts/generate_opencode_commands.py +4 -6
- package/scripts/inject_hook_cli.py +770 -205
- package/scripts/injection.py +102 -23
- package/scripts/install_steps/ai_tools.py +123 -83
- package/scripts/instruction_core.py +95 -0
- package/scripts/mcp_editors.py +934 -80
- package/scripts/mcp_manager.py +46 -26
- package/scripts/plugin.py +291 -114
- package/scripts/secure_fs.py +538 -0
- package/scripts/uninstall.py +1279 -208
|
@@ -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()
|