@@ -46,6 +46,40 @@ def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
4646 return meta , body
4747
4848
49+ def _extract_tools_from_frontmatter (lines : list [str ]) -> list [str ]:
50+ """Parse a YAML list of tools from frontmatter lines (handles ``tools:`` key)."""
51+ tools : list [str ] = []
52+ in_tools = False
53+ for line in lines :
54+ stripped = line .strip ()
55+ if stripped .startswith ("tools:" ):
56+ in_tools = True
57+ continue
58+ if in_tools :
59+ if stripped .startswith ("- " ):
60+ tools .append (stripped [2 :].strip ())
61+ else :
62+ break
63+ return tools
64+
65+
66+ def _extract_trigger_section (body : str ) -> list [str ]:
67+ """Pull bullet items from the ``## Trigger`` section of a SKILL.md body."""
68+ triggers : list [str ] = []
69+ in_trigger = False
70+ for line in body .splitlines ():
71+ stripped = line .strip ()
72+ if re .match (r"^##\s+Trigger" , stripped , re .IGNORECASE ):
73+ in_trigger = True
74+ continue
75+ if in_trigger :
76+ if stripped .startswith ("##" ):
77+ break
78+ if stripped .startswith ("- " ):
79+ triggers .append (stripped [2 :].strip ())
80+ return triggers
81+
82+
4983def parse_skills (repo_root : Path ) -> list [dict ]:
5084 skills_dir = repo_root / "skills"
5185 if not skills_dir .is_dir ():
@@ -80,10 +114,16 @@ def parse_skills(repo_root: Path) -> list[dict]:
80114 name = re .sub (r"^#+\s*" , "" , stripped )
81115 break
82116
117+ fm_lines = text .splitlines ()
118+ tools = _extract_tools_from_frontmatter (fm_lines )
119+ triggers = _extract_trigger_section (body )
120+
83121 results .append ({
84122 "name" : name ,
85123 "description" : description ,
86124 "category" : skill_dir .name ,
125+ "tools" : tools ,
126+ "triggers" : triggers ,
87127 })
88128
89129 return results
@@ -130,6 +170,60 @@ def parse_rules(repo_root: Path) -> list[dict]:
130170 return results
131171
132172
173+ def parse_changelog (repo_root : Path , max_entries : int = 2 ) -> list [dict ]:
174+ """Parse a Keep-a-Changelog formatted CHANGELOG.md.
175+
176+ Returns up to *max_entries* release entries (skipping ``[Unreleased]``),
177+ each as ``{ version, date, sections: [{ heading, items }] }``.
178+ """
179+ changelog_path = repo_root / "CHANGELOG.md"
180+ if not changelog_path .is_file ():
181+ return []
182+
183+ text = changelog_path .read_text (encoding = "utf-8" , errors = "replace" )
184+ entries : list [dict ] = []
185+ current_entry : dict | None = None
186+ current_section : dict | None = None
187+
188+ for line in text .splitlines ():
189+ heading_match = re .match (r"^##\s+\[(.+?)\](?:\s*-\s*(.+))?" , line )
190+ if heading_match :
191+ version = heading_match .group (1 )
192+ date = (heading_match .group (2 ) or "" ).strip ()
193+ if version .lower () == "unreleased" :
194+ continue
195+ if current_entry is not None :
196+ if current_section :
197+ current_entry ["sections" ].append (current_section )
198+ entries .append (current_entry )
199+ if len (entries ) >= max_entries :
200+ break
201+ current_entry = {"version" : version , "date" : date , "sections" : []}
202+ current_section = None
203+ continue
204+
205+ if current_entry is None :
206+ continue
207+
208+ sub_match = re .match (r"^###\s+(.+)" , line )
209+ if sub_match :
210+ if current_section :
211+ current_entry ["sections" ].append (current_section )
212+ current_section = {"heading" : sub_match .group (1 ).strip (), "entries" : []}
213+ continue
214+
215+ if current_section is not None and line .strip ().startswith ("- " ):
216+ current_section ["entries" ].append (line .strip ()[2 :].strip ())
217+
218+ if current_entry is not None :
219+ if current_section :
220+ current_entry ["sections" ].append (current_section )
221+ if len (entries ) < max_entries :
222+ entries .append (current_entry )
223+
224+ return entries
225+
226+
133227def load_mcp_tools (repo_root : Path ) -> list [dict ]:
134228 mcp_file = repo_root / "mcp-tools.json"
135229 if not mcp_file .is_file ():
@@ -185,6 +279,7 @@ def main():
185279 rules = parse_rules (repo_root )
186280 mcp_tools = load_mcp_tools (repo_root )
187281 mcp_grouped = group_by_category (mcp_tools )
282+ changelog = parse_changelog (repo_root )
188283
189284 context = {
190285 "plugin" : plugin ,
@@ -196,6 +291,8 @@ def main():
196291 "mcp_tools" : mcp_tools ,
197292 "mcp_tool_count" : len (mcp_tools ),
198293 "mcp_grouped" : mcp_grouped ,
294+ "changelog" : changelog ,
295+ "has_changelog" : len (changelog ) > 0 ,
199296 "build_date" : datetime .date .today ().isoformat (),
200297 }
201298
0 commit comments