Skip to content

Commit fe42614

Browse files
committed
fix(cli): improve skills validation
- Added a well-known endpoint for installing the `elements` skill via skills CLI - Implemented validation for skill names and descriptions to ensure compliance with defined standards. - Updated the `elements` skill metadata to include a title and license in the generated markdown. - Refactored skill markdown formatting to ensure proper structure and compliance with new requirements. - Introduced tests for validating skill entries and ensuring correct markdown formatting. - Removed deprecated markdown utility functions and updated imports accordingly. This update improves the usability and reliability of the Agent Skills feature in the CLI. Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent a9a0571 commit fe42614

17 files changed

Lines changed: 431 additions & 36 deletions

File tree

‎projects/cli/README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ Skills provide persistent context to AI agents for building UI with Elements.
163163

164164
Run `nve skills.list` or call MCP `skills_list` for the authoritative list. Deployments with the playground service enabled can also expose a `playground` skill for creating Elements Playground prototypes.
165165

166+
The Agent Skills well-known endpoint publishes the `elements` skill from the same registry for skill-only installation with the open `skills` CLI:
167+
168+
```shell
169+
npx skills add https://nvidia.github.io/elements
170+
```
171+
172+
This hosted route does not install the Elements CLI or configure the MCP server. Use `nve project.setup` for complete project setup, and continue to use the CLI or MCP tools for deterministic API lookup and template validation. Other registry skills, including a conditional `playground` skill, remain available through `nve` rather than the hosted endpoint.
173+
166174
### Tools
167175

168176
| Tool | Description |

‎projects/internals/tools/src/skills/index.test.ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ describe('skillEntries', () => {
147147
expect(uniqueNames.size).toBe(names.length);
148148
});
149149

