Skip to content

Commit 78f4e5c

Browse files
author
Peter
committed
Merge feature/ai-help: Phase 3 --help-ai YAML help
2 parents 3f35606 + cf26bd6 commit 78f4e5c

20 files changed

Lines changed: 1535 additions & 21 deletions

Examples/CurlAi/CurlAi.csproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<AssemblyName>CurlAi</AssemblyName>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.6" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\..\src\KingpinNet\KingpinNet.csproj" />
15+
</ItemGroup>
16+
17+
</Project>

Examples/CurlAi/Program.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System;
2+
using KingpinNet;
3+
using Microsoft.Extensions.Configuration;
4+
5+
namespace CurlAi;
6+
7+
class Program
8+
{
9+
static void Main(string[] args)
10+
{
11+
Kingpin
12+
.Version("1.0")
13+
.Author("Peter Andersen")
14+
.ApplicationName("curl-ai")
15+
.Help("An example implementation of curl exercising the AI-friendly help surface.")
16+
.ExitOnHelp()
17+
.ExitOnParseErrors()
18+
.ShowHelpOnParsingErrors();
19+
20+
// Global flags — each value-taking flag declares a `unit`.
21+
Kingpin.Flag("timeout", "Set connection timeout.")
22+
.Short('t').IsInt().Unit("seconds").Default("5");
23+
Kingpin.Flag("headers", "Add HTTP headers to the request.")
24+
.Short('H').Unit("header");
25+
Kingpin.Flag("verbose", "Verbose request/response logging.")
26+
.Short('v').IsBool();
27+
// Destructive flag carries a `caution` describing the risk and root requirement.
28+
Kingpin.Flag("insecure", "Skip TLS certificate verification.")
29+
.Short('k').IsBool()
30+
.Caution("Disables TLS verification; only use against trusted hosts you control.");
31+
32+
var getCategory = Kingpin.Category("Get", "Category for all the GET commands:");
33+
var get = getCategory.Command("get", "GET a resource.").IsDefault();
34+
var getUrl = get.Command("url", "Retrieve a URL.").IsDefault();
35+
// Required positional argument — exercises required: true in the YAML.
36+
getUrl.Argument("url", "URL to GET.").IsRequired().IsUrl();
37+
38+
var getFile = get.Command("file", "Retrieve a file.");
39+
getFile.Argument("file", "File to retrieve.").IsRequired();
40+
41+
var post = getCategory.Command("post", "POST a resource.");
42+
post.Flag("data", "Key-value data to POST.").Short('d');
43+
post.Flag("retries", "Number of retries on failure.")
44+
.IsInt().Unit("count").Default("3");
45+
post.Argument("url", "URL to POST to.").IsRequired().IsUrl();
46+
47+
var list = Kingpin.Command("list", "LIST a resource.");
48+
list.Flag("interval", "Polling interval between LIST calls.")
49+
.IsFloat().Unit("seconds").Default("1.0");
50+
list.Argument("url", "URL to LIST.").IsRequired().IsUrl();
51+
52+
// Global AI-help sections (root-only by design).
53+
Kingpin
54+
.ExitCode(0, "Success")
55+
.ExitCode(1, "Network or protocol error")
56+
.ExitCode(2, "Invalid argument or unknown URL scheme");
57+
58+
Kingpin
59+
.Example("Fetch a URL", "curl-ai get url https://example.com")
60+
.Example("POST JSON to an endpoint",
61+
"curl-ai post --data=@payload.json https://api.example.com/v1/items")
62+
.Example("Poll a list endpoint every 2s",
63+
"curl-ai list --interval=2.0 https://example.com/items");
64+
65+
Kingpin
66+
.Note("All commands accept --timeout to bound network operations.")
67+
.Note("HTTP/HTTPS are supported; other schemes are rejected at parse time.");
68+
69+
Kingpin
70+
.Prefer("Always pass --timeout explicitly",
71+
"Non-interactive or scripted invocations",
72+
"Without --timeout, curl-ai will block indefinitely on dead peers and stall calling agents.")
73+
.Prefer("Quote URLs that contain & or ?",
74+
"Any invocation involving a query string",
75+
"Bare query strings are interpreted by the shell, not curl-ai.");
76+
77+
Kingpin
78+
.Avoid("Don't use --insecure",
79+
"Hitting a trusted endpoint with a known-bad certificate during development",
80+
"Disables TLS verification globally; opens the request to MITM attacks.")
81+
.Avoid("Don't pass shell-expanded glob patterns as URLs",
82+
"Genuinely fetching multiple URLs",
83+
"curl-ai accepts exactly one URL per invocation; globs become noise.");
84+
85+
ParseResult result;
86+
try
87+
{
88+
result = Kingpin.Parse(args);
89+
}
90+
catch (ParseException)
91+
{
92+
return;
93+
}
94+
95+
if (result.ParsingFailed)
96+
return;
97+
98+
var configuration = new ConfigurationBuilder().AddKingpinNetCommandLine(args).Build();
99+
switch (configuration["command"])
100+
{
101+
case "get:url":
102+
Console.WriteLine($"Getting URL {configuration["get:url:url"]}");
103+
break;
104+
case "post":
105+
Console.WriteLine($"Posting to URL {configuration["post:url"]}");
106+
break;
107+
case "list":
108+
Console.WriteLine($"Listing URL {configuration["list:url"]}");
109+
break;
110+
default:
111+
Console.WriteLine("Use --help or --help-ai to see available commands.");
112+
break;
113+
}
114+
}
115+
}

