@softspark/ai-toolkit 2.12.0 → 3.0.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 +38 -0
- package/README.md +18 -8
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/skills/hook-creator/SKILL.md +84 -8
- package/app/skills/skill-creator/SKILL.md +8 -4
- package/benchmarks/ecosystem-doctor-snapshot.json +395 -0
- package/kb/history/completed/deep-coverage-v3-20260423.md +160 -0
- package/kb/history/completed/ecosystem-deep-sweep-20260423.md +273 -0
- package/kb/procedures/ecosystem-sync-sop.md +255 -0
- package/kb/procedures/maintenance-sop.md +13 -2
- package/kb/procedures/release-preparation-sop.md +83 -10
- package/kb/reference/global-install-model.md +15 -2
- package/kb/reference/supported-tools-registry.md +229 -0
- package/llms-full.txt +1052 -14
- package/llms.txt +4 -0
- package/manifest.json +1 -1
- package/package.json +4 -1
- package/scripts/ecosystem_doctor.py +348 -0
- package/scripts/ecosystem_tools.json +500 -0
- package/scripts/generate_aider_conf.py +26 -1
- package/scripts/generate_antigravity.py +77 -8
- package/scripts/generate_augment_agents.py +161 -0
- package/scripts/generate_augment_commands.py +160 -0
- package/scripts/generate_augment_hooks.py +162 -0
- package/scripts/generate_augment_skills.py +98 -0
- package/scripts/generate_cline_rules.py +96 -9
- package/scripts/generate_codex_hooks.py +13 -2
- package/scripts/generate_codex_skills.py +195 -0
- package/scripts/generate_copilot.py +296 -18
- package/scripts/generate_cursor_agents.py +144 -0
- package/scripts/generate_cursor_hooks.py +155 -0
- package/scripts/generate_cursor_mdc.py +20 -8
- package/scripts/generate_gemini_commands.py +158 -0
- package/scripts/generate_gemini_hooks.py +159 -0
- package/scripts/generate_gemini_skills.py +98 -0
- package/scripts/generate_roo_modes.py +42 -1
- package/scripts/generate_windsurf_hooks.py +143 -0
- package/scripts/generate_windsurf_rules.py +162 -10
- package/scripts/install.py +11 -2
- package/scripts/install_steps/ai_tools.py +120 -5
- package/scripts/validate.py +20 -3
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate ``.augment/agents/ai-toolkit-*.md`` files for Augment Code.
|
|
3
|
+
|
|
4
|
+
Each ai-toolkit agent in ``app/agents/`` is mirrored as an Augment native
|
|
5
|
+
subagent. Augment's subagent frontmatter (per docs.augmentcode.com) supports:
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
name: <slug>
|
|
9
|
+
description: "<single-line description>"
|
|
10
|
+
model: inherit # or explicit model id
|
|
11
|
+
color: <color-name> # optional UI hint
|
|
12
|
+
tools: [Read, Write, ...]
|
|
13
|
+
disabled_tools: []
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<body from agent file>
|
|
17
|
+
|
|
18
|
+
Design choices:
|
|
19
|
+
|
|
20
|
+
* ``model: inherit`` is used unconditionally. ai-toolkit stores short aliases
|
|
21
|
+
(``opus``/``sonnet``/``haiku``) that do not map to Augment's full model ids
|
|
22
|
+
without assuming a provider, so we defer to the user's default model.
|
|
23
|
+
* ``tools`` are passed through verbatim from the source file, normalized into
|
|
24
|
+
YAML flow-list form (``[Read, Write, ...]``) so Augment parses them as a
|
|
25
|
+
native list.
|
|
26
|
+
* ``disabled_tools: []`` is emitted as an explicit empty list to match the
|
|
27
|
+
shape Augment's UI expects when reading back the file.
|
|
28
|
+
* Files are prefixed ``ai-toolkit-`` so uninstall can identify ours without
|
|
29
|
+
touching user-authored agents.
|
|
30
|
+
* Regeneration removes stale ``ai-toolkit-*.md`` files whose source agent
|
|
31
|
+
no longer exists.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
python3 scripts/generate_augment_agents.py [target-dir]
|
|
35
|
+
|
|
36
|
+
Writes files to ``target-dir/.augment/agents/``.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import sys
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
44
|
+
from emission import agents_dir
|
|
45
|
+
from frontmatter import frontmatter_field
|
|
46
|
+
|
|
47
|
+
AGENT_PREFIX = "ai-toolkit-"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _agent_body(agent_file: Path) -> str:
|
|
51
|
+
"""Return the markdown body of an agent file (content after frontmatter)."""
|
|
52
|
+
text = agent_file.read_text(encoding="utf-8")
|
|
53
|
+
if not text.startswith("---"):
|
|
54
|
+
return text.strip() + "\n"
|
|
55
|
+
parts = text.split("---", 2)
|
|
56
|
+
if len(parts) < 3:
|
|
57
|
+
return text.strip() + "\n"
|
|
58
|
+
return parts[2].lstrip("\n")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_tools(tools_raw: str) -> list[str]:
|
|
62
|
+
"""Parse the comma-separated ``tools:`` frontmatter value into a list."""
|
|
63
|
+
if not tools_raw:
|
|
64
|
+
return []
|
|
65
|
+
return [t.strip() for t in tools_raw.split(",") if t.strip()]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _render_augment_agent(agent_file: Path) -> str:
|
|
69
|
+
"""Render a single Augment subagent .md file from an ai-toolkit agent."""
|
|
70
|
+
name = frontmatter_field(agent_file, "name")
|
|
71
|
+
description = frontmatter_field(agent_file, "description")
|
|
72
|
+
color = frontmatter_field(agent_file, "color")
|
|
73
|
+
tools_raw = frontmatter_field(agent_file, "tools")
|
|
74
|
+
tools = _parse_tools(tools_raw)
|
|
75
|
+
|
|
76
|
+
# Escape description for YAML quoted string
|
|
77
|
+
safe_desc = description.replace('"', "'")
|
|
78
|
+
|
|
79
|
+
lines: list[str] = ["---"]
|
|
80
|
+
lines.append(f"name: {name}")
|
|
81
|
+
lines.append(f'description: "{safe_desc}"')
|
|
82
|
+
# Augment recommends ``model: inherit`` for plugins that want to follow
|
|
83
|
+
# the user's active model selection. ai-toolkit agents are authored with
|
|
84
|
+
# short aliases that do not map to Augment's provider-qualified ids, so
|
|
85
|
+
# we deliberately emit ``inherit`` instead of translating.
|
|
86
|
+
lines.append("model: inherit")
|
|
87
|
+
if color:
|
|
88
|
+
lines.append(f"color: {color}")
|
|
89
|
+
if tools:
|
|
90
|
+
tools_flow = ", ".join(tools)
|
|
91
|
+
lines.append(f"tools: [{tools_flow}]")
|
|
92
|
+
else:
|
|
93
|
+
lines.append("tools: []")
|
|
94
|
+
# Explicit empty disabled_tools for shape parity with Augment UI exports
|
|
95
|
+
lines.append("disabled_tools: []")
|
|
96
|
+
lines.append("---")
|
|
97
|
+
lines.append("")
|
|
98
|
+
body = _agent_body(agent_file).rstrip()
|
|
99
|
+
if body:
|
|
100
|
+
lines.append(body)
|
|
101
|
+
lines.append("")
|
|
102
|
+
return "\n".join(lines)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _cleanup_stale(agents_out: Path) -> int:
|
|
106
|
+
"""Remove stale ai-toolkit-* agent files whose source no longer exists.
|
|
107
|
+
|
|
108
|
+
Only touches files with the ``ai-toolkit-`` prefix so user-authored
|
|
109
|
+
Augment agents are preserved.
|
|
110
|
+
"""
|
|
111
|
+
if not agents_out.is_dir():
|
|
112
|
+
return 0
|
|
113
|
+
removed = 0
|
|
114
|
+
for f in sorted(agents_out.glob(f"{AGENT_PREFIX}*.md")):
|
|
115
|
+
source_name = f.stem[len(AGENT_PREFIX):]
|
|
116
|
+
source = agents_dir / f"{source_name}.md"
|
|
117
|
+
if not source.is_file():
|
|
118
|
+
f.unlink()
|
|
119
|
+
removed += 1
|
|
120
|
+
return removed
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate(
|
|
124
|
+
target_dir: Path, config_root: Path | None = None
|
|
125
|
+
) -> tuple[int, int]:
|
|
126
|
+
"""Write Augment agent files and return (written, removed_stale).
|
|
127
|
+
|
|
128
|
+
By default writes to ``target_dir/.augment/agents/`` (project-local).
|
|
129
|
+
Pass ``config_root=~/.augment`` for the global layout, which writes
|
|
130
|
+
directly under ``agents/`` without the ``.augment/`` prefix.
|
|
131
|
+
"""
|
|
132
|
+
base = config_root if config_root is not None else target_dir / ".augment"
|
|
133
|
+
agents_out = base / "agents"
|
|
134
|
+
agents_out.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
written = 0
|
|
137
|
+
for agent_file in sorted(agents_dir.glob("*.md")):
|
|
138
|
+
name = frontmatter_field(agent_file, "name")
|
|
139
|
+
description = frontmatter_field(agent_file, "description")
|
|
140
|
+
if not name or not description:
|
|
141
|
+
continue
|
|
142
|
+
out_path = agents_out / f"{AGENT_PREFIX}{name}.md"
|
|
143
|
+
out_path.write_text(_render_augment_agent(agent_file), encoding="utf-8")
|
|
144
|
+
written += 1
|
|
145
|
+
|
|
146
|
+
removed = _cleanup_stale(agents_out)
|
|
147
|
+
return written, removed
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main() -> None:
|
|
151
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
152
|
+
written, removed = generate(target)
|
|
153
|
+
msg = f"Generated: .augment/agents/ ({written} agents"
|
|
154
|
+
if removed:
|
|
155
|
+
msg += f", {removed} stale removed"
|
|
156
|
+
msg += ")"
|
|
157
|
+
print(msg)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate ``.augment/commands/ai-toolkit-*.md`` files for Augment Code.
|
|
3
|
+
|
|
4
|
+
User-invocable skills become Augment custom slash commands. Knowledge skills
|
|
5
|
+
(``user-invocable: false``) are excluded — they load automatically via
|
|
6
|
+
AGENTS.md/rules context instead of ``/`` invocation.
|
|
7
|
+
|
|
8
|
+
Per docs.augmentcode.com, Augment custom commands are plain markdown files
|
|
9
|
+
with an optional YAML frontmatter header. The body of the file IS the prompt
|
|
10
|
+
(no ``template:`` field, no TOML). Supported frontmatter fields:
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
description: "<short one-liner shown in the palette>"
|
|
14
|
+
agent: <optional agent slug to route into>
|
|
15
|
+
argument-hint: "<optional hint shown after the command name>"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<prompt body — supports $ARGUMENTS placeholder>
|
|
19
|
+
|
|
20
|
+
Files are prefixed ``ai-toolkit-`` so install/uninstall can identify ours
|
|
21
|
+
without touching user files.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
python3 scripts/generate_augment_commands.py [target-dir]
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
32
|
+
from emission import skills_dir
|
|
33
|
+
from frontmatter import frontmatter_field
|
|
34
|
+
|
|
35
|
+
COMMAND_PREFIX = "ai-toolkit-"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _skill_body(skill_file: Path) -> str:
|
|
39
|
+
"""Return the markdown body of a skill (content after frontmatter)."""
|
|
40
|
+
text = skill_file.read_text(encoding="utf-8")
|
|
41
|
+
if not text.startswith("---"):
|
|
42
|
+
return text
|
|
43
|
+
parts = text.split("---", 2)
|
|
44
|
+
return parts[2].lstrip("\n") if len(parts) >= 3 else text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _map_agent_name(value: str) -> str:
|
|
48
|
+
"""Map ai-toolkit agent names to our prefixed Augment subagent slugs."""
|
|
49
|
+
value = value.strip()
|
|
50
|
+
if not value:
|
|
51
|
+
return value
|
|
52
|
+
if value.startswith(COMMAND_PREFIX):
|
|
53
|
+
return value
|
|
54
|
+
return f"{COMMAND_PREFIX}{value}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _render_augment_command(skill_file: Path) -> str:
|
|
58
|
+
"""Render a single Augment command .md file from a user-invocable skill."""
|
|
59
|
+
description = frontmatter_field(skill_file, "description")
|
|
60
|
+
agent_field = frontmatter_field(skill_file, "agent")
|
|
61
|
+
argument_hint = frontmatter_field(skill_file, "argument-hint")
|
|
62
|
+
body = _skill_body(skill_file).rstrip()
|
|
63
|
+
|
|
64
|
+
lines: list[str] = ["---"]
|
|
65
|
+
if description:
|
|
66
|
+
safe_desc = description.replace('"', "'")
|
|
67
|
+
lines.append(f'description: "{safe_desc}"')
|
|
68
|
+
if agent_field:
|
|
69
|
+
lines.append(f"agent: {_map_agent_name(agent_field)}")
|
|
70
|
+
if argument_hint:
|
|
71
|
+
safe_hint = argument_hint.replace('"', "'")
|
|
72
|
+
lines.append(f'argument-hint: "{safe_hint}"')
|
|
73
|
+
lines.append("---")
|
|
74
|
+
lines.append("")
|
|
75
|
+
if body:
|
|
76
|
+
lines.append(body)
|
|
77
|
+
lines.append("")
|
|
78
|
+
return "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_user_invocable(skill_file: Path) -> bool:
|
|
82
|
+
"""Return True if the skill should be exposed as a ``/`` command.
|
|
83
|
+
|
|
84
|
+
Matches the opencode generator's logic:
|
|
85
|
+
* explicit ``user-invocable: true`` -> yes
|
|
86
|
+
* implicit: ``disable-model-invocation: true`` -> yes (task skill)
|
|
87
|
+
* default -> no (keeps knowledge skills out of the command palette)
|
|
88
|
+
"""
|
|
89
|
+
invocable = frontmatter_field(skill_file, "user-invocable")
|
|
90
|
+
if invocable:
|
|
91
|
+
return invocable.lower() not in ("false", "0", "no")
|
|
92
|
+
disable_model = frontmatter_field(skill_file, "disable-model-invocation")
|
|
93
|
+
if disable_model and disable_model.lower() in ("true", "1", "yes"):
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cleanup_stale(commands_out: Path) -> int:
|
|
99
|
+
"""Remove stale ai-toolkit-* command files whose source no longer exists.
|
|
100
|
+
|
|
101
|
+
Also removes commands whose source skill is no longer user-invocable so
|
|
102
|
+
flipping a skill's ``user-invocable`` flag back to ``false`` cleanly
|
|
103
|
+
retracts the command.
|
|
104
|
+
"""
|
|
105
|
+
if not commands_out.is_dir():
|
|
106
|
+
return 0
|
|
107
|
+
removed = 0
|
|
108
|
+
for f in sorted(commands_out.glob(f"{COMMAND_PREFIX}*.md")):
|
|
109
|
+
source_name = f.stem[len(COMMAND_PREFIX):]
|
|
110
|
+
source = skills_dir / source_name / "SKILL.md"
|
|
111
|
+
if not source.is_file() or not _is_user_invocable(source):
|
|
112
|
+
f.unlink()
|
|
113
|
+
removed += 1
|
|
114
|
+
return removed
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def generate(
|
|
118
|
+
target_dir: Path, config_root: Path | None = None
|
|
119
|
+
) -> tuple[int, int]:
|
|
120
|
+
"""Write Augment command files and return (written, removed_stale).
|
|
121
|
+
|
|
122
|
+
By default writes to ``target_dir/.augment/commands/`` (project-local).
|
|
123
|
+
Pass ``config_root=~/.augment`` for the global layout.
|
|
124
|
+
"""
|
|
125
|
+
base = config_root if config_root is not None else target_dir / ".augment"
|
|
126
|
+
commands_out = base / "commands"
|
|
127
|
+
commands_out.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
|
|
129
|
+
written = 0
|
|
130
|
+
for skill_dir in sorted(skills_dir.iterdir()):
|
|
131
|
+
if skill_dir.name.startswith("_"):
|
|
132
|
+
continue
|
|
133
|
+
skill_file = skill_dir / "SKILL.md"
|
|
134
|
+
if not skill_file.is_file():
|
|
135
|
+
continue
|
|
136
|
+
if not _is_user_invocable(skill_file):
|
|
137
|
+
continue
|
|
138
|
+
name = frontmatter_field(skill_file, "name")
|
|
139
|
+
if not name:
|
|
140
|
+
continue
|
|
141
|
+
out_path = commands_out / f"{COMMAND_PREFIX}{name}.md"
|
|
142
|
+
out_path.write_text(_render_augment_command(skill_file), encoding="utf-8")
|
|
143
|
+
written += 1
|
|
144
|
+
|
|
145
|
+
removed = _cleanup_stale(commands_out)
|
|
146
|
+
return written, removed
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main() -> None:
|
|
150
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
151
|
+
written, removed = generate(target)
|
|
152
|
+
msg = f"Generated: .augment/commands/ ({written} commands"
|
|
153
|
+
if removed:
|
|
154
|
+
msg += f", {removed} stale removed"
|
|
155
|
+
msg += ")"
|
|
156
|
+
print(msg)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
main()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate Augment settings.json hooks block.
|
|
3
|
+
|
|
4
|
+
Augment stores hooks in `~/.augment/settings.json` (user scope) — there is no
|
|
5
|
+
per-workspace hooks file. `/etc/augment/settings.json` exists for system-wide
|
|
6
|
+
policy and we never write there. The generator accepts an optional target
|
|
7
|
+
directory; when omitted it defaults to the user's `$HOME` and writes
|
|
8
|
+
`~/.augment/settings.json`.
|
|
9
|
+
|
|
10
|
+
Augment hook events (per docs.augmentcode.com/cli/hooks.md):
|
|
11
|
+
PreToolUse, PostToolUse, SessionStart, SessionEnd, Stop
|
|
12
|
+
Schema mirrors Claude Code — each event holds an array of groups, each group
|
|
13
|
+
holds `{matcher?, hooks: [{type, command, timeout?}]}`. We tag the outer group
|
|
14
|
+
with `_source: ai-toolkit` so we can strip and rewrite idempotently without
|
|
15
|
+
clobbering user hook groups.
|
|
16
|
+
|
|
17
|
+
Common Augment tool names relevant to our hooks:
|
|
18
|
+
launch-process (shell), view (read), str-replace-editor (edit),
|
|
19
|
+
save-file (write), remove-files (delete), web-fetch, web-search,
|
|
20
|
+
codebase-retrieval, github-api, linear.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
python3 scripts/generate_augment_hooks.py [target-dir]
|
|
24
|
+
|
|
25
|
+
`target-dir` is the HOME-equivalent directory (default: `$HOME`). The
|
|
26
|
+
settings file is written to `<target-dir>/.augment/settings.json`.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
HOOKS_PREFIX = '"$HOME/.softspark/ai-toolkit/hooks/'
|
|
36
|
+
SOURCE_TAG = "ai-toolkit"
|
|
37
|
+
|
|
38
|
+
# Event -> list of (matcher, script). Matcher is a regex over tool_name.
|
|
39
|
+
AUGMENT_HOOKS: dict[str, list[tuple[str, str]]] = {
|
|
40
|
+
"SessionStart": [
|
|
41
|
+
("", "session-start.sh"),
|
|
42
|
+
("", "mcp-health.sh"),
|
|
43
|
+
("", "session-context.sh"),
|
|
44
|
+
],
|
|
45
|
+
"PreToolUse": [
|
|
46
|
+
("launch-process", "guard-destructive.sh"),
|
|
47
|
+
("launch-process", "commit-quality.sh"),
|
|
48
|
+
("view|str-replace-editor|save-file|remove-files", "guard-path.sh"),
|
|
49
|
+
("str-replace-editor|save-file", "guard-config.sh"),
|
|
50
|
+
],
|
|
51
|
+
"PostToolUse": [
|
|
52
|
+
("str-replace-editor|save-file", "post-tool-use.sh"),
|
|
53
|
+
("launch-process|str-replace-editor|save-file", "governance-capture.sh"),
|
|
54
|
+
],
|
|
55
|
+
"Stop": [
|
|
56
|
+
("", "quality-check.sh"),
|
|
57
|
+
("", "save-session.sh"),
|
|
58
|
+
],
|
|
59
|
+
"SessionEnd": [
|
|
60
|
+
("", "session-end.sh"),
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_hook_group(matcher: str, script: str) -> dict:
|
|
66
|
+
"""Build a single Augment hook group (merge-safe shape)."""
|
|
67
|
+
group: dict = {
|
|
68
|
+
"_source": SOURCE_TAG,
|
|
69
|
+
"hooks": [
|
|
70
|
+
{
|
|
71
|
+
"type": "command",
|
|
72
|
+
"command": f"{HOOKS_PREFIX}{script}\"",
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
# Augment ignores `matcher` on session events; omit cleanly so it doesn't
|
|
77
|
+
# show up as a spurious regex-against-nothing in their logs.
|
|
78
|
+
if matcher:
|
|
79
|
+
group["matcher"] = matcher
|
|
80
|
+
return group
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_toolkit_hooks() -> dict[str, list[dict]]:
|
|
84
|
+
return {event: [build_hook_group(m, s) for m, s in entries]
|
|
85
|
+
for event, entries in AUGMENT_HOOKS.items()}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_toolkit_entry(entry: dict) -> bool:
|
|
89
|
+
if not isinstance(entry, dict):
|
|
90
|
+
return False
|
|
91
|
+
if entry.get("_source") == SOURCE_TAG:
|
|
92
|
+
return True
|
|
93
|
+
# Pre-2.13 layout tagged the inner hook dict instead of the outer group.
|
|
94
|
+
for h in entry.get("hooks", []) or []:
|
|
95
|
+
if isinstance(h, dict) and h.get("_source") == SOURCE_TAG:
|
|
96
|
+
return True
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def strip_toolkit_hooks(hooks: dict) -> dict:
|
|
101
|
+
kept: dict = {}
|
|
102
|
+
for event, entries in hooks.items():
|
|
103
|
+
if not isinstance(entries, list):
|
|
104
|
+
kept[event] = entries
|
|
105
|
+
continue
|
|
106
|
+
survivors = [e for e in entries if not _is_toolkit_entry(e)]
|
|
107
|
+
if survivors:
|
|
108
|
+
kept[event] = survivors
|
|
109
|
+
return kept
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def merge_hooks(existing: dict, toolkit: dict) -> dict:
|
|
113
|
+
merged = strip_toolkit_hooks(existing)
|
|
114
|
+
for event, entries in toolkit.items():
|
|
115
|
+
merged.setdefault(event, []).extend(entries)
|
|
116
|
+
return merged
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _resolve_target_dir(argv: list[str]) -> Path:
|
|
120
|
+
"""Accept an explicit target dir or fall back to $HOME."""
|
|
121
|
+
if len(argv) > 1 and argv[1]:
|
|
122
|
+
return Path(argv[1])
|
|
123
|
+
home = os.environ.get("HOME")
|
|
124
|
+
if not home:
|
|
125
|
+
raise SystemExit("HOME is not set and no target directory was provided.")
|
|
126
|
+
return Path(home)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def generate(target_dir: Path) -> Path:
|
|
130
|
+
"""Write `<target_dir>/.augment/settings.json` and return its path."""
|
|
131
|
+
aug_dir = target_dir / ".augment"
|
|
132
|
+
aug_dir.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
path = aug_dir / "settings.json"
|
|
134
|
+
|
|
135
|
+
settings: dict = {}
|
|
136
|
+
if path.is_file():
|
|
137
|
+
try:
|
|
138
|
+
with open(path, encoding="utf-8") as f:
|
|
139
|
+
settings = json.load(f)
|
|
140
|
+
if not isinstance(settings, dict):
|
|
141
|
+
settings = {}
|
|
142
|
+
except (json.JSONDecodeError, OSError):
|
|
143
|
+
settings = {}
|
|
144
|
+
|
|
145
|
+
existing_hooks = settings.get("hooks") if isinstance(settings.get("hooks"), dict) else {}
|
|
146
|
+
settings["hooks"] = merge_hooks(existing_hooks or {}, build_toolkit_hooks())
|
|
147
|
+
|
|
148
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
149
|
+
json.dump(settings, f, indent=4, ensure_ascii=False, sort_keys=True)
|
|
150
|
+
f.write("\n")
|
|
151
|
+
return path
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main() -> None:
|
|
155
|
+
target = _resolve_target_dir(sys.argv)
|
|
156
|
+
path = generate(target)
|
|
157
|
+
total = sum(len(v) for v in AUGMENT_HOOKS.values())
|
|
158
|
+
print(f"Generated: {path} ({total} hooks across {len(AUGMENT_HOOKS)} events)")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate an Augment skill pointer under ``.augment/skills/``.
|
|
3
|
+
|
|
4
|
+
Augment's SKILL.md reader follows the Agent Skills standard at
|
|
5
|
+
``.augment/skills/<skill-name>/SKILL.md``. To avoid duplicating the full
|
|
6
|
+
ai-toolkit skill catalogue (99 SKILL.md files) into Augment's directory,
|
|
7
|
+
we emit a single **pointer skill** that teaches Augment to resolve real
|
|
8
|
+
skills from the canonical Claude Code locations:
|
|
9
|
+
|
|
10
|
+
* ``.claude/skills/<name>/SKILL.md`` (project-local install)
|
|
11
|
+
* ``~/.claude/skills/<name>/SKILL.md`` (global install)
|
|
12
|
+
|
|
13
|
+
This mirrors the pattern used by ``generate_antigravity.py`` and
|
|
14
|
+
``generate_gemini_skills.py``.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python3 scripts/generate_augment_skills.py [target-dir]
|
|
18
|
+
|
|
19
|
+
Writes ``<target-dir>/.augment/skills/ai-toolkit-skill-catalogue/SKILL.md``.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
+
from emission import emit_skills_bullets
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Skill pointer — a single SKILL.md that directs Augment to our catalogue
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
POINTER_SKILL_NAME = "ai-toolkit-skill-catalogue"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _pointer_skill_md() -> str:
|
|
38
|
+
"""Build the SKILL.md body for the Augment pointer skill."""
|
|
39
|
+
body = (
|
|
40
|
+
"# AI Toolkit Skill Catalogue\n\n"
|
|
41
|
+
"This workspace uses the ai-toolkit. Real skills are installed "
|
|
42
|
+
"alongside Claude Code at `.claude/skills/` (project install) or "
|
|
43
|
+
"`~/.claude/skills/` (global install). If the ai-toolkit installer "
|
|
44
|
+
"has run, every skill below already exists as an agent-invocable "
|
|
45
|
+
"SKILL.md on disk.\n\n"
|
|
46
|
+
"## When to use this skill\n\n"
|
|
47
|
+
"- You are asked to run a task that maps to one of the catalogued "
|
|
48
|
+
"skills below.\n"
|
|
49
|
+
"- You want to discover which skills the user has installed before "
|
|
50
|
+
"reinventing the wheel.\n\n"
|
|
51
|
+
"## How to invoke a catalogue entry\n\n"
|
|
52
|
+
"1. Match the user's task to a skill name in the catalogue.\n"
|
|
53
|
+
"2. Read the skill's SKILL.md from `.claude/skills/<name>/SKILL.md` "
|
|
54
|
+
"or `~/.claude/skills/<name>/SKILL.md` (whichever exists).\n"
|
|
55
|
+
"3. Follow its Rules, Gotchas, and When NOT to Use sections.\n\n"
|
|
56
|
+
"## Catalogue (installed skills)\n\n"
|
|
57
|
+
f"{emit_skills_bullets()}\n"
|
|
58
|
+
)
|
|
59
|
+
return (
|
|
60
|
+
"---\n"
|
|
61
|
+
f"name: {POINTER_SKILL_NAME}\n"
|
|
62
|
+
"description: Index of ai-toolkit skills installed at .claude/skills/"
|
|
63
|
+
" or ~/.claude/skills/. Read this first when the user's request "
|
|
64
|
+
"matches a named skill.\n"
|
|
65
|
+
"---\n"
|
|
66
|
+
f"{body}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _write_skill_pointer(target_dir: Path) -> None:
|
|
71
|
+
"""Write the pointer SKILL.md under .augment/skills/<name>/."""
|
|
72
|
+
skill_dir = target_dir / ".augment" / "skills" / POINTER_SKILL_NAME
|
|
73
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
(skill_dir / "SKILL.md").write_text(_pointer_skill_md(), encoding="utf-8")
|
|
75
|
+
print(f" Generated: .augment/skills/{POINTER_SKILL_NAME}/SKILL.md")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Main
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def generate(target_dir: Path, *, emit_skill_pointer: bool = True) -> None:
|
|
83
|
+
"""Write ``.augment/skills/ai-toolkit-skill-catalogue/SKILL.md``.
|
|
84
|
+
|
|
85
|
+
``emit_skill_pointer`` controls whether the pointer SKILL.md is written.
|
|
86
|
+
Set to ``False`` to leave Augment's skills directory untouched.
|
|
87
|
+
"""
|
|
88
|
+
if emit_skill_pointer:
|
|
89
|
+
_write_skill_pointer(target_dir)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> None:
|
|
93
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
94
|
+
generate(target)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
main()
|