Skip to content

Commit ee349f5

Browse files
committed
feat: skills
1 parent 73e7e0e commit ee349f5

File tree

12 files changed

+1922
-19
lines changed

12 files changed

+1922
-19
lines changed

YCode.CLI/Program.cs

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
var WORKDIR = Directory.GetCurrentDirectory();
1717

18+
var SKILLDIR = Path.Combine(WORKDIR, "skills");
19+
1820
var AGENTS = new Dictionary<string, JsonObject>()
1921
{
2022
["explore"] = new JsonObject
@@ -45,23 +47,6 @@
4547
["plan"] = ("📋", "yellow")
4648
};
4749

48-
var SYSTEM = $"""
49-
"You are a coding agent operating INSIDE the user's repository at {WORKDIR}.\n"
50-
"Follow this loop strictly: plan briefly → use TOOLS to act directly on files/shell → report concise results.\n"
51-
"Rules:\n"
52-
"- Prefer taking actions with tools (read/write/edit/bash) over long prose.\n"
53-
"- Keep outputs terse. Use bullet lists / checklists when summarizing.\n"
54-
"- Use Task tool for subtasks that need focused exploration or implementation.\n"
55-
"- Never invent file paths. Ask via reads or list directories first if unsure.\n"
56-
"- For edits, apply the smallest change that satisfies the request.\n"
57-
"- For bash, avoid destructive or privileged commands; stay inside the workspace.\n"
58-
"- Use the Todo tool to maintain multi-step plans when needed.\n"
59-
"- After finishing, summarize what changed and how to run or test."
60-
61-
"Task:\n"
62-
{GetAgentDescription()}
63-
""";
64-
6550
var INITIAL_REMINDER = $"""
6651
'<reminder source="system" topic="todos">'
6752
"System message: complex work should be tracked with the Todo tool. "
@@ -95,10 +80,34 @@
9580

9681
var mcp = new McpManager(WORKDIR);
9782

83+
var skills = new SkillsManager(SKILLDIR);
84+
85+
var SYSTEM = $"""
86+
"You are a coding agent operating INSIDE the user's repository at {WORKDIR}.\n"
87+
"Follow this loop strictly: plan briefly → use TOOLS to act directly on files/shell → report concise results.\n"
88+
"Rules:\n"
89+
"- Prefer taking actions with tools (read/write/edit/bash) over long prose.\n"
90+
"- Keep outputs terse. Use bullet lists / checklists when summarizing.\n"
91+
"- Use Skill tool IMMEDIATELY when a task matches a skill description.\n"
92+
"- Use Task tool for subtasks that need focused exploration or implementation.\n"
93+
"- Never invent file paths. Ask via reads or list directories first if unsure.\n"
94+
"- For edits, apply the smallest change that satisfies the request.\n"
95+
"- For bash, avoid destructive or privileged commands; stay inside the workspace.\n"
96+
"- Use the Todo tool to maintain multi-step plans when needed.\n"
97+
"- After finishing, summarize what changed and how to run or test."
98+
99+
"Task:\n"
100+
{GetAgentDescription()}
101+
102+
"Skills available (invoke with Skill tool when task matches)::\n"
103+
{skills.GetDescription()}
104+
""";
105+
98106
var tools = await mcp.Regist(
99107
(RunTodoUpdate, "TodoWriter", null),
100108
(RunToTask, "Task", $$"""
101-
{"name": "Task",
109+
{
110+
"name": "Task",
102111
"description": "Spawn a subagent for a focused subtask. Subagents run in ISOLATED context - they don't see parent's history. Use this to keep the main conversation clean. \n Agent types: \n {{GetAgentDescription()}} \n Example uses:\n - Task(explore): \"Find all files using the auth module.\"\n - Task(plan): \"Design a migration strategy for the database\"\n - Task(code): \"Implement the user registration form\"\n ",
103112
"arguments": {
104113
"type": "object",
@@ -110,6 +119,19 @@
110119
"required": ["description", "prompt", "agent_type"],
111120
}
112121
}
122+
"""),
123+
(RunToSkill, "Skill", $$"""
124+
{
125+
"name": "Skill",
126+
"description": "Load a skill to gain specialized knowledge for a task. Available skills: \n {{skills.GetDescription()}} \n When to use:\n - IMMEDIATELY when user task matches a skill description.\n - Before attempting domain-specific work. (PDF, MCP, etc.)\n The skill content will be injected into the conversation, giving you detailed instructions and access to resources.",
127+
"arguments": {
128+
"type": "object",
129+
"properties": {
130+
"skill": { "type": "string", "description": "Name of the skill to load." },
131+
},
132+
"required": ["skill"],
133+
}
134+
}
113135
"""));
114136

115137
var agent = new OpenAIClient(
@@ -413,6 +435,26 @@ You are a {agentType} subagent operating INSIDE the user's repository at {WORKDI
413435
return "(subagent returned no text)";
414436
}
415437

438+
string RunToSkill(string skillName)
439+
{
440+
var content = skills.GetSkillContent(skillName);
441+
442+
if (String.IsNullOrWhiteSpace(content))
443+
{
444+
var available = String.Join(',', skills.GetSkills()) ?? "none";
445+
446+
return $"Error: Unknown skill '{skillName}'. Available: {available}";
447+
}
448+
449+
return $"""
450+
<skill-loaded name="{skillName}">
451+
{content}
452+
</skill-loaded>
453+
454+
Follow the instructions in the skill above to complete the user's task.
455+
""";
456+
}
457+
416458
string GetAgentDescription()
417459
{
418460
return String.Join("\n", AGENTS.Select(x => $"- {x.Key}: {x.Value["description"]}"));
@@ -598,7 +640,6 @@ string EscapeMarkup(string text)
598640
.Replace("]", "]]");
599641
}
600642

601-
602643
void ShowToolSpinner(string toolName)
603644
{
604645
AnsiConsole.Markup($"[yellow]>[/] [dim]{EscapeMarkup(toolName)} executing...[/] ");

YCode.CLI/SkillsManager.cs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System.Text.Json.Nodes;
2+
using System.Text.RegularExpressions;
3+
using YamlDotNet.Serialization;
4+
5+
namespace YCode.CLI
6+
{
7+
internal class SkillsManager
8+
{
9+
private readonly IDeserializer _builder;
10+
private readonly string _skillsDir;
11+
private readonly Dictionary<string, JsonObject> _skills;
12+
13+
public SkillsManager(string skillsDir)
14+
{
15+
_skillsDir = skillsDir;
16+
17+
_skills = [];
18+
19+
_builder = new DeserializerBuilder().Build();
20+
21+
this.LoadSkills();
22+
}
23+
24+
private void LoadSkills()
25+
{
26+
if (!Directory.Exists(_skillsDir))
27+
{
28+
return;
29+
}
30+
31+
var skill_dir = new DirectoryInfo(_skillsDir);
32+
33+
foreach (var dir in skill_dir.GetDirectories())
34+
{
35+
if (!dir.Exists)
36+
{
37+
continue;
38+
}
39+
40+
var md = Path.Combine(dir.FullName, "SKILL.md");
41+
42+
if (!File.Exists(md))
43+
{
44+
continue;
45+
}
46+
47+
var skill = this.ParseSkill(md);
48+
49+
if (skill != null && skill.ContainsKey("name"))
50+
{
51+
_skills[skill["name"]?.ToString()!] = skill;
52+
}
53+
}
54+
}
55+
56+
private JsonObject? ParseSkill(string path)
57+
{
58+
var content = File.ReadAllText(path);
59+
60+
var match = Regex.Match(content, @"^---\s*\n(.*?)\n---\s*\n(.*)$", RegexOptions.Singleline);
61+
62+
if (!match.Success || match.Groups.Count < 2)
63+
{
64+
return null;
65+
}
66+
67+
var metadata = new JsonObject();
68+
69+
try
70+
{
71+
var yamlObject = _builder.Deserialize<Dictionary<string, object>>(match.Groups[1].Value);
72+
73+
foreach (var kvp in yamlObject)
74+
{
75+
metadata[kvp.Key] = kvp.Value?.ToString() ?? "";
76+
}
77+
}
78+
catch (Exception ex)
79+
{
80+
Console.WriteLine($"YAML解析错误: {ex.Message}");
81+
}
82+
83+
if (!metadata.ContainsKey("name") || !metadata.ContainsKey("description"))
84+
{
85+
return null;
86+
}
87+
88+
return new JsonObject
89+
{
90+
["name"] = metadata["name"]?.ToString(),
91+
["description"] = metadata["description"]?.ToString(),
92+
["body"] = match.Groups[2].Value,
93+
["path"] = path,
94+
["dir"] = Path.GetDirectoryName(path)
95+
};
96+
}
97+
98+
public string GetDescription()
99+
{
100+
if (_skills.Count == 0)
101+
{
102+
return "(no skills available)";
103+
}
104+
105+
return String.Join("\n", _skills.Values.Select(skill => $"- {skill["name"]}: {skill["description"]}"));
106+
}
107+
108+
public string GetSkillContent(string name)
109+
{
110+
if (!_skills.ContainsKey(name))
111+
{
112+
return String.Empty;
113+
}
114+
115+
var skill = _skills[name];
116+
117+
var content = $"# Skill: {skill["name"]}\n\n{skill["body"]}";
118+
119+
List<string> resources = [];
120+
121+
var supports = new Dictionary<string, string>()
122+
{
123+
{ "scripts", "Scripts" },
124+
{ "references", "References" },
125+
{ "assets", "Assets" }
126+
};
127+
128+
foreach (var kvp in supports)
129+
{
130+
var folder_path = Path.Combine(skill["dir"]?.ToString()!, kvp.Key);
131+
132+
if (Path.Exists(folder_path))
133+
{
134+
var files = Directory.GetFiles(folder_path, "*");
135+
136+
if (files.Length > 0)
137+
{
138+
resources.Add($"{kvp.Value}: {String.Join(',', files.Select(x => Path.GetFileName(x)))}");
139+
}
140+
}
141+
}
142+
143+
if (resources.Count > 0)
144+
{
145+
content += $"\n\n **Available resource in {skill["dir"]}:**\n";
146+
147+
content += String.Join("\n", resources.Select(r => $"- {r}"));
148+
}
149+
150+
return content;
151+
}
152+
153+
public string[] GetSkills()
154+
{
155+
return [.. _skills.Keys];
156+
}
157+
}
158+
}

YCode.CLI/YCode.CLI.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@
1919
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.0.0-preview.251016.1" />
2020
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
2121
<PackageReference Include="Spectre.Console" Version="0.53.0" />
22+
<PackageReference Include="YamlDotNet" Version="16.3.0" />
2223
</ItemGroup>
2324

2425
<ItemGroup>
2526
<Folder Include="nupkg\" />
2627
</ItemGroup>
2728

29+
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
30+
<Content Include="skills\**">
31+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
32+
</Content>
33+
</ItemGroup>
34+
2835
</Project>

0 commit comments

Comments
 (0)