Skip to content
This repository was archived by the owner on Apr 18, 2026. It is now read-only.

Commit d4863c9

Browse files
committed
Fix Windows git sources and NuGet packaging
1 parent 41c1bbc commit d4863c9

13 files changed

Lines changed: 165 additions & 47 deletions

File tree

Directory.Build.props

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,19 @@
66
<ImplicitUsings>enable</ImplicitUsings>
77
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
88
</PropertyGroup>
9+
10+
<PropertyGroup Condition="'$(IsPackable)' != 'false'">
11+
<Authors>Alexander Nachtmann</Authors>
12+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
13+
<PackageProjectUrl>https://github.com/ANcpLua/netagents</PackageProjectUrl>
14+
<RepositoryUrl>https://github.com/ANcpLua/netagents.git</RepositoryUrl>
15+
<RepositoryType>git</RepositoryType>
16+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
17+
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
18+
<PackageReadmeFile>README.md</PackageReadmeFile>
19+
</PropertyGroup>
20+
21+
<ItemGroup Condition="'$(IsPackable)' != 'false' and Exists('$(MSBuildThisFileDirectory)README.md')">
22+
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" Visible="false"/>
23+
</ItemGroup>
924
</Project>

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# netagents
2+
3+
`netagents` publishes a small set of packages for managing `.agents` skill repositories and building compile-time MCP servers in .NET.
4+
5+
## Packages
6+
7+
- `NetAgents`: a .NET tool for initializing, installing, syncing, and trusting `.agents` directories.
8+
- `Qyl.Agents.Abstractions`: `[McpServer]` and `[Tool]` attributes used by the generator.
9+
- `Qyl.Agents.Generator`: incremental source generator that emits MCP dispatch, metadata, schema, and telemetry glue.
10+
- `Qyl.Agents`: runtime protocol and hosting helpers for generated servers.
11+
12+
## Install
13+
14+
```bash
15+
dotnet tool install --global NetAgents
16+
dotnet add package Qyl.Agents.Abstractions
17+
dotnet add package Qyl.Agents.Generator
18+
dotnet add package Qyl.Agents
19+
```
20+
21+
## Quick Start
22+
23+
```csharp
24+
using Qyl.Agents;
25+
26+
[McpServer("calc-server")]
27+
public partial class CalcServer
28+
{
29+
[Tool]
30+
public int Add(int a, int b) => a + b;
31+
}
32+
```
33+
34+
The generator produces the MCP-facing dispatch and metadata at build time. The runtime package provides the protocol host and handler used to serve generated MCP servers.
35+
36+
For repository management, initialize a project with:
37+
38+
```bash
39+
netagents init
40+
netagents add getsentry/dotagents
41+
netagents install
42+
```
43+
44+
## Repository
45+
46+
- Source: https://github.com/ANcpLua/netagents
47+
- License: MIT

