@softspark/ai-toolkit 4.14.0 → 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 +35 -0
- package/README.md +11 -10
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/CLAUDE.md.template +3 -0
- package/app/agents/fact-checker.md +1 -1
- 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
package/scripts/mcp_manager.py
CHANGED
|
@@ -9,6 +9,7 @@ Usage:
|
|
|
9
9
|
mcp_manager.py install --editor <name[,..]> [--scope project|global] [--target <path>] [name..]
|
|
10
10
|
mcp_manager.py remove <name> Remove from .mcp.json
|
|
11
11
|
"""
|
|
12
|
+
|
|
12
13
|
from __future__ import annotations
|
|
13
14
|
|
|
14
15
|
import json
|
|
@@ -18,9 +19,11 @@ from pathlib import Path
|
|
|
18
19
|
from mcp_editors import (
|
|
19
20
|
editor_rows,
|
|
20
21
|
install_servers,
|
|
22
|
+
load_json_config,
|
|
21
23
|
load_project_mcp_servers,
|
|
22
24
|
remove_servers,
|
|
23
25
|
supported_editors,
|
|
26
|
+
write_json_config,
|
|
24
27
|
)
|
|
25
28
|
|
|
26
29
|
TOOLKIT_DIR = Path(__file__).resolve().parent.parent
|
|
@@ -32,12 +35,13 @@ MCP_CONFIG_NAME = ".mcp.json"
|
|
|
32
35
|
# Helpers
|
|
33
36
|
# ---------------------------------------------------------------------------
|
|
34
37
|
|
|
38
|
+
|
|
35
39
|
def load_template(name: str) -> dict:
|
|
36
40
|
"""Load a template JSON by name. Exits on error."""
|
|
37
41
|
template_path = TEMPLATES_DIR / f"{name}.json"
|
|
38
42
|
if not template_path.is_file():
|
|
39
43
|
print(f"Error: template '{name}' not found at {template_path}", file=sys.stderr)
|
|
40
|
-
print(
|
|
44
|
+
print("Run 'ai-toolkit mcp list' to see available templates.", file=sys.stderr)
|
|
41
45
|
sys.exit(1)
|
|
42
46
|
with open(template_path, encoding="utf-8") as f:
|
|
43
47
|
return json.load(f)
|
|
@@ -49,29 +53,29 @@ def available_templates() -> list[dict]:
|
|
|
49
53
|
for p in sorted(TEMPLATES_DIR.glob("*.json")):
|
|
50
54
|
with open(p, encoding="utf-8") as f:
|
|
51
55
|
data = json.load(f)
|
|
52
|
-
templates.append(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
templates.append(
|
|
57
|
+
{
|
|
58
|
+
"name": data.get("name", p.stem),
|
|
59
|
+
"description": data.get("description", ""),
|
|
60
|
+
"file": p.name,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
57
63
|
return templates
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
def load_mcp_config(target_dir: Path) -> dict:
|
|
61
67
|
"""Load existing .mcp.json or return empty structure."""
|
|
62
68
|
config_path = target_dir / MCP_CONFIG_NAME
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
data = load_json_config(config_path)
|
|
70
|
+
if config_path.exists():
|
|
71
|
+
return data
|
|
66
72
|
return {"mcpServers": {}}
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
def write_mcp_config(target_dir: Path, config: dict) -> None:
|
|
70
76
|
"""Write .mcp.json with pretty formatting."""
|
|
71
77
|
config_path = target_dir / MCP_CONFIG_NAME
|
|
72
|
-
|
|
73
|
-
json.dump(config, f, indent=2)
|
|
74
|
-
f.write("\n")
|
|
78
|
+
write_json_config(config_path, config)
|
|
75
79
|
print(f"Updated: {config_path}")
|
|
76
80
|
|
|
77
81
|
|
|
@@ -79,6 +83,7 @@ def write_mcp_config(target_dir: Path, config: dict) -> None:
|
|
|
79
83
|
# Commands
|
|
80
84
|
# ---------------------------------------------------------------------------
|
|
81
85
|
|
|
86
|
+
|
|
82
87
|
def cmd_list() -> None:
|
|
83
88
|
"""List all available MCP templates."""
|
|
84
89
|
templates = available_templates()
|
|
@@ -92,7 +97,7 @@ def cmd_list() -> None:
|
|
|
92
97
|
print(f"{t['name']:<25} {t['description']}")
|
|
93
98
|
print()
|
|
94
99
|
print(f"{len(templates)} templates available")
|
|
95
|
-
print(
|
|
100
|
+
print("Add with: ai-toolkit mcp add <name>")
|
|
96
101
|
|
|
97
102
|
|
|
98
103
|
def cmd_editors() -> None:
|
|
@@ -184,18 +189,24 @@ def cmd_install(
|
|
|
184
189
|
)
|
|
185
190
|
sys.exit(1)
|
|
186
191
|
|
|
187
|
-
eff_scope = scope or (
|
|
192
|
+
eff_scope = scope or (
|
|
193
|
+
"project" if target_dir is not None or not names else "global"
|
|
194
|
+
)
|
|
188
195
|
if eff_scope == "project":
|
|
189
196
|
project_dir = target_dir or Path.cwd()
|
|
190
197
|
if names:
|
|
191
|
-
cmd_add(names, project_dir)
|
|
192
198
|
servers = {}
|
|
193
199
|
for name in names:
|
|
194
200
|
servers.update(load_template(name).get("mcpServers", {}))
|
|
201
|
+
# `.mcp.json` is the canonical Claude project config. Include it
|
|
202
|
+
# in the same transaction as every requested native editor so a
|
|
203
|
+
# preflight or late write failure cannot leave partial state.
|
|
204
|
+
transaction_editors = list(dict.fromkeys(["claude", *editors]))
|
|
195
205
|
else:
|
|
196
206
|
servers = load_project_mcp_servers(project_dir)
|
|
207
|
+
transaction_editors = editors
|
|
197
208
|
updated = install_servers(
|
|
198
|
-
|
|
209
|
+
transaction_editors,
|
|
199
210
|
servers,
|
|
200
211
|
scope="project",
|
|
201
212
|
project_dir=project_dir,
|
|
@@ -218,6 +229,7 @@ def cmd_install(
|
|
|
218
229
|
# Track globally installed templates in state.json
|
|
219
230
|
if eff_scope == "global" and names:
|
|
220
231
|
from install_steps.install_state import record_mcp_template
|
|
232
|
+
|
|
221
233
|
for name in names:
|
|
222
234
|
record_mcp_template(name)
|
|
223
235
|
|
|
@@ -230,19 +242,17 @@ def cmd_install(
|
|
|
230
242
|
print(f"\n Note ({name}): {post_install}")
|
|
231
243
|
|
|
232
244
|
|
|
233
|
-
def cmd_remove(
|
|
245
|
+
def cmd_remove(
|
|
246
|
+
name: str, target_dir: Path | None, *, editors: list[str], scope: str | None
|
|
247
|
+
) -> None:
|
|
234
248
|
"""Remove an MCP server from .mcp.json."""
|
|
235
249
|
if editors:
|
|
236
250
|
eff_scope = scope or ("project" if target_dir else "global")
|
|
237
251
|
if eff_scope == "project":
|
|
238
252
|
project_dir = target_dir or Path.cwd()
|
|
239
|
-
|
|
240
|
-
if config_path.is_file():
|
|
241
|
-
config = load_mcp_config(project_dir)
|
|
242
|
-
config.get("mcpServers", {}).pop(name, None)
|
|
243
|
-
write_mcp_config(project_dir, config)
|
|
253
|
+
transaction_editors = list(dict.fromkeys(["claude", *editors]))
|
|
244
254
|
updated = remove_servers(
|
|
245
|
-
|
|
255
|
+
transaction_editors,
|
|
246
256
|
[name],
|
|
247
257
|
scope="project",
|
|
248
258
|
project_dir=project_dir,
|
|
@@ -251,6 +261,7 @@ def cmd_remove(name: str, target_dir: Path | None, *, editors: list[str], scope:
|
|
|
251
261
|
updated = remove_servers(editors, [name], scope="global")
|
|
252
262
|
# Untrack globally removed template from state.json
|
|
253
263
|
from install_steps.install_state import remove_mcp_template
|
|
264
|
+
|
|
254
265
|
remove_mcp_template(name)
|
|
255
266
|
for path in updated:
|
|
256
267
|
print(f"Updated: {path}")
|
|
@@ -267,7 +278,10 @@ def cmd_remove(name: str, target_dir: Path | None, *, editors: list[str], scope:
|
|
|
267
278
|
|
|
268
279
|
if name not in servers:
|
|
269
280
|
print(f"Error: server '{name}' not found in {config_path}.", file=sys.stderr)
|
|
270
|
-
print(
|
|
281
|
+
print(
|
|
282
|
+
f"Available servers: {', '.join(servers.keys()) or '(none)'}",
|
|
283
|
+
file=sys.stderr,
|
|
284
|
+
)
|
|
271
285
|
sys.exit(1)
|
|
272
286
|
|
|
273
287
|
del servers[name]
|
|
@@ -279,7 +293,10 @@ def cmd_remove(name: str, target_dir: Path | None, *, editors: list[str], scope:
|
|
|
279
293
|
# Argument parsing
|
|
280
294
|
# ---------------------------------------------------------------------------
|
|
281
295
|
|
|
282
|
-
|
|
296
|
+
|
|
297
|
+
def parse_options(
|
|
298
|
+
args: list[str],
|
|
299
|
+
) -> tuple[list[str], Path | None, list[str], str | None]:
|
|
283
300
|
"""Extract common MCP CLI options."""
|
|
284
301
|
target_dir: Path | None = None
|
|
285
302
|
editors: list[str] = []
|
|
@@ -288,7 +305,10 @@ def parse_options(args: list[str]) -> tuple[list[str], Path | None, list[str], s
|
|
|
288
305
|
i = 0
|
|
289
306
|
while i < len(args):
|
|
290
307
|
if args[i] == "--target" and i + 1 < len(args):
|
|
291
|
-
|
|
308
|
+
# Keep symlink provenance so native writers can reject redirected
|
|
309
|
+
# project roots instead of silently resolving and writing through
|
|
310
|
+
# them. `absolute()` normalizes the cwd without dereferencing.
|
|
311
|
+
target_dir = Path(args[i + 1]).expanduser().absolute()
|
|
292
312
|
i += 2
|
|
293
313
|
elif args[i] == "--editor" and i + 1 < len(args):
|
|
294
314
|
editors = [e.strip() for e in args[i + 1].split(",") if e.strip()]
|