@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.
- package/CHANGELOG.md +66 -0
- package/README.md +35 -15
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/hook-creator/SKILL.md +18 -4
- package/app/surface.json +4 -0
- package/benchmarks/ecosystem-doctor-snapshot.json +67 -19
- package/bin/ai-toolkit.js +27 -3
- package/kb/reference/architecture-overview.md +13 -5
- package/kb/reference/claude-ecosystem-expansion-foundations.md +30 -5
- package/kb/reference/cli-reference.md +27 -2
- package/kb/reference/codex-cli-compatibility.md +101 -11
- package/kb/reference/global-install-model.md +10 -8
- package/kb/reference/hooks-catalog.md +41 -5
- package/kb/reference/mcp-editor-compatibility.md +3 -3
- package/kb/reference/mcp-templates.md +3 -3
- package/kb/reference/opencode-compatibility.md +53 -5
- package/kb/reference/supported-tools-registry.md +31 -26
- package/llms-full.txt +312 -73
- package/manifest.json +1 -1
- package/package.json +6 -2
- package/scripts/antigravity_plugin.py +570 -0
- package/scripts/codex_plugin.py +764 -0
- package/scripts/ecosystem_tools.json +92 -17
- package/scripts/generate_antigravity.py +16 -14
- package/scripts/generate_antigravity_agents.py +255 -0
- package/scripts/generate_antigravity_hooks.py +344 -0
- package/scripts/generate_cline_hooks.py +391 -0
- package/scripts/generate_cline_rules.py +210 -43
- package/scripts/generate_cline_skills.py +65 -2
- package/scripts/generate_codex_hooks.py +70 -8
- package/scripts/generate_gemini_agents.py +197 -0
- package/scripts/generate_gemini_hooks.py +24 -4
- package/scripts/generate_opencode_skills.py +544 -0
- package/scripts/inject_hook_cli.py +4 -26
- package/scripts/install.py +11 -10
- package/scripts/install_steps/ai_tools.py +209 -34
- package/scripts/mcp_editors.py +9 -1
- package/scripts/plugin.py +21 -0
- package/scripts/plugin_schema.py +8 -7
- package/scripts/secure_fs.py +35 -0
- package/scripts/uninstall.py +162 -18
- package/scripts/validate.py +42 -12
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"description": "Authoritative registry of tools ai-toolkit integrates with. Consumed by scripts/ecosystem_doctor.py to detect upstream doc/version drift.",
|
|
4
|
-
"last_updated": "2026-
|
|
4
|
+
"last_updated": "2026-08-19",
|
|
5
5
|
"tools": [
|
|
6
6
|
{
|
|
7
7
|
"id": "claude-code",
|
|
@@ -20,9 +20,23 @@
|
|
|
20
20
|
".claude/rules/*.md",
|
|
21
21
|
".claude/agents/*.md",
|
|
22
22
|
".claude/skills/*/SKILL.md",
|
|
23
|
-
"~/.claude/themes/*.json"
|
|
23
|
+
"~/.claude/themes/*.json",
|
|
24
|
+
".claude-plugin/plugin.json",
|
|
25
|
+
"skills/*/SKILL.md",
|
|
26
|
+
"commands/*.md",
|
|
27
|
+
"agents/*.md",
|
|
28
|
+
"workflows/",
|
|
29
|
+
"output-styles/",
|
|
30
|
+
"themes/",
|
|
31
|
+
"hooks/hooks.json",
|
|
32
|
+
".mcp.json",
|
|
33
|
+
".lsp.json",
|
|
34
|
+
"monitors/monitors.json",
|
|
35
|
+
"bin/*",
|
|
36
|
+
"settings.json"
|
|
24
37
|
],
|
|
25
38
|
"our_generators": [],
|
|
39
|
+
"status_note": "Claude Code auto-discovers native plugin skills, commands, agents, workflows, output styles, themes, hooks, MCP servers, LSP servers, monitors, executables, and root settings. ai-toolkit's primary install emits its existing rules, skills, agents, and hooks; plugin workflows, output styles, themes, monitors, LSP servers, channels, and root plugin settings are recognized class C surfaces that ai-toolkit does not generate. Themes and monitors are experimental, and plugin settings support only agent and subagentStatusLine.",
|
|
26
40
|
"capability_markers": [
|
|
27
41
|
"PreToolUse",
|
|
28
42
|
"PostToolUse",
|
|
@@ -52,8 +66,18 @@
|
|
|
52
66
|
"CwdChanged",
|
|
53
67
|
"FileChanged",
|
|
54
68
|
"ConfigChange",
|
|
69
|
+
"DirectoryAdded",
|
|
55
70
|
"Setup",
|
|
56
71
|
"InstructionsLoaded",
|
|
72
|
+
"plugins",
|
|
73
|
+
"workflows",
|
|
74
|
+
"outputStyles",
|
|
75
|
+
"themes",
|
|
76
|
+
"LSP servers",
|
|
77
|
+
"monitors",
|
|
78
|
+
"channels",
|
|
79
|
+
"userConfig",
|
|
80
|
+
"settings.json",
|
|
57
81
|
"slash command",
|
|
58
82
|
"MCP server",
|
|
59
83
|
"sub-agent",
|
|
@@ -265,16 +289,20 @@
|
|
|
265
289
|
"~/.gemini/settings.json",
|
|
266
290
|
".gemini/commands/*.toml",
|
|
267
291
|
".gemini/skills/*/SKILL.md",
|
|
292
|
+
".gemini/agents/*.md",
|
|
268
293
|
".agents/skills/*/SKILL.md",
|
|
269
294
|
"~/.gemini/skills/*/SKILL.md",
|
|
295
|
+
"~/.gemini/agents/*.md",
|
|
270
296
|
"~/.agents/skills/*/SKILL.md",
|
|
271
|
-
"
|
|
297
|
+
"<extension-package>/gemini-extension.json",
|
|
298
|
+
"~/.gemini/extensions/<name>/gemini-extension.json"
|
|
272
299
|
],
|
|
273
300
|
"our_generators": [
|
|
274
301
|
"scripts/generate_gemini.py",
|
|
275
302
|
"scripts/generate_gemini_hooks.py",
|
|
276
303
|
"scripts/generate_gemini_commands.py",
|
|
277
|
-
"scripts/generate_gemini_skills.py"
|
|
304
|
+
"scripts/generate_gemini_skills.py",
|
|
305
|
+
"scripts/generate_gemini_agents.py"
|
|
278
306
|
],
|
|
279
307
|
"capability_markers": [
|
|
280
308
|
"GEMINI.md",
|
|
@@ -295,9 +323,10 @@
|
|
|
295
323
|
"SKILL.md",
|
|
296
324
|
"activate_skill",
|
|
297
325
|
"custom commands",
|
|
326
|
+
".gemini/agents/*.md",
|
|
298
327
|
"gemini-extension.json"
|
|
299
328
|
],
|
|
300
|
-
"status_note": "Gemini CLI drops free + paid (AI Pro/Ultra) tiers on 2026-06-18 in favor of the new Antigravity CLI (we ship generate_antigravity.py); enterprise Gemini Code Assist Standard/Enterprise keeps full Gemini CLI support.
|
|
329
|
+
"status_note": "Gemini CLI drops free + paid (AI Pro/Ultra) tiers on 2026-06-18 in favor of the new Antigravity CLI (we ship generate_antigravity.py); enterprise Gemini Code Assist Standard/Enterprise keeps full Gemini CLI support. Profile full now emits project and user-tier native agents. Gemini extension export remains class C / not adopted; extension manifests live at the package root and installed extensions live under ~/.gemini/extensions/<name>/.",
|
|
301
330
|
"version_probe": {
|
|
302
331
|
"kind": "command",
|
|
303
332
|
"command": "gemini --version"
|
|
@@ -319,13 +348,16 @@
|
|
|
319
348
|
".clinerules/hooks/",
|
|
320
349
|
".cline/rules/*.md",
|
|
321
350
|
".cline/hooks/",
|
|
351
|
+
".cline/agents/",
|
|
322
352
|
".cline/plugins/",
|
|
323
353
|
".cline/skills/*/SKILL.md",
|
|
324
354
|
"~/.cline/rules/",
|
|
325
355
|
"~/.cline/hooks/",
|
|
356
|
+
"~/.cline/agents/",
|
|
326
357
|
"~/.cline/plugins/",
|
|
327
358
|
"~/.cline/skills/*/SKILL.md",
|
|
328
|
-
"~/Documents/Cline/Rules/
|
|
359
|
+
"~/Documents/Cline/Rules/",
|
|
360
|
+
"~/Documents/Cline/Hooks/",
|
|
329
361
|
"~/.cline/data/settings/cline_mcp_settings.json",
|
|
330
362
|
".claude/skills/*/SKILL.md",
|
|
331
363
|
"AGENTS.md",
|
|
@@ -334,7 +366,8 @@
|
|
|
334
366
|
"our_generators": [
|
|
335
367
|
"scripts/generate_cline.py",
|
|
336
368
|
"scripts/generate_cline_rules.py",
|
|
337
|
-
"scripts/generate_cline_skills.py"
|
|
369
|
+
"scripts/generate_cline_skills.py",
|
|
370
|
+
"scripts/generate_cline_hooks.py"
|
|
338
371
|
],
|
|
339
372
|
"capability_markers": [
|
|
340
373
|
"clinerules",
|
|
@@ -501,19 +534,33 @@
|
|
|
501
534
|
".agents/rules/*.md",
|
|
502
535
|
".agents/workflows/*.md",
|
|
503
536
|
".agents/skills/*/SKILL.md",
|
|
537
|
+
".agents/agents/ai-toolkit-*/agent.md",
|
|
504
538
|
".agents/hooks.json",
|
|
539
|
+
".agents/hooks/ai-toolkit-antigravity-hook.py",
|
|
540
|
+
".agents/plugins/*/plugin.json",
|
|
505
541
|
".agents/mcp_config.json",
|
|
542
|
+
"~/.gemini/config/skills/*/SKILL.md",
|
|
543
|
+
"~/.gemini/config/agents/ai-toolkit-*/agent.md",
|
|
544
|
+
"~/.gemini/config/hooks.json",
|
|
545
|
+
"~/.gemini/config/hooks/ai-toolkit-antigravity-hook.py",
|
|
546
|
+
"~/.gemini/config/plugins/*/plugin.json",
|
|
506
547
|
"~/.gemini/config/mcp_config.json",
|
|
507
548
|
".agent/rules/*.md",
|
|
508
549
|
".agent/workflows/*.md",
|
|
509
550
|
".agent/skills/*/SKILL.md",
|
|
551
|
+
"~/.gemini/antigravity-cli/skills/*/SKILL.md",
|
|
552
|
+
"~/.gemini/antigravity-cli/plugins/*/plugin.json",
|
|
510
553
|
"AGENTS.md",
|
|
511
554
|
"GEMINI.md"
|
|
512
555
|
],
|
|
513
556
|
"our_generators": [
|
|
514
|
-
"scripts/generate_antigravity.py"
|
|
557
|
+
"scripts/generate_antigravity.py",
|
|
558
|
+
"scripts/generate_antigravity_hooks.py",
|
|
559
|
+
"scripts/generate_antigravity_agents.py",
|
|
560
|
+
"scripts/antigravity_plugin.py",
|
|
561
|
+
"scripts/mcp_editors.py"
|
|
515
562
|
],
|
|
516
|
-
"status_note": "Antigravity CLI (
|
|
563
|
+
"status_note": "Antigravity 2.0 v2.8.1, Antigravity CLI v1.1.14, IDE v2.5.5 (verified 2026-08-19). Project skills use .agents/skills/; only .agent/skills/ is legacy. Global skills use ~/.gemini/antigravity-cli/skills/ for CLI and ~/.gemini/config/skills/ for the IDE/shared product. Native hooks use .agents/hooks.json or ~/.gemini/config/hooks.json with exact events PreToolUse, PostToolUse, PreInvocation, PostInvocation, and Stop plus a self-contained adjacent adapter. Full-profile installs add native subagents. antigravity-plugin exports an opt-in deterministic package for .agents/plugins/, ~/.gemini/antigravity-cli/plugins/, or ~/.gemini/config/plugins/. Native MCP output consumes portable transport metadata and uses serverUrl; legacy url input is normalized and httpUrl is rejected. Unrelated config data is preserved transactionally.",
|
|
517
564
|
"capability_markers": [
|
|
518
565
|
"agent manager",
|
|
519
566
|
"artifacts",
|
|
@@ -522,9 +569,20 @@
|
|
|
522
569
|
"rules",
|
|
523
570
|
"skills",
|
|
524
571
|
"hooks",
|
|
572
|
+
"PreToolUse",
|
|
573
|
+
"PostToolUse",
|
|
574
|
+
"PreInvocation",
|
|
575
|
+
"PostInvocation",
|
|
576
|
+
"Stop",
|
|
577
|
+
"subagents",
|
|
578
|
+
"plugins",
|
|
525
579
|
"AGENTS.md",
|
|
526
580
|
"GEMINI.md",
|
|
527
|
-
"agent permissions"
|
|
581
|
+
"agent permissions",
|
|
582
|
+
"global skills",
|
|
583
|
+
"global hooks",
|
|
584
|
+
"global agents",
|
|
585
|
+
"serverUrl"
|
|
528
586
|
],
|
|
529
587
|
"version_probe": null
|
|
530
588
|
},
|
|
@@ -547,13 +605,18 @@
|
|
|
547
605
|
".codex/hooks.json",
|
|
548
606
|
"~/.codex/hooks.json",
|
|
549
607
|
".codex/config.toml",
|
|
550
|
-
"~/.codex/config.toml"
|
|
608
|
+
"~/.codex/config.toml",
|
|
609
|
+
".codex-plugin/plugin.json",
|
|
610
|
+
"hooks/hooks.json",
|
|
611
|
+
"skills/*/SKILL.md",
|
|
612
|
+
".agents/plugins/marketplace.json"
|
|
551
613
|
],
|
|
552
614
|
"our_generators": [
|
|
553
615
|
"scripts/generate_codex.py",
|
|
554
616
|
"scripts/generate_codex_agents.py",
|
|
555
617
|
"scripts/generate_codex_hooks.py",
|
|
556
|
-
"scripts/generate_codex_skills.py"
|
|
618
|
+
"scripts/generate_codex_skills.py",
|
|
619
|
+
"scripts/codex_plugin.py"
|
|
557
620
|
],
|
|
558
621
|
"capability_markers": [
|
|
559
622
|
"AGENTS.md",
|
|
@@ -564,6 +627,7 @@
|
|
|
564
627
|
"hook event: PreToolUse",
|
|
565
628
|
"hook event: PostToolUse",
|
|
566
629
|
"hook event: SessionStart",
|
|
630
|
+
"hook event: SessionEnd",
|
|
567
631
|
"hook event: UserPromptSubmit",
|
|
568
632
|
"hook event: Stop",
|
|
569
633
|
"hook event: PermissionRequest",
|
|
@@ -575,7 +639,12 @@
|
|
|
575
639
|
"hook handler: prompt",
|
|
576
640
|
"hook handler: agent",
|
|
577
641
|
".agents/skills",
|
|
578
|
-
".codex/agents"
|
|
642
|
+
".codex/agents",
|
|
643
|
+
".codex-plugin/plugin.json",
|
|
644
|
+
"hooks/hooks.json",
|
|
645
|
+
"PLUGIN_ROOT",
|
|
646
|
+
"/plugins",
|
|
647
|
+
"plugin marketplace"
|
|
579
648
|
],
|
|
580
649
|
"version_probe": {
|
|
581
650
|
"kind": "command",
|
|
@@ -593,6 +662,7 @@
|
|
|
593
662
|
},
|
|
594
663
|
"config_paths": [
|
|
595
664
|
"opencode.json",
|
|
665
|
+
"opencode.jsonc",
|
|
596
666
|
".opencode/agents/*.md",
|
|
597
667
|
".opencode/commands/*.md",
|
|
598
668
|
".opencode/plugins/*",
|
|
@@ -602,15 +672,18 @@
|
|
|
602
672
|
".agents/skills/*/SKILL.md",
|
|
603
673
|
"~/.config/opencode/skills/*/SKILL.md",
|
|
604
674
|
"~/.claude/skills/*/SKILL.md",
|
|
605
|
-
"~/.agents/skills/*/SKILL.md"
|
|
675
|
+
"~/.agents/skills/*/SKILL.md",
|
|
676
|
+
"skills/*.md",
|
|
677
|
+
"skills/**/SKILL.md"
|
|
606
678
|
],
|
|
607
|
-
"status_note": "
|
|
679
|
+
"status_note": "OpenCode's v2 plugin API is beta. Root plugin packages can expose flat skills/*.md and nested skills/**/SKILL.md entries, and config can add local paths or HTTP URLs through the skills array. No migration: the stable JS plugin generator remains unchanged until v2 stabilizes. Note sst/opencode redirects to anomalyco/opencode.",
|
|
608
680
|
"our_generators": [
|
|
609
681
|
"scripts/generate_opencode.py",
|
|
610
682
|
"scripts/generate_opencode_agents.py",
|
|
611
683
|
"scripts/generate_opencode_commands.py",
|
|
612
684
|
"scripts/generate_opencode_json.py",
|
|
613
|
-
"scripts/generate_opencode_plugin.py"
|
|
685
|
+
"scripts/generate_opencode_plugin.py",
|
|
686
|
+
"scripts/generate_opencode_skills.py"
|
|
614
687
|
],
|
|
615
688
|
"capability_markers": [
|
|
616
689
|
"opencode.json",
|
|
@@ -632,7 +705,9 @@
|
|
|
632
705
|
"plugin event: tool.execute.after",
|
|
633
706
|
"plugin event: permission.asked",
|
|
634
707
|
"plugin event: command.executed",
|
|
635
|
-
"plugin event: message.updated"
|
|
708
|
+
"plugin event: message.updated",
|
|
709
|
+
"plugin event: message.part.updated",
|
|
710
|
+
"skills array (local paths + HTTP URLs)"
|
|
636
711
|
],
|
|
637
712
|
"version_probe": {
|
|
638
713
|
"kind": "command",
|
|
@@ -15,10 +15,10 @@ Antigravity reads rules from:
|
|
|
15
15
|
* ``.agents/rules/*.md`` — per-category rule files (this generator)
|
|
16
16
|
* ``.agents/workflows/*.md`` — workflow templates (this generator)
|
|
17
17
|
|
|
18
|
-
Antigravity also supports the Agent Skills standard. The
|
|
19
|
-
``.
|
|
20
|
-
|
|
21
|
-
dual-emitted
|
|
18
|
+
Antigravity also supports the Agent Skills standard. The canonical project
|
|
19
|
+
surface is ``.agents/skills/<skill-name>/SKILL.md``. The older singular
|
|
20
|
+
``.agent/skills/`` path remains a compatibility output only, so the skill
|
|
21
|
+
pointer is dual-emitted. We do not duplicate our full skill catalogue; instead we
|
|
22
22
|
emit a single pointer skill that teaches Antigravity to look up the real
|
|
23
23
|
catalogue in ``~/.softspark/ai-toolkit/app/skills/`` (global install) or
|
|
24
24
|
``.claude/skills/`` (local install).
|
|
@@ -27,7 +27,8 @@ Usage:
|
|
|
27
27
|
python3 scripts/generate_antigravity.py [target-dir]
|
|
28
28
|
|
|
29
29
|
Writes rules/workflows to target-dir/.agents/{rules,workflows}/ and the skill
|
|
30
|
-
pointer to target-dir/.
|
|
30
|
+
pointer to target-dir/.agents/skills/ (canonical) + target-dir/.agent/skills/
|
|
31
|
+
(legacy compatibility).
|
|
31
32
|
"""
|
|
32
33
|
from __future__ import annotations
|
|
33
34
|
|
|
@@ -88,11 +89,11 @@ def _pointer_skill_md() -> str:
|
|
|
88
89
|
def _write_skill_pointer(target_dir: Path) -> None:
|
|
89
90
|
"""Write the pointer SKILL.md for both the IDE and the CLI surface.
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
``.agents/skills/`` is canonical. ``.agent/skills/`` is retained only for
|
|
93
|
+
older Antigravity installations.
|
|
93
94
|
"""
|
|
94
95
|
content = _pointer_skill_md()
|
|
95
|
-
for tree in (".
|
|
96
|
+
for tree in (".agents", ".agent"):
|
|
96
97
|
skill_dir = target_dir / tree / "skills" / POINTER_SKILL_NAME
|
|
97
98
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
98
99
|
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
|
|
@@ -105,10 +106,10 @@ def _write_skill_pointer(target_dir: Path) -> None:
|
|
|
105
106
|
|
|
106
107
|
def generate_global(target_dir: Path) -> None:
|
|
107
108
|
"""Write the skill pointer to Antigravity's documented HOME-scoped skill
|
|
108
|
-
dirs. ``~/.gemini/
|
|
109
|
-
|
|
110
|
-
Antigravity RULES have no documented global file surface, so only the
|
|
111
|
-
|
|
109
|
+
dirs. ``~/.gemini/antigravity-cli/skills/`` is canonical for the CLI;
|
|
110
|
+
``~/.gemini/config/skills/`` is canonical for the IDE/shared product.
|
|
111
|
+
Antigravity RULES have no documented global file surface, so only the
|
|
112
|
+
product-specific skill pointers are emitted globally (rules stay local).
|
|
112
113
|
"""
|
|
113
114
|
content = _pointer_skill_md()
|
|
114
115
|
for rel in (".gemini/config/skills", ".gemini/antigravity-cli/skills"):
|
|
@@ -124,8 +125,9 @@ def generate(target_dir: Path, *,
|
|
|
124
125
|
emit_skill_pointer: bool = True) -> None:
|
|
125
126
|
"""Write ``.agents/{rules,workflows}/`` and the dual skill pointers.
|
|
126
127
|
|
|
127
|
-
``emit_skill_pointer`` controls whether the pointer SKILL.md is written
|
|
128
|
-
|
|
128
|
+
``emit_skill_pointer`` controls whether the pointer SKILL.md is written.
|
|
129
|
+
``.agents/skills/`` is the canonical workspace surface, while
|
|
130
|
+
``.agent/skills/`` is a legacy compatibility copy. Set this option to
|
|
129
131
|
``False`` if you manage those directories yourself.
|
|
130
132
|
"""
|
|
131
133
|
rules = dict(STANDARD_RULES)
|
|
@@ -0,0 +1,255 @@
|
|
|
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 Antigravity subagents under ``.agents/agents``."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
|
+
|
|
16
|
+
from emission import agents_dir
|
|
17
|
+
from frontmatter import frontmatter_field
|
|
18
|
+
from secure_fs import (
|
|
19
|
+
SecureDestination,
|
|
20
|
+
SecureTransaction,
|
|
21
|
+
lexical_absolute,
|
|
22
|
+
nearest_existing_root,
|
|
23
|
+
run_secure_transaction,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
AGENT_PREFIX = "ai-toolkit-"
|
|
28
|
+
MANAGED_MARKER = "<!-- ai-toolkit-managed: antigravity-agent -->"
|
|
29
|
+
SAFE_NAME = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
30
|
+
TOOL_MAPPING: dict[str, tuple[str, ...]] = {
|
|
31
|
+
"Read": ("view_file",),
|
|
32
|
+
"Write": ("write_to_file",),
|
|
33
|
+
"Edit": ("replace_file_content", "multi_replace_file_content"),
|
|
34
|
+
"Bash": ("run_command",),
|
|
35
|
+
"Grep": ("grep_search",),
|
|
36
|
+
"Glob": ("find_by_name", "list_dir"),
|
|
37
|
+
"Agent": ("invoke_subagent",),
|
|
38
|
+
"TeamCreate": ("manage_subagents",),
|
|
39
|
+
"TeamDelete": ("manage_subagents",),
|
|
40
|
+
"SendMessage": ("send_message",),
|
|
41
|
+
"TaskCreate": ("manage_task",),
|
|
42
|
+
"TaskList": ("manage_task",),
|
|
43
|
+
"TaskUpdate": ("manage_task",),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _source_tools(source: Path) -> list[str]:
|
|
48
|
+
raw = frontmatter_field(source, "tools")
|
|
49
|
+
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _mapped_tools(source: Path) -> list[str]:
|
|
53
|
+
mapped: list[str] = []
|
|
54
|
+
unknown: list[str] = []
|
|
55
|
+
for tool in _source_tools(source):
|
|
56
|
+
native = TOOL_MAPPING.get(tool)
|
|
57
|
+
if native is None:
|
|
58
|
+
unknown.append(tool)
|
|
59
|
+
continue
|
|
60
|
+
for name in native:
|
|
61
|
+
if name not in mapped:
|
|
62
|
+
mapped.append(name)
|
|
63
|
+
if unknown:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Unknown Antigravity source tool(s) in {source}: "
|
|
66
|
+
+ ", ".join(sorted(unknown))
|
|
67
|
+
)
|
|
68
|
+
return mapped
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _body(source: Path) -> str:
|
|
72
|
+
text = source.read_text(encoding="utf-8")
|
|
73
|
+
if not text.startswith("---\n"):
|
|
74
|
+
return text.strip()
|
|
75
|
+
parts = text.split("---", 2)
|
|
76
|
+
return (parts[2] if len(parts) == 3 else text).strip()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _quote(value: str) -> str:
|
|
80
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_agent(source: Path) -> tuple[str, str]:
|
|
84
|
+
"""Render and return ``(name, agent.md)`` after strict source validation."""
|
|
85
|
+
if source.is_symlink() or not source.is_file():
|
|
86
|
+
raise RuntimeError(f"Unsafe Antigravity agent source: {source}")
|
|
87
|
+
name = frontmatter_field(source, "name")
|
|
88
|
+
description = frontmatter_field(source, "description")
|
|
89
|
+
if not SAFE_NAME.fullmatch(name):
|
|
90
|
+
raise ValueError(f"Invalid Antigravity agent name in {source}: {name!r}")
|
|
91
|
+
if not description:
|
|
92
|
+
raise ValueError(f"Missing Antigravity agent description in {source}")
|
|
93
|
+
tools = _mapped_tools(source)
|
|
94
|
+
lines = [
|
|
95
|
+
"---",
|
|
96
|
+
f"name: {name}",
|
|
97
|
+
f"description: {_quote(description)}",
|
|
98
|
+
"mainAgent: false",
|
|
99
|
+
"subagent: true",
|
|
100
|
+
"model: inherit",
|
|
101
|
+
"commandExecutionPolicy: sandbox",
|
|
102
|
+
"tools:",
|
|
103
|
+
]
|
|
104
|
+
lines.extend(f" - {tool}" for tool in tools)
|
|
105
|
+
lines.extend(["---", "", MANAGED_MARKER, "", _body(source), ""])
|
|
106
|
+
return name, "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _is_managed(content: bytes | None) -> bool:
|
|
110
|
+
return content is not None and MANAGED_MARKER.encode() in content[:2048]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _source_plan(source_dir: Path) -> list[tuple[str, str]]:
|
|
114
|
+
if source_dir.is_symlink() or not source_dir.is_dir():
|
|
115
|
+
raise RuntimeError(f"Unsafe Antigravity agents source directory: {source_dir}")
|
|
116
|
+
plan: list[tuple[str, str]] = []
|
|
117
|
+
seen: set[str] = set()
|
|
118
|
+
for source in sorted(source_dir.glob("*.md")):
|
|
119
|
+
name, rendered = render_agent(source)
|
|
120
|
+
if name in seen:
|
|
121
|
+
raise ValueError(f"Duplicate Antigravity agent name: {name}")
|
|
122
|
+
seen.add(name)
|
|
123
|
+
plan.append((name, rendered))
|
|
124
|
+
return plan
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _stale_files(output_dir: Path, expected: set[str]) -> list[Path]:
|
|
128
|
+
if not output_dir.exists():
|
|
129
|
+
return []
|
|
130
|
+
if output_dir.is_symlink() or not output_dir.is_dir():
|
|
131
|
+
raise RuntimeError(f"Unsafe Antigravity agents directory: {output_dir}")
|
|
132
|
+
stale: list[Path] = []
|
|
133
|
+
for child in sorted(output_dir.iterdir()):
|
|
134
|
+
if child.name in expected or not child.name.startswith(AGENT_PREFIX):
|
|
135
|
+
continue
|
|
136
|
+
if child.is_symlink() or not child.is_dir():
|
|
137
|
+
continue
|
|
138
|
+
agent_file = child / "agent.md"
|
|
139
|
+
if agent_file.is_symlink() or not agent_file.is_file():
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
content = agent_file.read_bytes()
|
|
143
|
+
except OSError:
|
|
144
|
+
continue
|
|
145
|
+
if _is_managed(content):
|
|
146
|
+
stale.append(agent_file)
|
|
147
|
+
return stale
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def generate(
|
|
151
|
+
target_dir: Path,
|
|
152
|
+
*,
|
|
153
|
+
source_dir: Path | None = None,
|
|
154
|
+
config_root: Path | None = None,
|
|
155
|
+
) -> tuple[int, int]:
|
|
156
|
+
"""Generate agents atomically and remove only stale managed definitions."""
|
|
157
|
+
source_root = lexical_absolute(source_dir or agents_dir)
|
|
158
|
+
plan = _source_plan(source_root) # Preflight all source tools before mutation.
|
|
159
|
+
target = lexical_absolute(target_dir)
|
|
160
|
+
if target.is_symlink() or not target.is_dir():
|
|
161
|
+
raise RuntimeError(f"Unsafe Antigravity target directory: {target}")
|
|
162
|
+
base = lexical_absolute(config_root) if config_root is not None else target / ".agents"
|
|
163
|
+
output_dir = base / "agents"
|
|
164
|
+
if base.is_symlink() or output_dir.is_symlink():
|
|
165
|
+
raise RuntimeError(f"Unsafe Antigravity config path: {output_dir}")
|
|
166
|
+
expected = {f"{AGENT_PREFIX}{name}" for name, _ in plan}
|
|
167
|
+
stale = _stale_files(output_dir, expected)
|
|
168
|
+
root = nearest_existing_root(target)
|
|
169
|
+
|
|
170
|
+
writes: list[tuple[SecureDestination, bytes]] = []
|
|
171
|
+
for name, rendered in plan:
|
|
172
|
+
path = output_dir / f"{AGENT_PREFIX}{name}" / "agent.md"
|
|
173
|
+
writes.append(
|
|
174
|
+
(
|
|
175
|
+
SecureDestination(path, root, f"Antigravity agent {name}"),
|
|
176
|
+
rendered.encode(),
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
stale_destinations = [
|
|
180
|
+
SecureDestination(path, root, f"stale Antigravity agent {path.parent.name}")
|
|
181
|
+
for path in stale
|
|
182
|
+
]
|
|
183
|
+
destinations = [destination for destination, _ in writes] + stale_destinations
|
|
184
|
+
|
|
185
|
+
def apply(transaction: SecureTransaction) -> tuple[int, int]:
|
|
186
|
+
written = 0
|
|
187
|
+
removed = 0
|
|
188
|
+
for destination, content in writes:
|
|
189
|
+
existing = transaction.initial_content(destination)
|
|
190
|
+
if existing is not None and not _is_managed(existing):
|
|
191
|
+
continue
|
|
192
|
+
transaction.atomic_write(destination, content, 0o644)
|
|
193
|
+
written += 1
|
|
194
|
+
for destination in stale_destinations:
|
|
195
|
+
existing = transaction.initial_content(destination)
|
|
196
|
+
if _is_managed(existing):
|
|
197
|
+
transaction.unlink(destination)
|
|
198
|
+
removed += 1
|
|
199
|
+
return written, removed
|
|
200
|
+
|
|
201
|
+
written, removed = run_secure_transaction(destinations, apply)
|
|
202
|
+
for stale_file in stale:
|
|
203
|
+
directory = stale_file.parent
|
|
204
|
+
if directory.is_dir() and not directory.is_symlink():
|
|
205
|
+
try:
|
|
206
|
+
directory.rmdir()
|
|
207
|
+
except OSError:
|
|
208
|
+
pass
|
|
209
|
+
return written, removed
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def cleanup(target_dir: Path, *, config_root: Path | None = None) -> int:
|
|
213
|
+
"""Remove managed Antigravity agents while preserving every user agent."""
|
|
214
|
+
target = lexical_absolute(target_dir)
|
|
215
|
+
if target.is_symlink() or not target.is_dir():
|
|
216
|
+
raise RuntimeError(f"Unsafe Antigravity target directory: {target}")
|
|
217
|
+
base = lexical_absolute(config_root) if config_root is not None else target / ".agents"
|
|
218
|
+
output_dir = base / "agents"
|
|
219
|
+
stale = _stale_files(output_dir, set())
|
|
220
|
+
if not stale:
|
|
221
|
+
return 0
|
|
222
|
+
root = nearest_existing_root(target)
|
|
223
|
+
destinations = [
|
|
224
|
+
SecureDestination(path, root, f"Antigravity agent {path.parent.name}")
|
|
225
|
+
for path in stale
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
def apply(transaction: SecureTransaction) -> int:
|
|
229
|
+
removed = 0
|
|
230
|
+
for destination in destinations:
|
|
231
|
+
if _is_managed(transaction.initial_content(destination)):
|
|
232
|
+
transaction.unlink(destination)
|
|
233
|
+
removed += 1
|
|
234
|
+
return removed
|
|
235
|
+
|
|
236
|
+
removed = run_secure_transaction(destinations, apply)
|
|
237
|
+
for path in stale:
|
|
238
|
+
try:
|
|
239
|
+
path.parent.rmdir()
|
|
240
|
+
except OSError:
|
|
241
|
+
pass
|
|
242
|
+
return removed
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main() -> None:
|
|
246
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
247
|
+
written, removed = generate(target)
|
|
248
|
+
print(
|
|
249
|
+
f"Generated: .agents/agents/ ({written} agents, "
|
|
250
|
+
f"{removed} stale removed)"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
if __name__ == "__main__":
|
|
255
|
+
main()
|