@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +18 -8
  3. package/app/.claude-plugin/plugin.json +1 -1
  4. package/app/skills/hook-creator/SKILL.md +84 -8
  5. package/app/skills/skill-creator/SKILL.md +8 -4
  6. package/benchmarks/ecosystem-doctor-snapshot.json +395 -0
  7. package/kb/history/completed/deep-coverage-v3-20260423.md +160 -0
  8. package/kb/history/completed/ecosystem-deep-sweep-20260423.md +273 -0
  9. package/kb/procedures/ecosystem-sync-sop.md +255 -0
  10. package/kb/procedures/maintenance-sop.md +13 -2
  11. package/kb/procedures/release-preparation-sop.md +83 -10
  12. package/kb/reference/global-install-model.md +15 -2
  13. package/kb/reference/supported-tools-registry.md +229 -0
  14. package/llms-full.txt +1052 -14
  15. package/llms.txt +4 -0
  16. package/manifest.json +1 -1
  17. package/package.json +4 -1
  18. package/scripts/ecosystem_doctor.py +348 -0
  19. package/scripts/ecosystem_tools.json +500 -0
  20. package/scripts/generate_aider_conf.py +26 -1
  21. package/scripts/generate_antigravity.py +77 -8
  22. package/scripts/generate_augment_agents.py +161 -0
  23. package/scripts/generate_augment_commands.py +160 -0
  24. package/scripts/generate_augment_hooks.py +162 -0
  25. package/scripts/generate_augment_skills.py +98 -0
  26. package/scripts/generate_cline_rules.py +96 -9
  27. package/scripts/generate_codex_hooks.py +13 -2
  28. package/scripts/generate_codex_skills.py +195 -0
  29. package/scripts/generate_copilot.py +296 -18
  30. package/scripts/generate_cursor_agents.py +144 -0
  31. package/scripts/generate_cursor_hooks.py +155 -0
  32. package/scripts/generate_cursor_mdc.py +20 -8
  33. package/scripts/generate_gemini_commands.py +158 -0
  34. package/scripts/generate_gemini_hooks.py +159 -0
  35. package/scripts/generate_gemini_skills.py +98 -0
  36. package/scripts/generate_roo_modes.py +42 -1
  37. package/scripts/generate_windsurf_hooks.py +143 -0
  38. package/scripts/generate_windsurf_rules.py +162 -10
  39. package/scripts/install.py +11 -2
  40. package/scripts/install_steps/ai_tools.py +120 -5
  41. 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()