From ngql-preview
**Preview build of the NGql Skill.** Tracks the latest preview release of NGql — content here may reference library APIs that have not yet shipped to the stable `ngql` channel. Use this if you're testing an upcoming NGql release; otherwise install `ngql` (stable). Translates the user's GraphQL intent into NGql query-builder C# code: triggered by "build a query", a pasted GraphQL operation or curl that needs porting to NGql, "filter / preserve fields", or mentions of QueryBuilder / Mutation / PreservationBuilder / NGql. NGql is a zero-dependency, schema-less .NET GraphQL query builder — the user supplies the schema knowledge (field names, types), the Skill produces the fluent code.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ngql-preview:ngqlThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are generating NGql query-builder code for a .NET project. **NGql does not own a schema.** The user knows their GraphQL API. Your job is to render their intent as fluent NGql calls — not to invent fields, types, or arguments they didn't supply.
You are generating NGql query-builder code for a .NET project. NGql does not own a schema. The user knows their GraphQL API. Your job is to render their intent as fluent NGql calls — not to invent fields, types, or arguments they didn't supply.
Pick one based on the user's input. If you can't tell, ask before generating code.
"Get the top 10 repositories of GitHub user
dolifer, with stargazer counts."
QueryBuilder.CreateDefaultBuilder(...) for queries, QueryBuilder.CreateMutationBuilder(...) for mutations. Both share the same fluent surface (AddField, Include, WithMetadata, etc.) — the only difference is the operation prefix at render time."Here's the curl I have today, port it to NGql:" (paste)
{"query": "...", "variables": {...}} — pull query and variables out before parsing.query Foo($x: ID!) → QueryBuilder.CreateDefaultBuilder("Foo") plus new Variable("$x", "ID!") passed as an argument value (auto-promoted to the operation signature).field(arg: 5, name: "bob") → .AddField("field", new Dictionary<string, object?> { ["arg"] = 5, ["name"] = "bob" }, ...).new EnumValue("ADMIN") as the argument value..AddField("user.profile.name")) or a sub-field lambda (.AddField("user", b => b.AddField("name"))). Prefer dot-paths when there are no per-level arguments; use the lambda when intermediate levels carry args or you need readable nesting..Include(other).mutation → use QueryBuilder.CreateMutationBuilder("Name"). The fluent surface is the same as the query path; only the operation prefix differs at render time."Take this builder and produce a public view that drops
ssnand
PreservationBuilder.Create(source).Preserve("path.a", "path.b").Build() to keep listed paths.PreserveFromExpression<T>(x => x.user.profile.email != null). Mention the T model class must mirror the query shape — the Skill cannot synthesize that DTO without seeing or being told its structure.Preserve(...) is inclusive ("keep these"), not exclusive ("drop these"). If the user says "drop X", flip it: list everything except X, or use PreserveFromExpression with a predicate that excludes the field.using NGql.Core; // Variable, EnumValue, MergingStrategy
using NGql.Core.Builders; // QueryBuilder, FieldBuilder, PreservationBuilder
Query and Mutation types still exist in NGql.Core for back-compat, but you should not generate code that uses them — see the "Legacy types" section at the bottom.
QueryBuilder — the fluent API for both queries and mutationsQueryBuilder.CreateDefaultBuilder("OperationName"); // query Operation { ... }
QueryBuilder.CreateDefaultBuilder("OperationName", MergingStrategy.MergeByFieldPath);
QueryBuilder.CreateMutationBuilder("OperationName"); // mutation Operation { ... }
QueryBuilder.CreateMutationBuilder("OperationName", MergingStrategy.MergeByFieldPath);
The two factories return the same QueryBuilder type with the same fluent methods — CreateMutationBuilder only changes the operation prefix at ToString() time. Variables, arguments, sub-fields, Include, WithMetadata, PreservationBuilder — all work identically across the two.
AddField — pick the shape that matches your intent. These are the only call shapes you should generate; together they cover every realistic case.
.AddField("path.to.field") // simple, including dot-paths
.AddField("users", new Dictionary<string, object?> { ... }) // with arguments
.AddField("users", new[] { "id", "name" }) // with sub-field names
.AddField("users", new Dictionary<string, object?> { ... },
new[] { "id", "name" }) // arguments + sub-fields (args FIRST)
.AddField("user", b => b.AddField("name").AddField("email")) // sub-field lambda
.AddField("user", new Dictionary<string, object?> { ... },
b => b.AddField("name")) // arguments + sub-field lambda
.AddField("amount", "Money!") // typed leaf (rare)
Inside a sub-field lambda you call the same set of shapes on the lambda's b. Args go BEFORE sub-fields, exactly the same as on the outer QueryBuilder.
Metadata (the per-field Dictionary<string, object?> for telemetry/tags/etc., separate from GraphQL arguments) is attached via a lambda, not as a positional dict:
.AddField("user", new Dictionary<string, object?> { ["id"] = idVar }, b => b
.WithMetadata(new Dictionary<string, object> { ["cached"] = true })
.AddField("name"))
Never pass a metadata dict as a positional argument — always use the WithMetadata(...) step inside a sub-field lambda. (This rule means callers and Claude never have to disambiguate two adjacent Dictionary<string, object?> parameters.)
Use Dictionary<string, object?> (not IDictionary<,>) at the call site — that is what the public overloads declare.
Compose:
var combined = QueryBuilder
.CreateDefaultBuilder("Combined", MergingStrategy.MergeByFieldPath)
.Include(fragmentA)
.Include(fragmentB);
MergingStrategy values:
MergeByDefault — inherit parentMergeByFieldPath — merge same-path fragments, auto-alias on argument conflict (the default for a fresh CreateDefaultBuilder)NeverMerge — keep every Include distinctVariablevar idVar = new Variable("$id", "ID!"); // name *includes* the $ prefix
Pass the Variable instance as an argument value and it is auto-promoted to the operation's variable list:
.AddField("user", new Dictionary<string, object?> { ["id"] = idVar }, new[] { "name" })
EnumValue.AddField("users", new Dictionary<string, object?> {
["role"] = new EnumValue("ADMIN") // renders unquoted: role:ADMIN
})
Without EnumValue, a string would render as "ADMIN" (quoted). Always wrap GraphQL enum literals.
PreservationBuildervar publicView = PreservationBuilder.Create(fullQuery)
.Preserve("user.name", "user.email")
.Build();
var conditional = PreservationBuilder.Create(fullQuery)
.PreserveFromExpression<UserDto>(x => x.user.profile.email != null)
.Build();
var scoped = PreservationBuilder.Create(fullQuery)
.PreserveAtPath("createdAt", "user.posts") // only keep `createdAt` under `user.posts`
.Build();
Build() returns a fresh QueryBuilder — the source is never mutated.
Query and Mutation (do not generate)The NGql.Core.Query and NGql.Core.Mutation classes are the NGql 1.x classic API. They still render correctly and are not removed, but new code should not use them — QueryBuilder.CreateDefaultBuilder and QueryBuilder.CreateMutationBuilder cover both surfaces with a richer fluent API. If you encounter user code that uses these types, you can read it (to extract intent) but do not produce more of it. When porting, map:
| Classic | New |
|---|---|
new Query("Foo", vars) | QueryBuilder.CreateDefaultBuilder("Foo") |
new Mutation("Foo", vars) | QueryBuilder.CreateMutationBuilder("Foo") |
.Where("name", value) (on a sub-query for arguments) | argument dictionary on AddField(...) |
.Select("a", "b") | .AddField("a").AddField("b") or new[] { "a", "b" } |
.Include<T>("name") | no direct equivalent — extract field names from the type yourself and call AddField |
--var stringsTwo distinct contexts need correct value handling: argument literals inside the C# snippet (handed to NGql.Core) and shell --var key=value strings (handed to ngql --execute). Pick the right form for each context.
When generating arguments for AddField, Where, etc., these CLR types map to GraphQL value forms:
| GraphQL value | CLR literal | Renders as |
|---|---|---|
Int | int, long, short, sbyte (and unsigned counterparts) | 42 |
Float | float, double, decimal | 3.14 |
String | string | "alice" (quotes, backslashes, and control chars are escaped — just write the string naturally) |
Boolean | bool | true / false |
null | null | null |
ID | string | "abc123" (GraphQL ID is a string on the wire) |
| enum literal | new EnumValue("ADMIN") | ADMIN (unquoted) |
| variable reference | new Variable("$id", "ID!") | $id (auto-promoted to the operation signature) |
| List of T | new[] { ... }, new List<T> { ... }, or any IList | [1, 2, 3] / [ADMIN, EDITOR] |
| Input object | new Dictionary<string, object?> { ["k"] = v, ... } | {k:v, ...} (keys alphabetized) |
| DateTime / DateTimeOffset | new DateTime(...) | ISO-8601 quoted string. Most GraphQL servers expect a custom scalar here — verify the wire shape matches the server's contract. |
CLR enums (StringComparison.Ordinal) also work — they render as their .ToString() name. Prefer new EnumValue("EXACT_NAME") over CLR enums unless the CLR enum's casing already matches the server's expected name; GraphQL convention is SCREAMING_SNAKE_CASE while C# is PascalCase.
Nested freely: a Dictionary inside a Dictionary, a list of Dictionarys, all combine. Variables can sit anywhere a CLR value can.
--var key=value (execute mode)The tool JSON-parses each value; if parsing fails, the value is treated as a bare string. So:
| You want | Pass | Becomes (JSON variable) |
|---|---|---|
| number | --var first=10 | 10 |
| boolean | --var active=true | true |
| null | --var maybe=null | null |
| string | --var name=alice | "alice" (parse fails → bare-string fallback) |
| string with spaces | --var name="Anne Ware" | "Anne Ware" |
| list | --var ids='[1,2,3]' | [1, 2, 3] |
| list of strings | --var tags='["a","b"]' | ["a", "b"] |
| input object | --var filter='{"min":10,"tags":["a"]}' | {"min": 10, "tags": ["a"]} |
Single-quote the value when it contains JSON syntax — otherwise the shell will eat the brackets/quotes/braces. The tool does not validate against a schema, so a wrong type silently reaches the server (which will reject it). This matches what curl users already do.
When the user wants to execute a snippet that uses Variable("$x", "T") references, they must pass matching --var x=value flags (the $ prefix is for the snippet, not the CLI). Example:
// snippet.cs:
QueryBuilder.CreateDefaultBuilder("GetOrder")
.AddField("order",
new Dictionary<string, object?> { ["id"] = new Variable("$id", "ID!") },
new[] { "id", "status" })
ngql snippet.cs --execute --endpoint https://... --var id=42
The user's ToString() calls produce GraphQL with these conventions — match them when you describe expected output:
users{ not users {)EnumValue) unquoted; null literal; numbers raw$name:Type in the operation signatureExample — given:
var query = QueryBuilder.CreateDefaultBuilder("GetUsers")
.AddField("users.name")
.AddField("users.email");
query.ToString() produces:
query GetUsers{
users{
email
name
}
}
Note email before name — alphabetical, not insertion order.
Ask, don't guess, in these cases:
repositories query expose?"delete, transferFunds). Confirm the operation name and the input shape before generating — the cost of a wrong mutation is higher than a wrong query.Preserve is inclusive.MergingStrategy.Query or Mutation API (new Query(...), new Mutation(...), .Where(...), .Include<T>(...)). They are soft-deprecated in 2.1 — QueryBuilder.CreateDefaultBuilder and QueryBuilder.CreateMutationBuilder cover both surfaces with a richer fluent API. The classic types still render correctly for back-compat but are no longer the recommended path.repos.totalStars and you don't know the real field name (stargazerCount on GitHub), ask — don't generate code that won't compile against their server..ToString() themselves to see the GraphQL.using directives for namespaces you don't reference.IDictionary<string, object?> literals — the public overloads take Dictionary<string, object?>. Use a concrete dictionary at the call site.metadata dictionary as a positional argument to AddField. Attach metadata via a sub-field lambda using b.WithMetadata(...). This keeps arguments as the only positional Dictionary<string, object?> slot and removes the only place where two adjacent dicts could be confused.try/catch, or "future-proofing" abstractions in generated code. NGql calls are pure builder construction; let them throw on bad input rather than wrapping them.ngql via Bash when the user explicitly asks ("send this", "run that", "execute it", "try it against the endpoint"). For any other binary — which, dotnet tool list, curl, cat, gh, git, etc. — ask first and get explicit consent before running. Pattern: surface the intent ("want me to check whether ngql is installed by running which ngql?"), wait for "yes" / "go" / similar, then run. Never silently shell out for diagnostics.ngql for the first time in a session, confirm two things in your message: (1) the endpoint isn't going to surprise the user — read the URL aloud (localhost, staging, prod, third-party) and pause for go-ahead if it's anything other than localhost or a clear sandbox; and (2) for mutations, that --allow-mutations is intentional. For pure queries against localhost or services the user clearly owns, you can run without asking. For everything else, single-line confirm.ngql exits non-zero, report the actual exit code and stderr verbatim, then offer one fix. Common cases: exit 1 = the snippet didn't compile (offer to fix the snippet); exit 2 = the server returned a GraphQL errors array (interpret the errors); exit 3 = HTTP failure (surface the status code); exit 4 = mutation blocked (offer the --allow-mutations form, with the safety re-check from the previous bullet); exit 127 or "command not found" = either ngql isn't installed or ~/.dotnet/tools/ isn't on $PATH. For "not installed", see the install section below — ask the user about channel (default to whichever channel matches this Skill's plugin name) and scope (default-suggest local). For "PATH issue", offer export PATH="$PATH:$HOME/.dotnet/tools" for the current shell. Check both — don't assume install is the right fix.errors array." If the response body looks like HTML, plain text, an echo dump (e.g. webhook.site, request bins), or anything other than a JSON object with a data field, call that out explicitly: "the server returned a 200 but the body isn't a GraphQL response — looks like <HTML/echo/etc>. Is this actually a GraphQL endpoint?" Don't claim success just because the exit code was 0.which ngql", run Bash: which ngql, not a file search or grep. Substituting tools mid-action confuses the user about what your message meant and breaks the trust contract that the announcement matched the run.ngql as-isngql evaluates a snippet as a C# script (Roslyn scripting). The contract is "the final expression yields the builder" — it's not a normal C# program. Concretely:
Console.WriteLine(...). ngql calls .ToString() on the script's last expression value and prints that itself. Adding Console.WriteLine either won't be invoked or duplicates output.var x = ...; and let the builder be the bare last expression, or end with a bare reference (x on its own line). Do not use return — top-level scripts don't allow it.ngql): System, System.Collections.Generic, System.Linq, NGql.Core, NGql.Core.Builders. Do not add using directives for these. Other namespaces (e.g. System.Text.Json) need explicit using.Canonical shape:
QueryBuilder.CreateDefaultBuilder("Hello")
.AddField("world.name")
That's it. No var query =, no return, no Console.WriteLine. If you find yourself writing any of those, the snippet is wrong for ngql — fix it before running.
NGql is a schema-less query builder. It doesn't model every GraphQL syntactic construct. The following constructs are not supported by NGql:
| GraphQL construct | NGql support |
|---|---|
Inline fragments (... on Type { … }) | Supported via FieldBuilder.OnType("TypeName", b => …). See worked example below. |
| Unions / interfaces with multiple type narrowings | Supported — call .OnType(…) once per concrete type on the parent field. |
Named fragments (fragment X on T { … }, ...X) | None — tracked in issue #20. For now, inline the selection set at each use site. |
Directives (@include, @skip, @deprecated, custom) | None as first-class syntax. |
Subscriptions (subscription S { … }) | None. QueryBuilder and Mutation are the only operation types. |
When the user's request needs ANY of the unsupported constructs above (named fragments, directives, subscriptions):
Do NOT generate broken code "as a starting point" or "for reference." Generating something that looks like working NGql but renders to invalid GraphQL is the worst failure mode — it invites copy-paste and hides the gap. Skip the broken version entirely; the user's first contact with code should be code that actually works.
Inline fragments and union/interface narrowing are NOT in this restricted list anymore — generate them freely using OnType (see worked example below).
"Get the top 10 repositories of GitHub user
dolifer, with name and stargazer count."
using NGql.Core;
using NGql.Core.Builders;
var query = QueryBuilder.CreateDefaultBuilder("TopRepos")
.AddField("user",
new Dictionary<string, object?> { ["login"] = "dolifer" },
b => b.AddField("repositories",
new Dictionary<string, object?>
{
["first"] = 10,
["orderBy"] = new Dictionary<string, object?>
{
["field"] = new EnumValue("STARGAZERS"),
["direction"] = new EnumValue("DESC"),
},
},
new[] { "name", "stargazerCount" }));
Assumed schema (GitHub v4):
user(login: String!) { repositories(first, orderBy) { name, stargazerCount } }. If your schema differs, swap the field names.
"Build a
CreateUsermutation taking$nameandidandcreatedAt."
using NGql.Core;
using NGql.Core.Builders;
var nameVar = new Variable("$name", "String!");
var emailVar = new Variable("$email", "String!");
var mutation = QueryBuilder.CreateMutationBuilder("CreateUser")
.AddField("createUser",
new Dictionary<string, object?>
{
["name"] = nameVar,
["email"] = emailVar,
},
new[] { "createdAt", "id" });
Input:
curl https://api.example.com/graphql \
-H 'authorization: Bearer XXX' \
-d '{"query":"query GetOrder($id: ID!) { order(id: $id) { id status total } }","variables":{"id":"42"}}'
Output:
using NGql.Core;
using NGql.Core.Builders;
var idVar = new Variable("$id", "ID!");
var query = QueryBuilder.CreateDefaultBuilder("GetOrder")
.AddField("order",
new Dictionary<string, object?> { ["id"] = idVar },
new[] { "id", "status", "total" });
// Submit with variables: {"id": "42"} — NGql renders the operation only,
// you bind variable values at the transport layer.
When a field returns a GraphQL union or interface (e.g. GitHub's search.nodes returns SearchResultItem, an interface implemented by Repository, Issue, PullRequest, …) and you need fields that only exist on a specific concrete type, use FieldBuilder.OnType("ConcreteType", b => …). It renders as ... on ConcreteType { … }.
"Build a query that searches GitHub for the top 10 starred repositories and returns name, stargazerCount, and url."
using NGql.Core;
using NGql.Core.Builders;
QueryBuilder.CreateDefaultBuilder("TopRepos")
.AddField("search",
new Dictionary<string, object?>
{
["query"] = "stars:>1",
["type"] = new EnumValue("REPOSITORY"),
["first"] = 10,
},
b => b.AddField("nodes", n => n
.OnType("Repository", r => r
.AddField("name")
.AddField("stargazerCount")
.AddField("url"))));
Renders to:
query TopRepos{
search(first:10, query:"stars:>1", type:REPOSITORY){
nodes{
... on Repository{
name
stargazerCount
url
}
}
}
}
Things to note:
"Repository" string is a schema type name, not user-defined — it must match a real type on the server's schema.OnType calls for the same parent are valid and common — one per concrete type the union/interface needs to narrow to. Repeated OnType("Repository", …) calls on the same parent merge into one fragment definition.OnType("PullRequest", p => p.OnType("Mergeable", m => …)) produces a fragment-inside-fragment.For named fragments (fragment X on T { … } + ...X reuse), NGql doesn't support those yet — see issue #20. Inline the equivalent selection set at each use site for now.
"Take
fullProfileand produce a public view with only name and avatar."
using NGql.Core.Builders;
var publicView = PreservationBuilder.Create(fullProfile)
.Preserve("user.name", "user.avatar")
.Build();
ngqlNGql ships a companion .NET global tool, dotnet-ngql, that compiles a snippet against the bundled NGql.Core and prints the rendered GraphQL. It's the cleanest way for the user to confirm what the code actually produces.
You may run
ngqlfor the user when explicitly asked. "Send this," "run that," "execute," "try it" — go ahead and runngql .... For first-time runs against non-localhost endpoints, single-line confirm the URL is intended. For mutations, confirm--allow-mutationsis intended. For probing the environment (which ngql,dotnet tool list), ask before running — surface the intent in your message, wait for go-ahead. Never silently shell out for diagnostics.
Install (one-time): ask the user two questions before suggesting a command — channel and scope.
Channel (stable vs preview). Default to whichever channel matches this Skill's own channel: if the user invoked /ngql-preview:ngql, default-suggest preview; if they invoked /ngql:ngql, default-suggest stable. You can read your own plugin name from the SKILL.md frontmatter (name: ngql vs name: ngql-preview) — match that. Phrase it as a confirmation, e.g. "You're using the preview Skill, so I'd install the preview tool to match — sound right?" If the user disagrees, switch to the other channel.
| Channel | Command suffix | When to pick |
|---|---|---|
| stable | (no flag) | Matches /ngql:ngql. Production-ready code. Note: as of mid-2026 no stable release exists on NuGet yet — install will fail with "not found in NuGet feeds" until it ships. |
| preview | --prerelease | Matches /ngql-preview:ngql. Tracks the latest features. The --prerelease flag is required to see preview versions. |
Scope (local vs global). Default-suggest local (per-project manifest), since it pins the tool version to the project and doesn't pollute the user's global tool set. Frame the choice:
dotnet new tool-manifest (one-time per project) then dotnet tool install dotnet-ngql [--prerelease]. Invoked as dotnet ngql .... Pinned to the project; reproducible across machines via the .config/dotnet-tools.json checked into git.dotnet tool install -g dotnet-ngql [--prerelease]. Invoked as ngql .... Convenient for one-off use, but version is shared across all projects on the machine.Ask before assuming: "Want me to install it locally to this project (recommended — it pins the version) or globally?" Wait for the answer before running.
Update to the latest: same channel split applies. dotnet tool update dotnet-ngql [--prerelease] (local) or dotnet tool update -g dotnet-ngql [--prerelease] (global).
Version conflict. If install or update reports requested version is lower than existing version, the user has a higher version locally (often from a --add-source ./artifacts/packages dev install). Don't pick a side automatically — ask:
dotnet tool uninstall [-g] dotnet-ngql && dotnet tool install [-g] dotnet-ngql [--prerelease] (uninstall + reinstall, only dotnet-ngql is touched, no other tools).Wait for the user's pick before running anything. The dotnet tool commands above are scoped to dotnet-ngql specifically — never list, modify, or remove other installed tools.
If ngql runs but reports "command not found" after install, the user's shell can't find ~/.dotnet/tools/ on $PATH (global install) or hasn't picked up the manifest (local install). For global: tell them to export PATH="$PATH:$HOME/.dotnet/tools" (current shell) or persist it in ~/.zshrc / ~/.bashrc. For local: remind them to invoke as dotnet ngql ..., not bare ngql ....
If the user just wants to see the GraphQL produced by a snippet:
ngql snippet.cs # from a file
echo '<snippet body>' | ngql # from stdin
The tool prints the GraphQL to stdout. Exit code 0 on success, 1 on compile/runtime error. Composable with shell redirects:
ngql snippet.cs > expected.graphql
When the user signals they have a real GraphQL endpoint to test against — e.g. mentions "verify against...", "run this against...", "test it on the staging API", or pastes a curl that includes a server URL — suggest the --execute flow. Do not assume an endpoint; if the user hasn't provided one, ask. The Skill should never invent endpoint URLs.
ngql snippet.cs --execute \
--endpoint https://api.example.com/graphql \
-H "Authorization: Bearer $TOKEN" \
--var id=42 \
--var login=octocat
-H "Name: value" is repeatable; one per header.--var key=value is repeatable. Values are JSON-parsed when possible — numbers, booleans, arrays, and objects all work without quoting tricks. Bare strings (--var login=octocat) are passed as-is.0 success, 2 GraphQL errors array in response, 3 HTTP failure, 4 mutation blocked, 1 snippet failed to render, 64 invalid usage.ngql --execute refuses to send mutations by default. When the rendered operation begins with mutation, the tool prints a refusal message and exits with code 4 unless the user passes --allow-mutations.
If the user asks to execute a mutation and you suggest the command line, also flag the side-effect risk in the same message:
Heads up — this is a mutation, so it'll actually change data on the server. If you're sure (the endpoint is a sandbox, you have a backup, etc.), pass
--allow-mutations. Otherwise dry-run by dropping--executeto just see the rendered GraphQL first.
Don't pre-add --allow-mutations to suggested command lines unless the user has explicitly confirmed the side effect is intended.
When ngql reports a compile error, the most common causes are:
using NGql.Core.Builders; (for QueryBuilder / PreservationBuilder / FieldBuilder). The tool auto-imports this for stdin/file snippets, but if the user is integrating the snippet into their own project, they need it.IDictionary<string, object?> instead of Dictionary<string, object?> to AddField.new EnumValue(MyEnum.Admin) — that overload exists, but for safety prefer new EnumValue("ADMIN") with the literal GraphQL enum name; CLR enum names may not match GraphQL enum names.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub dolifer/claude-plugins --plugin ngql-preview