src/NetAgents/Cli/Commands/AddCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public static async Task<AddResult> RunAddAsync(AddOptions opts, CancellationTok
6767
var sourceForStorage = parsed.Ref is not null
6868
? specifier[..^(parsed.Ref.Length + 1)]
6969
: specifier;
70+
sourceForStorage = SkillResolver.NormalizeGitSourceForStorage(sourceForStorage);
7071

7172
TrustValidator.ValidateTrustedSource(hintedSpecifier, config.Trust);
7273

src/NetAgents/NetAgents.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<ToolCommandName>netagents</ToolCommandName>
66
<PublishAot Condition="'$(Configuration)' == 'Release'">true</PublishAot>
77
<RootNamespace>NetAgents</RootNamespace>
8+
<Description>Dotnet tool for bootstrapping, installing, syncing, and trusting .agents skill repositories.</Description>
9+
<PackageTags>netagents;agents;mcp;skills;dotnet-tool</PackageTags>
810
</PropertyGroup>
911

1012
<ItemGroup>

src/NetAgents/Skills/SkillResolver.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ public static string ApplyDefaultRepositorySource(
100100
return $"https://{host}/{owner}/{repo}{refSuffix}";
101101
}
102102

103+
public static string NormalizeGitSourceForStorage(string specifier)
104+
{
105+
if (!specifier.StartsWith("git:", StringComparison.Ordinal))
106+
return specifier;
107+
108+
var raw = specifier[4..];
109+
if (!Path.IsPathRooted(raw))
110+
return specifier;
111+
112+
return $"git:{new Uri(Path.GetFullPath(raw)).AbsoluteUri}";
113+
}
114+
103115
/// <summary>
104116
/// Parse a source string into its components.
105117
/// </summary>

src/NetAgents/Sources/SkillCache.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
namespace NetAgents.Sources;
22

3+
using System.Security.Cryptography;
4+
using System.Text;
5+
36
public sealed record CacheResult(string RepoDir, string Commit);
47

58
public static class SkillCache
@@ -22,9 +25,7 @@ public static async Task<CacheResult> EnsureCachedAsync(
2225
{
2326
var stateDir = Environment.GetEnvironmentVariable("NETAGENTS_STATE_DIR") ?? DefaultStateDir;
2427
var effectiveTtl = ttl ?? DefaultTtl;
25-
// Ensure cacheKey is relative so Path.Combine doesn't discard the stateDir
26-
var safeCacheKey = cacheKey.TrimStart(Path.DirectorySeparatorChar).TrimStart(Path.AltDirectorySeparatorChar);
27-
var repoDir = Path.Combine(stateDir, safeCacheKey);
28+
var repoDir = Path.Combine(stateDir, GetCacheDirectoryName(cacheKey));
2829

2930
if (GitSource.IsGitRepo(repoDir))
3031
{
@@ -67,4 +68,20 @@ private static bool IsStale(string repoDir, TimeSpan ttl)
6768
return true;
6869
}
6970
}
71+
72+
private static string GetCacheDirectoryName(string cacheKey)
73+
{
74+
var normalized = cacheKey.Replace('\\', '/').Trim().Trim('/');
75+
var readable = normalized.Length == 0
76+
? "repo"
77+
: new string(normalized.Select(ch => char.IsLetterOrDigit(ch) ? ch : '-').ToArray()).Trim('-');
78+
79+
if (readable.Length == 0)
80+
readable = "repo";
81+
if (readable.Length > 48)
82+
readable = readable[..48].Trim('-');
83+
84+
var hash = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(cacheKey)));
85+
return $"{readable}-{hash[..12]}";
86+
}
7087
}

src/NetAgents/Trust/TrustValidator.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public static void ValidateTrustedSource(string source, TrustConfig? trust)
4949
if (parsed.Type == SourceType.Local)
5050
return;
5151

52+
if (parsed.Type == SourceType.Git && parsed.Url is not null && IsLocalGitUrl(parsed.Url))
53+
return;
54+
5255
if (parsed.Type == SourceType.Github)
5356
{
5457
var owner = parsed.Owner!.ToLowerInvariant();
@@ -127,6 +130,12 @@ private static ParsedSource ParseSource(string source)
127130
return new ParsedSource(SourceType.Git, null, null, source);
128131
}
129132

133+
private static bool IsLocalGitUrl(string url)
134+
{
135+
return Path.IsPathRooted(url) ||
136+
Uri.TryCreate(url, UriKind.Absolute, out var parsed) && parsed.IsFile;
137+
}
138+
130139
// ── Source parsing (mirrors dotagents parseSource for trust checks) ───────
131140

132141
private enum SourceType

tests/NetAgents.Tests/Cli/AddCommandTests.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace NetAgents.Tests.Cli;
22

3+
using NetAgents.Tests;
34
using NetAgents.Cli.Commands;
45
using Utils;
56
using Xunit;
@@ -16,7 +17,7 @@ public TempDir()
1617