KingpinNet.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</Folder>
66
<Folder Name="/Examples/">
77
<Project Path="Examples/Curl/Curl.csproj" />
8+
<Project Path="Examples/CurlAi/CurlAi.csproj" />
89
<Project Path="Examples/Motif/Motif.csproj" />
910
<Project Path="Examples/Ping/Ping.csproj" />
1011
<Project Path="Examples/TestKingpinNet/TestKingpinNet.csproj" />

README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,111 @@ iex "$({Your-tool-executable} --suggestion-script-pwsh)"
130130

131131
After you run the script, you are able to have TAB auto complete on your tool.
132132

133+
## AI-friendly help (`--help-ai`)
134+
135+
KingpinNet apps emit a YAML description of their command tree designed for LLM agents to consume directly, alongside the human-rendered `--help`. Two global flags are registered automatically (parallel to `--help`) once `Initialize()` runs:
136+
137+
- `--help-ai` — writes the YAML to stdout.
138+
- `--help-ai-file` — writes a Markdown-wrapped copy to `<exename>-ai-help.md` next to the executable. Pass an explicit path with `--help-ai-file=/path/to/file.md` to override.
139+
140+
Both flags are available at the root and on every subcommand; invoking on a subcommand scopes the output to that subtree. The human `--help` output gains a single footer line — `For agents: run with --help-ai for a machine-readable description.` — so an LLM that reads `--help` first can discover the AI variant without external docs.
141+
142+
### Authoring the global sections
143+
144+
Five fluent methods on `KingpinApplication` (and the static `Kingpin` facade) populate the root-level sections of the YAML — each is emitted only when non-empty:
145+
146+
```csharp
147+
Kingpin
148+
.ExitCode(0, "Success")
149+
.ExitCode(1, "Network or protocol error")
150+
151+
.Example("Fetch a URL", "curl-ai get url https://example.com")
152+
153+
.Note("All commands accept --timeout to bound network operations.")
154+
155+
.Prefer(
156+
rule: "Always pass --timeout explicitly",
157+
when: "Non-interactive or scripted invocations",
158+
why: "Without --timeout, curl-ai blocks indefinitely on dead peers.")
159+
160+
.Avoid(
161+
rule: "Don't use --insecure",
162+
unless: "Hitting a trusted endpoint with a known-bad certificate during dev",
163+
why: "Disables TLS verification globally; opens the request to MITM.");
164+
```
165+
166+
Per-flag/argument, `.Unit("seconds")` declares what a value means and `.Caution("...")` surfaces destructive-flag warnings inline:
167+
168+
```csharp
169+
Kingpin.Flag("timeout", "Set connection timeout.")
170+
.Short('t').IsInt().Unit("seconds").Default("5");
171+
172+
Kingpin.Flag("insecure", "Skip TLS certificate verification.")
173+
.Short('k').IsBool()
174+
.Caution("Disables TLS verification; only use against trusted hosts you control.");
175+
```
176+
177+
### Sample output
178+
179+
Running the bundled `Examples/CurlAi` with `--help-ai` produces, in part:
180+
181+
```yaml
182+
command: curl-ai
183+
summary: An example implementation of curl exercising the AI-friendly help surface.
184+
synopsis: "curl-ai [commands] [flags]"
185+
186+
global_flags:
187+
- long: --timeout
188+
short: "-t"
189+
type: int
190+
takes_value: true
191+
unit: seconds
192+
default: "5"
193+
required: false
194+
help: Set connection timeout.
195+
- long: --insecure
196+
short: "-k"
197+
type: bool
198+
takes_value: false
199+
required: false
200+
help: Skip TLS certificate verification.
201+
caution: Disables TLS verification; only use against trusted hosts you control.
202+
203+
commands:
204+
- path: [get, url]
205+
summary: Retrieve a URL.
206+
synopsis: "curl-ai get url <arguments>"
207+
argument:
208+
- type: url
209+
takes_value: true
210+
required: true
211+
help: URL to GET.
212+
213+
exit_codes:
214+
0: Success
215+
1: Network or protocol error
216+
217+
prefer:
218+
- rule: Always pass --timeout explicitly
219+
when: Non-interactive or scripted invocations
220+
why: Without --timeout, curl-ai blocks indefinitely on dead peers.
221+
222+
conventions:
223+
flag_form: "--flag=value preferred; bare for booleans"
224+
inheritance: Commands inherit flags from each ancestor and from global_flags
225+
```
226+
227+
The full set of fields, the inheritance rule, and the format of every section are documented in `specs/roadmap.md` under "Phase 3 — AI-Friendly Help".
228+
133229
## What's New
134230