150+
it('should have valid Agent Skills names and descriptions',()=>{
151+
skills.forEach(skill=>{
152+
expect(skill.name).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
153+
expect(skill.name.length).toBeLessThanOrEqual(64);
154+
expect(skill.description.length).toBeGreaterThanOrEqual(1);
155+
expect(skill.description.length).toBeLessThanOrEqual(1024);
156+
});
157+
});
158+
150159
it('should include authoring, artifact, and elements entries',()=>{
151160
expect(skills.some(skill=>skill.name==='authoring')).toBe(true);
152161
expect(skills.some(skill=>skill.name==='artifact')).toBe(true);
@@ -170,9 +179,21 @@ describe('skillEntries', () => {
170179

171180
constmarkdown=formatSkillMarkdown(elementsSkill);
172181

173-
expect(markdown).toMatch(/^---\nname:"elements"\ntitle:"ElementsDesignSystem\(nve\)"/);
174-
expect(markdown).toContain('description: "Use this skill by default');
182+
expect(markdown).toMatch(/^---\nname:"elements"\ndescription:"Usethisskillbydefault/);
183+
expect(markdown).toContain('\nlicense: "Apache-2.0"\n');
184+
expect(markdown).toContain('\nmetadata:\n title: "NVIDIA Elements Design System \(nve\)"\n');
185+
expect(markdown).not.toMatch(/^title:/m);
175186
expect(markdown).toContain('# Building UI with NVIDIA Elements');
176187
expect(markdown.endsWith('\n')).toBe(true);
188+
expect(markdown.endsWith('\n\n')).toBe(false);
189+
});
190+
191+
it('should terminate every formatted skill with one newline',()=>{
192+
skills.forEach(skill=>{
193+
constmarkdown=formatSkillMarkdown(skill);
194+
195+
expect(markdown.endsWith('\n')).toBe(true);
196+
expect(markdown.endsWith('\n\n')).toBe(false);
197+
});
177198
});
178199
});

‎projects/internals/tools/src/skills/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
export*from'./registry.js';
5-
export*from'./markdown.js';
65
export*from'./service.js';
6+
export*from'./utils.js';

‎projects/internals/tools/src/skills/markdown.ts‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

‎projects/internals/tools/src/skills/registry.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ const migrateProjectPrompt: Prompt = {
206206

207207
constelementsSkill: Skill={
208208
name: 'elements',
209-
title: 'Elements Design System (nve)',
209+
title: 'NVIDIA Elements Design System (nve)',
210210
description:
211211
'Use this skill by default for any UI-related work or with NVIDIA Elements (nve-*), including creating, editing, reviewing, or debugging HTML, CSS, layout, theming, components, applications, prototypes, Claude Artifacts, Codex Sites pages, and standalone UI artifacts.',
212212
context: `

‎projects/internals/tools/src/skills/service.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ describe('SkillsService', () => {
2929
expect(result).toContain(
3030
`---
3131
name: "authoring"
32-
title: "NVIDIA Elements Authoring Guidelines"
3332
description: "Best practices and workflow guidance for authoring UI with NVIDIA Elements."
33+
license: "Apache-2.0"
34+
metadata:
35+
title: "NVIDIA Elements Authoring Guidelines"
3436
---`
3537
);
3638
expect(result).toMatch(/^---\nname:"authoring"/);
39+
expect(result).not.toMatch(/^title:/m);
40+
expect(result.endsWith('\n')).toBe(true);
41+
expect(result.endsWith('\n\n')).toBe(false);
3742
expect(result).toContain('## Authoring Guidelines');
3843
expect((SkillsService.getasToolMethod<unknown>).metadata.name).toBe('get');
3944
expect((SkillsService.getasToolMethod<unknown>).metadata.command).toBe('get');
@@ -43,6 +48,7 @@ description: "Best practices and workflow guidance for authoring UI with NVIDIA
4348
it('should get a skill context as json',async()=>{
4449
constresult=(awaitSkillsService.get({name: 'elements',format: 'json'}))asSkill;
4550
expect(result.name).toBe('elements');
51+
expect(result.title).toBe('NVIDIA Elements Design System (nve)');
4652
expect(result.context).toContain('Building UI with NVIDIA Elements');
4753
});
4854

‎projects/internals/tools/src/skills/service.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import{service,tool}from'../internal/tools.js';
55
import{markdownDescription}from'../internal/utils.js';
66
import{skills,typeSkill}from'./registry.js';
7-
import{formatSkillMarkdown}from'./markdown.js';
7+
import{formatSkillMarkdown}from'./utils.js';
88

99
typeOutputFormat='markdown'|'json';
1010

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import{createHash}from'node:crypto';
5+
import{promisesasfsp}from'node:fs';
6+
import{tmpdir}from'node:os';
7+
importnodePathfrom'node:path';
8+
import{afterEach,describe,expect,it}from'vitest';
9+
import{skills,typeSkill}from'./registry.js';
10+
import{
11+
AGENT_SKILLS_DISCOVERY_SCHEMA,
12+
createAgentSkillArtifacts,
13+
validateSkillDescription,
14+
validateSkillName,
15+
writeAgentSkillArtifacts
16+
}from'./utils.js';
17+
18+
consttemporaryDirectories: string[]=[];
19+
20+
functioncreateSkill(name: string,overrides: Partial<Skill>={}): Skill{
21+
return{
22+
name,
23+
title: `${name} title`,
24+
description: `${name} description`,
25+
context: `# ${name}\n\n${name} context`,
26+
...overrides
27+
};
28+
}
29+
30+
asyncfunctioncreateTemporaryDirectory(){
31+
constdirectory=awaitfsp.mkdtemp(nodePath.join(tmpdir(),'elements-agent-skills-'));
32+
temporaryDirectories.push(directory);
33+
returndirectory;
34+
}
35+
36+
afterEach(async()=>{
37+
awaitPromise.all(
38+
temporaryDirectories.splice(0).map(directory=>fsp.rm(directory,{recursive: true,force: true}))
39+
);
40+
});
41+
42+
describe('validateSkillName',()=>{
43+
it.each(['a','elements','skill-1','a'.repeat(64)])('should accept valid name %s',name=>{
44+
expect(()=>validateSkillName(name)).not.toThrow();
45+
});
46+
47+
it.each([undefined,null,1,'','Invalid','-invalid','invalid-','invalid--name','invalid_name','a'.repeat(65)])(
48+
'should reject invalid name %s',
49+
name=>{
50+
expect(()=>validateSkillName(name)).toThrow(/InvalidAgentSkillname/);
51+
}
52+
);
53+
});
54+
55+
describe('validateSkillDescription',()=>{
56+
it.each(['a','a'.repeat(1024)])('should accept a description between 1 and 1,024 characters',description=>{
57+
expect(()=>validateSkillDescription('elements',description)).not.toThrow();
58+
});
59+
60+
it.each([undefined,null,1,'',' ','a'.repeat(1025)])('should reject invalid description %s',description=>{
61+
expect(()=>validateSkillDescription('elements',description)).toThrow(
62+
/InvalidAgentSkilldescriptionfor"elements"/
63+
);
64+
});
65+
});
66+
67+
describe('createAgentSkillArtifacts',()=>{
68+
it('should create deterministic discovery 0.2 entries',()=>{
69+
constregistry=[createSkill('zeta'),createSkill('alpha')];
70+
constartifacts=createAgentSkillArtifacts(registry);
71+
72+
expect(artifacts.index.$schema).toBe(AGENT_SKILLS_DISCOVERY_SCHEMA);
73+
expect(artifacts.index.skills.map(skill=>skill.name)).toEqual(['alpha','zeta']);
74+
expect(artifacts.index.skills.every(skill=>skill.type==='skill-md')).toBe(true);
75+
expect(artifacts.index.skills.map(skill=>skill.url)).toEqual(['alpha/SKILL.md','zeta/SKILL.md']);
76+
expect(createAgentSkillArtifacts([...registry].reverse())).toEqual(artifacts);
77+
});
78+
79+
it('should hash the exact generated Markdown bytes',()=>{
80+
constartifacts=createAgentSkillArtifacts();
81+
82+
expect(artifacts.index.skills.map(entry=>entry.name)).toEqual(skills.map(skill=>skill.name).sort());
83+
artifacts.index.skills.forEach(entry=>{
84+
constmarkdown=artifacts.files.get(entry.url);
85+
expect(markdown).toBeDefined();
86+
if(!markdown)return;
87+
expect(entry.digest).toBe(`sha256:${createHash('sha256').update(Buffer.from(markdown,'utf8')).digest('hex')}`);
88+
});
89+
});
90+
91+
it('should generate standard frontmatter with the registry context',()=>{
92+
constelements=skills.find(skill=>skill.name==='elements');
93+
expect(elements).toBeDefined();
94+
if(!elements)return;
95+
96+
constartifacts=createAgentSkillArtifacts([elements]);
97+
constmarkdown=artifacts.files.get('elements/SKILL.md');
98+
99+
expect(markdown).toMatch(
100+
/^---\nname:"elements"\ndescription:".+"\nlicense:"Apache-2.0"\nmetadata:\ntitle:"NVIDIAElementsDesignSystem\(nve\)"\n---\n/
101+
);
102+
expect(markdown).not.toMatch(/^title:/m);
103+
expect(markdown).toContain(elements.context.trim());
104+
expect(markdown?.endsWith('\n')).toBe(true);
105+
expect(markdown?.endsWith('\n\n')).toBe(false);
106+
});
107+
108+
it('should publish conditional skills supplied by the registry',()=>{
109+
constartifacts=createAgentSkillArtifacts([createSkill('playground')]);
110+
111+
expect(artifacts.index.skills.map(skill=>skill.name)).toEqual(['playground']);
112+
expect(artifacts.files.has('playground/SKILL.md')).toBe(true);
113+
});
114+
115+
it('should reject duplicate skill names',()=>{
116+
expect(()=>createAgentSkillArtifacts([createSkill('duplicate'),createSkill('duplicate')])).toThrow(
117+
'Duplicate Agent Skill name "duplicate".'
118+
);
119+
});
120+
});
121+
122+
describe('writeAgentSkillArtifacts',()=>{
123+
it('should write the index and skill directory tree',async()=>{
124+
constpublicOutputPath=awaitcreateTemporaryDirectory();
125+
awaitwriteAgentSkillArtifacts(publicOutputPath,[createSkill('alpha'),createSkill('beta')]);
126+
constoutputPath=nodePath.join(publicOutputPath,'.well-known','agent-skills');
127+
128+
constindex=JSON.parse(awaitfsp.readFile(nodePath.join(outputPath,'index.json'),'utf8'));
129+
expect(index.skills).toEqual([
130+
expect.objectContaining({name: 'alpha'}),
131+
expect.objectContaining({name: 'beta'})
132+
]);
133+
awaitexpect(fsp.readFile(nodePath.join(outputPath,'alpha','SKILL.md'),'utf8')).resolves.toContain(
134+
'name: "alpha"'
135+
);
136+
awaitexpect(fsp.readFile(nodePath.join(outputPath,'beta','SKILL.md'),'utf8')).resolves.toContain(
137+
'name: "beta"'
138+
);
139+
expect((awaitfsp.readFile(nodePath.join(outputPath,'index.json'),'utf8')).endsWith('\n')).toBe(true);
140+
});
141+
142+
it('should remove stale skills before writing',async()=>{
143+
constpublicOutputPath=awaitcreateTemporaryDirectory();
144+
constoutputPath=nodePath.join(publicOutputPath,'.well-known','agent-skills');
145+
awaitwriteAgentSkillArtifacts(publicOutputPath,[createSkill('current'),createSkill('stale')]);
146+
awaitwriteAgentSkillArtifacts(publicOutputPath,[createSkill('current')]);
147+
148+
awaitexpect(fsp.stat(nodePath.join(outputPath,'stale'))).rejects.toMatchObject({code: 'ENOENT'});
149+
awaitexpect(fsp.readFile(nodePath.join(outputPath,'current','SKILL.md'),'utf8')).resolves.toBeDefined();
150+
});
151+
});

0 commit comments

Comments
 (0)