1718
public void Dispose()
1819
{
19-
if (Directory.Exists(Path)) Directory.Delete(Path, true);
20+
TestWorkspace.DeleteDirectory(Path);
2021
}
2122
}
2223

@@ -49,7 +50,7 @@ private static async Task<string> CreateRepo(string parentDir, CancellationToken
4950

5051
await ProcessRunner.RunAsync("git", ["add", "."], repoDir, ct: ct);
5152
await ProcessRunner.RunAsync("git", ["commit", "-m", "initial"], repoDir, ct: ct);
52-
return repoDir;
53+
return TestWorkspace.ToGitSource(repoDir);
5354
}
5455

5556
[Fact]
@@ -62,7 +63,7 @@ public async Task AddsSingleSkill_ViaNames()
6263
try
6364
{
6465
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
65-
var result = await AddCommand.RunAddAsync(new AddOptions(scope, $"git:{repoDir}", Names: ["pdf"]), CT);
66+
var result = await AddCommand.RunAddAsync(new AddOptions(scope, repoDir, Names: ["pdf"]), CT);
6667

6768
Assert.Equal("pdf", result.SingleName);
6869
var toml = await File.ReadAllTextAsync(Path.Combine(project, "agents.toml"), CT);
@@ -85,7 +86,7 @@ public async Task AddsMultipleSkills_ViaNames()
8586
{
8687
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
8788
var result = await AddCommand.RunAddAsync(
88-
new AddOptions(scope, $"git:{repoDir}", Names: ["pdf", "review"]), CT);
89+
new AddOptions(scope, repoDir, Names: ["pdf", "review"]), CT);
8990

9091
Assert.NotNull(result.MultipleNames);
9192
Assert.Contains("pdf", result.MultipleNames);
@@ -108,7 +109,7 @@ public async Task ThrowsWhenSkillNotFound()
108109
{
109110
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
110111
await Assert.ThrowsAsync<AddException>(() =>
111-
AddCommand.RunAddAsync(new AddOptions(scope, $"git:{repoDir}", Names: ["nonexistent"]), CT));
112+
AddCommand.RunAddAsync(new AddOptions(scope, repoDir, Names: ["nonexistent"]), CT));
112113
}
113114
finally
114115
{
@@ -124,13 +125,13 @@ public async Task ThrowsWhenSkillAlreadyExists()
124125
Directory.CreateDirectory(Path.Combine(project, ".agents", "skills"));
125126
var repoDir = await CreateRepo(tmp.Path, CT, "pdf");
126127
File.WriteAllText(Path.Combine(project, "agents.toml"),
127-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n");
128+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n");
128129
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
129130
try
130131
{
131132
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
132133
var ex = await Assert.ThrowsAsync<AddException>(() =>
133-
AddCommand.RunAddAsync(new AddOptions(scope, $"git:{repoDir}", Names: ["pdf"]), CT));
134+
AddCommand.RunAddAsync(new AddOptions(scope, repoDir, Names: ["pdf"]), CT));
134135
Assert.Contains("already exists", ex.Message);
135136
}
136137
finally
@@ -150,7 +151,7 @@ public async Task ThrowsWhenAllUsedWithNames()
150151
{
151152
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
152153
await Assert.ThrowsAsync<AddException>(() =>
153-
AddCommand.RunAddAsync(new AddOptions(scope, $"git:{repoDir}", Names: ["pdf"], All: true), CT));
154+
AddCommand.RunAddAsync(new AddOptions(scope, repoDir, Names: ["pdf"], All: true), CT));
154155
}
155156
finally
156157
{
@@ -177,7 +178,7 @@ public async Task AutoSelectsSingleSkillRepo()
177178
try
178179
{
179180
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
180-
var result = await AddCommand.RunAddAsync(new AddOptions(scope, $"git:{singleRepo}"), CT);
181+
var result = await AddCommand.RunAddAsync(new AddOptions(scope, TestWorkspace.ToGitSource(singleRepo)), CT);
181182

182183
Assert.Equal("only-skill", result.SingleName);
183184
}
@@ -198,7 +199,7 @@ public async Task ThrowsInNonInteractiveMode_WithMultipleSkills()
198199
{
199200
var scope = ScopeResolver.ResolveScope(ScopeKind.Project, project);
200201
var ex = await Assert.ThrowsAsync<AddException>(() =>
201-
AddCommand.RunAddAsync(new AddOptions(scope, $"git:{repoDir}"), CT));
202+
AddCommand.RunAddAsync(new AddOptions(scope, repoDir), CT));
202203
Assert.Contains("Multiple skills found", ex.Message);
203204
}
204205
finally

