@softspark/ai-toolkit 4.24.0 → 4.25.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +35 -15
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/skills/hook-creator/SKILL.md +18 -4
  5. package/app/surface.json +4 -0
  6. package/benchmarks/ecosystem-doctor-snapshot.json +67 -19
  7. package/bin/ai-toolkit.js +27 -3
  8. package/kb/reference/architecture-overview.md +13 -5
  9. package/kb/reference/claude-ecosystem-expansion-foundations.md +30 -5
  10. package/kb/reference/cli-reference.md +27 -2
  11. package/kb/reference/codex-cli-compatibility.md +101 -11
  12. package/kb/reference/global-install-model.md +10 -8
  13. package/kb/reference/hooks-catalog.md +41 -5
  14. package/kb/reference/mcp-editor-compatibility.md +3 -3
  15. package/kb/reference/mcp-templates.md +3 -3
  16. package/kb/reference/opencode-compatibility.md +53 -5
  17. package/kb/reference/supported-tools-registry.md +31 -26
  18. package/llms-full.txt +312 -73
  19. package/manifest.json +1 -1
  20. package/package.json +6 -2
  21. package/scripts/antigravity_plugin.py +570 -0
  22. package/scripts/codex_plugin.py +764 -0
  23. package/scripts/ecosystem_tools.json +92 -17
  24. package/scripts/generate_antigravity.py +16 -14
  25. package/scripts/generate_antigravity_agents.py +255 -0
  26. package/scripts/generate_antigravity_hooks.py +344 -0
  27. package/scripts/generate_cline_hooks.py +391 -0
  28. package/scripts/generate_cline_rules.py +210 -43
  29. package/scripts/generate_cline_skills.py +65 -2
  30. package/scripts/generate_codex_hooks.py +70 -8
  31. package/scripts/generate_gemini_agents.py +197 -0
  32. package/scripts/generate_gemini_hooks.py +24 -4
  33. package/scripts/generate_opencode_skills.py +544 -0
  34. package/scripts/inject_hook_cli.py +4 -26
  35. package/scripts/install.py +11 -10
  36. package/scripts/install_steps/ai_tools.py +209 -34
  37. package/scripts/mcp_editors.py +9 -1
  38. package/scripts/plugin.py +21 -0
  39. package/scripts/plugin_schema.py +8 -7
  40. package/scripts/secure_fs.py +35 -0
  41. package/scripts/uninstall.py +162 -18
  42. package/scripts/validate.py +42 -12
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python3
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ # Copyright 2024-2026 Lukasz Krzemien (biuro@softspark.eu)
4
+ # Source: https://github.com/softspark/ai-toolkit
5
+
6
+ """Generate native Google Antigravity hooks and their portable adapter.
7
+
8
+ Project installs use ``.agents/hooks.json`` and a workspace-relative runtime.
9
+ Global installs use ``~/.gemini/config/hooks.json`` and an adjacent runtime.
10
+ Only the top-level ``ai-toolkit`` namespace is owned by this generator.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
21
+
22
+ from secure_fs import (
23
+ SecureDestination,
24
+ SecureTransaction,
25
+ lexical_absolute,
26
+ nearest_existing_root,
27
+ run_secure_transaction,
28
+ )
29
+
30
+
31
+ MANAGED_NAMESPACE = "ai-toolkit"
32
+ HOOK_EVENTS = (
33
+ "PreToolUse",
34
+ "PostToolUse",
35
+ "PreInvocation",
36
+ "PostInvocation",
37
+ "Stop",
38
+ )
39
+ TOOL_EVENTS = frozenset({"PreToolUse", "PostToolUse"})
40
+ HOOK_TIMEOUT_SECONDS = 5
41
+ RUNTIME_NAME = "ai-toolkit-antigravity-hook.py"
42
+ RUNTIME_MARKER = "# ai-toolkit-managed: antigravity-hook-runtime"
43
+ TOOL_MATCHER = (
44
+ "run_command|write_to_file|replace_file_content|"
45
+ "multi_replace_file_content|view_file"
46
+ )
47
+
48
+
49
+ RUNTIME_SOURCE = '''#!/usr/bin/env python3
50
+ # SPDX-License-Identifier: Apache-2.0
51
+ # ai-toolkit-managed: antigravity-hook-runtime
52
+ """Translate Antigravity hook input to bounded ai-toolkit safety decisions."""
53
+
54
+ from __future__ import annotations
55
+
56
+ import json
57
+ import re
58
+ import signal
59
+ import sys
60
+ from typing import Any
61
+
62
+ MAX_INPUT_BYTES = 1024 * 1024
63
+ MAX_RUNTIME_SECONDS = 4
64
+ EVENTS = {"PreToolUse", "PostToolUse", "PreInvocation", "PostInvocation", "Stop"}
65
+ DESTRUCTIVE = (
66
+ re.compile(r"(?:^|[;&|]\\s*)rm\\s+(?:-[^\\s]*r[^\\s]*f|-[^\\s]*f[^\\s]*r)\\b"),
67
+ re.compile(r"\\bgit\\s+reset\\s+--hard\\b"),
68
+ re.compile(r"\\bgit\\s+clean\\s+-[^\\s]*f"),
69
+ re.compile(r"\\b(?:mkfs|format)\\b", re.IGNORECASE),
70
+ re.compile(r"\\bDROP\\s+(?:DATABASE|SCHEMA|TABLE)\\b", re.IGNORECASE),
71
+ )
72
+
73
+
74
+ def _timeout(_signum: int, _frame: object) -> None:
75
+ raise TimeoutError("Antigravity hook runtime exceeded its deadline")
76
+
77
+
78
+ def _read_payload() -> dict[str, Any]:
79
+ raw = sys.stdin.buffer.read(MAX_INPUT_BYTES + 1)
80
+ if len(raw) > MAX_INPUT_BYTES:
81
+ raise ValueError("Antigravity hook payload exceeds 1 MiB")
82
+ if not raw.strip():
83
+ return {}
84
+ value = json.loads(raw)
85
+ if not isinstance(value, dict):
86
+ raise ValueError("Antigravity hook payload must be an object")
87
+ return value
88
+
89
+
90
+ def _tool(payload: dict[str, Any]) -> tuple[str, dict[str, Any]]:
91
+ tool_call = payload.get("toolCall", {})
92
+ if not isinstance(tool_call, dict):
93
+ return "", {}
94
+ name = tool_call.get("name", "")
95
+ args = tool_call.get("args", {})
96
+ return (
97
+ name if isinstance(name, str) else "",
98
+ args if isinstance(args, dict) else {},
99
+ )
100
+
101
+
102
+ def _pre_tool(payload: dict[str, Any]) -> dict[str, str]:
103
+ name, args = _tool(payload)
104
+ if name != "run_command":
105
+ return {"decision": "allow"}
106
+ command = args.get("CommandLine", "")
107
+ if not isinstance(command, str):
108
+ return {"decision": "ask"}
109
+ if any(pattern.search(command) for pattern in DESTRUCTIVE):
110
+ return {"decision": "deny"}
111
+ return {"decision": "allow"}
112
+
113
+
114
+ def respond(event: str, payload: dict[str, Any]) -> dict[str, Any]:
115
+ if event == "PreToolUse":
116
+ return _pre_tool(payload)
117
+ if event == "PostToolUse":
118
+ return {}
119
+ if event == "PreInvocation":
120
+ return {
121
+ "injectSteps": [
122
+ {
123
+ "ephemeralMessage": (
124
+ "Follow repository instructions and verify before "
125
+ "claiming completion."
126
+ )
127
+ }
128
+ ]
129
+ }
130
+ if event == "PostInvocation":
131
+ return {"injectSteps": [], "terminationBehavior": ""}
132
+ if event == "Stop":
133
+ execution_num = payload.get("executionNum")
134
+ is_first_execution = (
135
+ isinstance(execution_num, int)
136
+ and not isinstance(execution_num, bool)
137
+ and 0 <= execution_num <= 1
138
+ )
139
+ if payload.get("fullyIdle") is False and is_first_execution:
140
+ return {
141
+ "decision": "continue",
142
+ "reason": "The invocation is not fully idle; finish pending work.",
143
+ }
144
+ return {"decision": "stop"}
145
+ raise ValueError(f"unsupported Antigravity hook event: {event}")
146
+
147
+
148
+ def main() -> int:
149
+ if len(sys.argv) != 2 or sys.argv[1] not in EVENTS:
150
+ print("usage: ai-toolkit-antigravity-hook.py <event>", file=sys.stderr)
151
+ return 2
152
+ signal.signal(signal.SIGALRM, _timeout)
153
+ signal.alarm(MAX_RUNTIME_SECONDS)
154
+ try:
155
+ result = respond(sys.argv[1], _read_payload())
156
+ print(json.dumps(result, separators=(",", ":"), sort_keys=True))
157
+ return 0
158
+ except (ValueError, json.JSONDecodeError, TimeoutError) as error:
159
+ print(str(error), file=sys.stderr)
160
+ return 1
161
+ finally:
162
+ signal.alarm(0)
163
+
164
+
165
+ if __name__ == "__main__":
166
+ raise SystemExit(main())
167
+ '''
168
+
169
+
170
+ def _hook(command: str) -> dict[str, Any]:
171
+ return {"command": command, "timeout": HOOK_TIMEOUT_SECONDS}
172
+
173
+
174
+ def build_managed_hooks(command_prefix: str) -> dict[str, list[dict[str, Any]]]:
175
+ """Return the exact managed namespace using Antigravity's native schema."""
176
+ managed: dict[str, list[dict[str, Any]]] = {}
177
+ for event in HOOK_EVENTS:
178
+ command = f"{command_prefix} {event}"
179
+ if event in TOOL_EVENTS:
180
+ managed[event] = [{"matcher": TOOL_MATCHER, "hooks": [_hook(command)]}]
181
+ else:
182
+ managed[event] = [_hook(command)]
183
+ return managed
184
+
185
+
186
+ def build_document(command_prefix: str) -> dict[str, Any]:
187
+ return {MANAGED_NAMESPACE: build_managed_hooks(command_prefix)}
188
+
189
+
190
+ def validate_document(document: Any, command_prefix: str | None = None) -> None:
191
+ """Reject schema drift before any config is written or packaged."""
192
+ if not isinstance(document, dict):
193
+ raise ValueError("Antigravity hooks.json must contain an object")
194
+ managed = document.get(MANAGED_NAMESPACE)
195
+ if not isinstance(managed, dict) or set(managed) != set(HOOK_EVENTS):
196
+ raise ValueError("Antigravity managed hooks must contain the exact event set")
197
+ for event in HOOK_EVENTS:
198
+ entries = managed[event]
199
+ if not isinstance(entries, list) or not entries:
200
+ raise ValueError(f"Antigravity {event} must contain hook entries")
201
+ handlers: list[Any]
202
+ if event in TOOL_EVENTS:
203
+ handlers = []
204
+ for group in entries:
205
+ if not isinstance(group, dict) or set(group) != {"matcher", "hooks"}:
206
+ raise ValueError(f"Antigravity {event} has invalid matcher group")
207
+ if not isinstance(group["matcher"], str) or not group["matcher"]:
208
+ raise ValueError(f"Antigravity {event} matcher must be non-empty")
209
+ if not isinstance(group["hooks"], list) or not group["hooks"]:
210
+ raise ValueError(f"Antigravity {event} hooks must be non-empty")
211
+ handlers.extend(group["hooks"])
212
+ else:
213
+ handlers = entries
214
+ for handler in handlers:
215
+ if not isinstance(handler, dict) or set(handler) != {"command", "timeout"}:
216
+ raise ValueError(f"Antigravity {event} handler must be command-only")
217
+ if not isinstance(handler["command"], str) or not handler["command"]:
218
+ raise ValueError(f"Antigravity {event} command must be non-empty")
219
+ if not isinstance(handler["timeout"], int) or not 1 <= handler["timeout"] <= 10:
220
+ raise ValueError(f"Antigravity {event} timeout must be bounded")
221
+ expected_command = f"{command_prefix} {event}"
222
+ if command_prefix is not None and handler["command"] != expected_command:
223
+ raise ValueError(f"Antigravity {event} command was tampered")
224
+
225
+
226
+ def _load_existing(content: bytes | None, path: Path) -> dict[str, Any]:
227
+ if content is None:
228
+ return {}
229
+ try:
230
+ value = json.loads(content)
231
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
232
+ raise ValueError(f"Invalid Antigravity hooks JSON at {path}: {error}") from error
233
+ if not isinstance(value, dict):
234
+ raise ValueError(f"{path} must contain a JSON object")
235
+ return value
236
+
237
+
238
+ def _paths(target_dir: Path, global_install: bool) -> tuple[Path, Path, str]:
239
+ if global_install:
240
+ config_root = target_dir / ".gemini" / "config"
241
+ runtime = config_root / "hooks" / RUNTIME_NAME
242
+ command = f'python3 "$HOME/.gemini/config/hooks/{RUNTIME_NAME}"'
243
+ return config_root / "hooks.json", runtime, command
244
+ config_root = target_dir / ".agents"
245
+ runtime = config_root / "hooks" / RUNTIME_NAME
246
+ command = f"python3 .agents/hooks/{RUNTIME_NAME}"
247
+ return config_root / "hooks.json", runtime, command
248
+
249
+
250
+ def generate(target_dir: Path, *, global_install: bool = False) -> Path:
251
+ """Merge hooks and write their adjacent runtime transactionally."""
252
+ target = lexical_absolute(target_dir)
253
+ if target.is_symlink() or not target.is_dir():
254
+ raise RuntimeError(f"Unsafe Antigravity target directory: {target}")
255
+ hooks_path, runtime_path, command_prefix = _paths(target, global_install)
256
+ root = nearest_existing_root(target)
257
+ hooks_destination = SecureDestination(hooks_path, root, "Antigravity hooks.json")
258
+ runtime_destination = SecureDestination(
259
+ runtime_path, root, "Antigravity hook runtime"
260
+ )
261
+ destinations = [hooks_destination, runtime_destination]
262
+
263
+ managed = build_managed_hooks(command_prefix)
264
+ validate_document({MANAGED_NAMESPACE: managed}, command_prefix)
265
+
266
+ def apply(transaction: SecureTransaction) -> None:
267
+ existing = _load_existing(
268
+ transaction.initial_content(hooks_destination), hooks_path
269
+ )
270
+ existing[MANAGED_NAMESPACE] = managed
271
+ validate_document(existing, command_prefix)
272
+ current_runtime = transaction.initial_content(runtime_destination)
273
+ if (
274
+ current_runtime is not None
275
+ and RUNTIME_MARKER.encode() not in current_runtime[:256]
276
+ ):
277
+ raise RuntimeError(f"Refusing user-owned Antigravity runtime: {runtime_path}")
278
+ transaction.atomic_write(runtime_destination, RUNTIME_SOURCE.encode(), 0o755)
279
+ content = (
280
+ json.dumps(existing, indent=2, ensure_ascii=False, sort_keys=True)
281
+ + "\n"
282
+ ).encode()
283
+ transaction.atomic_write(hooks_destination, content, 0o600)
284
+
285
+ run_secure_transaction(destinations, apply)
286
+ return hooks_path
287
+
288
+
289
+ def generate_global(home_dir: Path) -> Path:
290
+ return generate(home_dir, global_install=True)
291
+
292
+
293
+ def cleanup(target_dir: Path, *, global_install: bool = False) -> None:
294
+ """Remove only ai-toolkit's namespace and managed adjacent runtime."""
295
+ target = lexical_absolute(target_dir)
296
+ hooks_path, runtime_path, _ = _paths(target, global_install)
297
+ if not hooks_path.is_file() or hooks_path.is_symlink():
298
+ return
299
+ root = nearest_existing_root(target)
300
+ hooks_destination = SecureDestination(hooks_path, root, "Antigravity hooks.json")
301
+ destinations = [hooks_destination]
302
+ runtime_destination: SecureDestination | None = None
303
+ if runtime_path.is_file() and not runtime_path.is_symlink():
304
+ runtime_destination = SecureDestination(
305
+ runtime_path, root, "Antigravity hook runtime"
306
+ )
307
+ destinations.append(runtime_destination)
308
+
309
+ def apply(transaction: SecureTransaction) -> None:
310
+ document = _load_existing(
311
+ transaction.initial_content(hooks_destination), hooks_path
312
+ )
313
+ if MANAGED_NAMESPACE not in document:
314
+ return
315
+ document.pop(MANAGED_NAMESPACE)
316
+ if document:
317
+ transaction.atomic_write(
318
+ hooks_destination,
319
+ (
320
+ json.dumps(
321
+ document, indent=2, ensure_ascii=False, sort_keys=True
322
+ )
323
+ + "\n"
324
+ ).encode(),
325
+ )
326
+ else:
327
+ transaction.unlink(hooks_destination)
328
+ if runtime_destination is not None:
329
+ runtime = transaction.initial_content(runtime_destination)
330
+ if runtime is not None and RUNTIME_MARKER.encode() in runtime[:256]:
331
+ transaction.unlink(runtime_destination)
332
+
333
+ run_secure_transaction(destinations, apply)
334
+
335
+
336
+ def main() -> None:
337
+ target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
338
+ path = generate(target)
339
+ relative_path = path.relative_to(lexical_absolute(target))
340
+ print(f"Generated: {relative_path} ({len(HOOK_EVENTS)} events)")
341
+
342
+
343
+ if __name__ == "__main__":
344
+ main()