231+
### Phase 3 — AI-friendly help
232+
- New `--help-ai` and `--help-ai-file` global flags emit a YAML description of the command tree
233+
- Fluent API for declaring `ExitCode`, `Example`, `Note`, `Prefer`, and `Avoid` sections
234+
- Per-flag/argument `.Unit(...)` and `.Caution(...)` for machine-readable hints
235+
- Hand-rolled, trim/AOT-safe YAML emitter (no `YamlDotNet` runtime dependency)
236+
- `Examples/CurlAi/` exercises the full surface end-to-end
237+
135238
### Phase 2 — Modern .NET compatibility
136239
- Replaced DotLiquid with Scriban for trimming-safe help rendering
137240
- Library is now marked `IsAotCompatible` with trim, AOT, and single-file analyzers enabled

specs/2026-05-12-ai-help/plan.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Plan — AI-Friendly Help (`--help-ai`)
2+
3+
Branch: `feature/ai-help`
4+
Spec dir: `specs/2026-05-12-ai-help/`
5+
6+
Task groups are ordered by dependency. Do not start a group until every step in the preceding group is complete (or explicitly stubbed and tracked).
7+
8+
## Group 1 — Model & metadata surface
9+
10+
Foundation for the emitter. Adds the new fields and the author-facing fluent API for global sections, but does not yet emit anything.
11+
12+
1.1 Add `Unit` (`string?`) and `Caution` (`string?`) to `FlagItem` and `ArgumentItem`. Default to null. Wire fluent setters: `.Unit("seconds")`, `.Caution("Requires root.")` on both flag and argument builders.
13+
1.2 Introduce a `GlobalSections` record on `KingpinApplication` holding five lists: `ExitCodes`, `Examples`, `Notes`, `Prefer`, `Avoid`, each of a small strongly-typed record:
14+
- `ExitCode(int code, string description)`
15+
- `Example(string intent, string command)`
16+
- `Note(string text)`
17+
- `Prefer(string rule, string when, string why)`
18+
- `Avoid(string rule, string unless, string why)`
19+
1.3 Add fluent extension methods on `KingpinApplication` so apps can declare each section. Pattern: `app.ExitCode(0, "OK").Example("intent", "cmd").Prefer(rule: …, when: …, why: …)`. Keep chainable.
20+
1.4 Add basic xUnit coverage in `test/UnitTests/` for the new fluent setters: setting `Unit` and `Caution` round-trips into the model; each global-section setter appends an entry.
21+
22+
## Group 2 — YAML emitter
23+
24+
The trim/AOT-safe writer. No flag wiring yet; all tests drive it directly with a constructed `KingpinApplication`.
25+
26+
2.1 Add `src/KingpinNet/Help/AiHelpYamlWriter.cs`. `internal sealed class` accepting a `TextWriter`. Single public entry point `Write(KingpinApplication app, CommandItem? scope = null)` — when `scope` is set, only the subtree rooted at that command is emitted.
27+
2.2 Implement YAML primitives: indented scalar emission, list emission, block-scalar (`|`) for help text containing newlines, double-quoted scalar for strings containing `:`, `#`, leading `-`, or other YAML-significant characters. Centralise quoting in one private `WriteScalar(string key, string value)` helper.
28+
2.3 Implement command/flag/argument walkers: `WriteRoot`, `WriteGlobalFlags`, `WriteCommand` (recursive), `WriteFlag`, `WriteArgument`. Emit fields in the order documented in `specs/roadmap.md`'s Format reference. Suppress fields that are null/empty (especially `short`, `unit`, `default`, `help`, `caution`).
29+
2.4 Implement global-section emitters: `WriteExitCodes`, `WriteExamples`, `WriteNotes`, `WritePrefer`, `WriteAvoid`. Each writes its top-level key only if its source list is non-empty.
30+
2.5 Emit the fixed `conventions` block at the root with:
31+
- `flag_form: --flag=value preferred; bare for booleans`
32+
- `inheritance: Commands inherit flags from each ancestor in \`inherits\` and from \`global_flags\``
33+
2.6 Unit tests in `test/UnitTests/` covering, at minimum: scalar quoting per edge case (`:`, `#`, leading `-`, embedded newline), block-scalar selection, nested `commands:` indentation, omission of empty optional fields, omission of empty global sections, `scope` parameter restricting output to the subtree.
34+
35+
## Group 3 — `--help-ai` flag
36+
37+
Wire the emitter into the parser.
38+
39+
3.1 Register a built-in global flag `--help-ai` on every `KingpinApplication` and on every `CommandItem` (mirroring how `--help` is registered today; consult existing registration in `KingpinApplication.cs` / `Parser.cs`).
40+
3.2 In the parser, intercept `--help-ai` before normal handler dispatch (same point in the pipeline as the existing `--help` short-circuit). Resolve the scope (root vs. specific subcommand based on which node the flag was attached to).
41+
3.3 Default destination is stdout via the existing `IConsole` abstraction. Construct the writer, call `AiHelpYamlWriter.Write(app, scope)`, exit cleanly (do not invoke a command handler).
42+
3.4 Integration test: a small in-process `KingpinApplication` invoked with `--help-ai` produces non-empty output that begins with `command:` and contains the expected subcommand names.
43+
44+
## Group 4 — File output mode + discovery hint
45+
46+
Adds the second flag and the human-help footer.
47+
48+
4.1 Register `--help-ai-file` as a built-in global flag that optionally takes a path. With no value, the destination is `<exename>-ai-help.md` resolved next to the running executable (use `Environment.ProcessPath` / `AppContext.BaseDirectory`).
49+
4.2 Implement the Markdown wrapper: write `# <exename> help`, blank line, fenced ` ```yaml ` block containing the same YAML payload `--help-ai` emits, closing fence, trailing newline. Reuse `AiHelpYamlWriter` unchanged — the wrapper is purely outside the writer.
50+
4.3 Append the single-line footer to the human-rendered help output (the existing Scriban template surface in `src/KingpinNet/Help/`). Wording: `For agents: run with --help-ai for a machine-readable description.`
51+
4.4 Integration test: invoking the flag with no value creates the expected file in a temp directory; invoking it with an explicit path writes there. Assert the file starts with `# ` and contains a `\`\`\`yaml` fence.
52+
53+
## Group 5 — Test reinforcement
54+
55+
The validation gates that prevent regressions and authored-content drift.
56+
57+
5.1 Add `YamlDotNet` (or an equivalent .NET YAML parser) to the test project only — never to `src/KingpinNet/KingpinNet.csproj`. Verify with `dotnet list test/UnitTests package`.
58+
5.2 Round-trip validity test: for a representative `KingpinApplication` fixture, emit YAML to a string, parse it with `YamlDotNet`, assert no exceptions and that a handful of expected keys (`command`, `commands`, `conventions`) are present.
59+
5.3 Golden/snapshot test(s): one or two representative apps (one trivial, one with nested commands + all global sections) emit YAML compared against checked-in `.expected.yaml` files under `test/UnitTests/`.
60+
5.4 `examples[].command` parse-check: a test iterates every `Example` declared on the fixture app, splits the command line, and feeds it through `Parser` / the existing token + parse pipeline. Asserts it resolves to a valid command + flag set without errors.
61+
62+
## Group 6 — Documentation & example app
63+
64+
User-facing surface.
65+
66+
6.1 Create `Examples/CurlAi/` by copying `Examples/Curl/`. Rename `csproj` to `CurlAi.csproj`. Update assembly name / namespace as appropriate.
67+
6.2 Extend the program to populate every Phase 3 surface: declare at least one `caution` on a destructive flag (e.g. `--force` or `--insecure`), make at least one positional `required: true`, declare a `unit` on every value-taking flag, and call the fluent API for all five global sections.
68+
6.3 Add `Examples/CurlAi/CurlAi.csproj` to `KingpinNet.slnx` so it builds in CI.
69+
6.4 Update `README.md` with a new section titled "AI-friendly help (`--help-ai`)". Cover: what the flag emits, how to invoke it, file mode behavior and default location, the fluent API for the five global sections, and a short (~20-line) YAML excerpt taken from CurlAi's actual output.
70+
71+
## Group 7 — Final validation pass
72+
73+
Runs the merge checklist before the PR is raised. See `validation.md` for the exact commands.
74+
75+
7.1 `dotnet build` clean.
76+
7.2 `dotnet test` clean (all existing + new tests pass).
77+
7.3 AOT publish gate green for a sample project; no new trim warnings introduced.
78+
7.4 Manual smoke: build CurlAi, run `--help-ai` to stdout (eyeball YAML), run `--help-ai-file` (verify `curl-ai-ai-help.md` written next to the binary with the expected wrapper).
79+
7.5 Tick the Phase 3 checkboxes in `specs/roadmap.md` as each is satisfied. Do not tick them speculatively before the validation gates pass.

0 commit comments

Comments
 (0)