tests/NetAgents.Tests/Cli/InstallCommandTests.cs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace NetAgents.Tests.Cli;
22

3+
using NetAgents.Tests;
34
using NetAgents.Cli.Commands;
45
using NetAgents.Lockfile;
56
using Utils;
@@ -17,10 +18,7 @@ public TempDir()
1718

1819
public void Dispose()
1920
{
20-
if (!Directory.Exists(Path)) return;
21-
foreach (var info in new DirectoryInfo(Path).EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
22-
info.Attributes = FileAttributes.Normal;
23-
Directory.Delete(Path, true);
21+
TestWorkspace.DeleteDirectory(Path);
2422
}
2523
}
2624

@@ -46,7 +44,7 @@ public static async Task<string> CreateRepoWithSkills(string parentDir, Cancella
4644

4745
await ProcessRunner.RunAsync("git", ["add", "."], repoDir, ct: ct);
4846
await ProcessRunner.RunAsync("git", ["commit", "-m", "initial"], repoDir, ct: ct);
49-
return repoDir;
47+
return TestWorkspace.ToGitSource(repoDir);
5048
}
5149
}
5250

@@ -69,7 +67,7 @@ public async Task InstallsSkillFromGitSource()
6967
SetupProject(project);
7068
var repoDir = await GitHelper.CreateRepoWithSkills(tmp.Path, CT, "pdf");
7169
File.WriteAllText(Path.Combine(project, "agents.toml"),
72-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n");
70+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n");
7371
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
7472
try
7573
{
@@ -93,7 +91,7 @@ public async Task CreatesLockfileAfterInstall()
9391
SetupProject(project);
9492
var repoDir = await GitHelper.CreateRepoWithSkills(tmp.Path, CT, "pdf");
9593
File.WriteAllText(Path.Combine(project, "agents.toml"),
96-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n");
94+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n");
9795
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
9896
try
9997
{
@@ -132,7 +130,7 @@ public async Task FailsWithFrozen_WhenNoLockfile()
132130
SetupProject(project);
133131
var repoDir = await GitHelper.CreateRepoWithSkills(tmp.Path, CT, "pdf");
134132
File.WriteAllText(Path.Combine(project, "agents.toml"),
135-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n");
133+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n");
136134
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
137135
try
138136
{
@@ -154,7 +152,7 @@ public async Task FrozenMode_PassesWhenLockfileMatches()
154152
SetupProject(project);
155153
var repoDir = await GitHelper.CreateRepoWithSkills(tmp.Path, CT, "pdf");
156154
File.WriteAllText(Path.Combine(project, "agents.toml"),
157-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n");
155+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n");
158156
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
159157
try
160158
{
@@ -199,7 +197,7 @@ public async Task InstallsAllSkillsFromWildcard()
199197
SetupProject(project);
200198
var repoDir = await GitHelper.CreateRepoWithSkills(tmp.Path, CT, "pdf", "skills/review");
201199
File.WriteAllText(Path.Combine(project, "agents.toml"),
202-
$"version = 1\n\n[[skills]]\nname = \"*\"\nsource = \"git:{repoDir}\"\n");
200+
$"version = 1\n\n[[skills]]\nname = \"*\"\nsource = \"{repoDir}\"\n");
203201
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
204202
try
205203
{
@@ -223,7 +221,7 @@ public async Task WildcardRespectsExcludeList()
223221
SetupProject(project);
224222
var repoDir = await GitHelper.CreateRepoWithSkills(tmp.Path, CT, "pdf", "skills/review");
225223
File.WriteAllText(Path.Combine(project, "agents.toml"),
226-
$"version = 1\n\n[[skills]]\nname = \"*\"\nsource = \"git:{repoDir}\"\nexclude = [\"review\"]\n");
224+
$"version = 1\n\n[[skills]]\nname = \"*\"\nsource = \"{repoDir}\"\nexclude = [\"review\"]\n");
227225
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
228226
try
229227
{

tests/NetAgents.Tests/Cli/RemoveCommandTests.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
namespace NetAgents.Tests.Cli;
22

3+
using NetAgents.Tests;
34
using NetAgents.Cli.Commands;
45
using NetAgents.Config;
56
using NetAgents.Lockfile;
@@ -18,10 +19,7 @@ public TempDir()
1819

1920
public void Dispose()
2021
{
21-
if (!Directory.Exists(Path)) return;
22-
foreach (var info in new DirectoryInfo(Path).EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
23-
info.Attributes = FileAttributes.Normal;
24-
Directory.Delete(Path, true);
22+
TestWorkspace.DeleteDirectory(Path);
2523
}
2624
}
2725

@@ -47,7 +45,7 @@ private static async Task<string> CreateRepo(string parentDir, CancellationToken
4745

4846
await ProcessRunner.RunAsync("git", ["add", "."], repoDir, ct: ct);
4947
await ProcessRunner.RunAsync("git", ["commit", "-m", "initial"], repoDir, ct: ct);
50-
return repoDir;
48+
return TestWorkspace.ToGitSource(repoDir);
5149
}
5250

5351
[Fact]
@@ -58,7 +56,7 @@ public async Task RemovesExplicitSkillEntry()
5856
Directory.CreateDirectory(Path.Combine(project, ".agents", "skills"));
5957
var repoDir = await CreateRepo(tmp.Path, CT, "pdf");
6058
File.WriteAllText(Path.Combine(project, "agents.toml"),
61-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n");
59+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n");
6260
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
6361
try
6462
{
@@ -103,7 +101,7 @@ public async Task ReturnsWildcardResult_ForWildcardSourced()
103101
Directory.CreateDirectory(Path.Combine(project, ".agents", "skills"));
104102
var repoDir = await CreateRepo(tmp.Path, CT, "pdf");
105103
File.WriteAllText(Path.Combine(project, "agents.toml"),
106-
$"version = 1\n\n[[skills]]\nname = \"*\"\nsource = \"git:{repoDir}\"\n");
104+
$"version = 1\n\n[[skills]]\nname = \"*\"\nsource = \"{repoDir}\"\n");
107105
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
108106
try
109107
{
@@ -130,7 +128,7 @@ public async Task RemovesExplicitEntry_EvenWhenWildcardExists()
130128
Directory.CreateDirectory(Path.Combine(project, ".agents", "skills"));
131129
var repoDir = await CreateRepo(tmp.Path, CT, "pdf", "skills/review");
132130
File.WriteAllText(Path.Combine(project, "agents.toml"),
133-
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"git:{repoDir}\"\n\n[[skills]]\nname = \"*\"\nsource = \"git:{repoDir}\"\n");
131+
$"version = 1\n\n[[skills]]\nname = \"pdf\"\nsource = \"{repoDir}\"\n\n[[skills]]\nname = \"*\"\nsource = \"{repoDir}\"\n");
134132
Environment.SetEnvironmentVariable("NETAGENTS_STATE_DIR", Path.Combine(tmp.Path, "state"));
135133
try
136134
{

0 commit comments

Comments
 (0)