From dace1884ac2ae0804a685e4e98357f612cfcb215 Mon Sep 17 00:00:00 2001 From: mohammadali2549 Date: Fri, 17 Jul 2026 07:29:54 +0500 Subject: [PATCH 1/2] feat(maxio-sdk-clone): add experimental map-in-clone plugin variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forks maxio-sdk (v0.2.0) into a new experimental plugin, maxio-sdk-clone, that reads the SDK map from the SDK source clone instead of bundling it. maxio-plan clones the SDK first (gains Bash) and grounds against the shipped-with-source map, with a source fallback under a hard anti-spelunk guard; maxio-debug reuses the clone. Additive — baseline maxio-sdk untouched; targets the experimental dev-raw line. claude plugin validate --strict passes for plugin + marketplace. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 5 + .../.claude-plugin/plugin.json | 29 ++ plugins/maxio-sdk-clone/README.md | 86 ++++++ plugins/maxio-sdk-clone/agents/maxio-debug.md | 75 +++++ plugins/maxio-sdk-clone/agents/maxio-plan.md | 143 ++++++++++ .../skills/dotnet-authentication/SKILL.md | 145 ++++++++++ .../skills/dotnet-calling-endpoints/SKILL.md | 213 ++++++++++++++ .../dotnet-client-initialization/SKILL.md | 120 ++++++++ .../dotnet-configuration-resilience/SKILL.md | 229 +++++++++++++++ .../skills/dotnet-error-handling/SKILL.md | 264 ++++++++++++++++++ .../skills/dotnet-models/SKILL.md | 113 ++++++++ .../skills/dotnet-models/reference.md | 64 +++++ .../skills/dotnet-testing/SKILL.md | 173 ++++++++++++ .../skills/integrate-maxio/SKILL.md | 145 ++++++++++ .../skills/maxio-getting-started/SKILL.md | 237 ++++++++++++++++ 15 files changed, 2041 insertions(+) create mode 100644 plugins/maxio-sdk-clone/.claude-plugin/plugin.json create mode 100644 plugins/maxio-sdk-clone/README.md create mode 100644 plugins/maxio-sdk-clone/agents/maxio-debug.md create mode 100644 plugins/maxio-sdk-clone/agents/maxio-plan.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-authentication/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-calling-endpoints/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-client-initialization/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-configuration-resilience/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-error-handling/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-models/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-models/reference.md create mode 100644 plugins/maxio-sdk-clone/skills/dotnet-testing/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md create mode 100644 plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f1a9060..da59c33 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -20,6 +20,11 @@ "name": "maxio-sdk", "source": "./plugins/maxio-sdk", "description": "Install and consume the Maxio Advanced Billing .NET SDK with skill-only guidance, a bundled SDK map, and subagent orchestration (integrate-maxio router + maxio-plan contract-sheet agent + maxio-debug escalation-first agent). The agent looks up signatures, errors, enums, and unions in the map and opens only the exact source file it needs in the cloned SDK, instead of grepping. No MCP server." + }, + { + "name": "maxio-sdk-clone", + "source": "./plugins/maxio-sdk-clone", + "description": "EXPERIMENTAL variant of maxio-sdk. The SDK map is not bundled in the plugin — it ships inside the SDK source, so the maxio-plan and maxio-debug agents clone the SDK first, look facts up in the map that travels with it, and open the exact source file the map names only when the map cannot settle a fact. Same integrate-maxio router + maxio-plan + maxio-debug orchestration. No MCP server. (Do not install alongside maxio-sdk — they share skill/agent names.)" } ] } diff --git a/plugins/maxio-sdk-clone/.claude-plugin/plugin.json b/plugins/maxio-sdk-clone/.claude-plugin/plugin.json new file mode 100644 index 0000000..4e89003 --- /dev/null +++ b/plugins/maxio-sdk-clone/.claude-plugin/plugin.json @@ -0,0 +1,29 @@ +{ + "name": "maxio-sdk-clone", + "displayName": "Maxio SDK Assistant (map-in-clone)", + "version": "0.1.0", + "description": "EXPERIMENTAL variant of maxio-sdk (descends from maxio-sdk v0.2.0). The generated SDK map is NOT bundled in the plugin — it ships inside the SDK source, so the maxio-plan and maxio-debug agents clone the SDK first, look facts up in the map that travels with it, and open the exact source file the map names only when the map cannot settle a fact. Includes the integrate-maxio router, a map-grounded maxio-plan contract-sheet agent, and a map-first maxio-debug agent. No MCP server.", + "author": { + "name": "APIMatic", + "email": "developer@apimatic.io" + }, + "license": "MIT", + "homepage": "https://www.apimatic.io", + "repository": "https://github.com/apimatic/plugin-marketplace", + "keywords": ["maxio", "advanced-billing", "chargify", "apimatic", "dotnet", "csharp", "sdk"], + "skills": [ + "./skills/integrate-maxio", + "./skills/maxio-getting-started", + "./skills/dotnet-client-initialization", + "./skills/dotnet-authentication", + "./skills/dotnet-calling-endpoints", + "./skills/dotnet-models", + "./skills/dotnet-error-handling", + "./skills/dotnet-configuration-resilience", + "./skills/dotnet-testing" + ], + "agents": [ + "./agents/maxio-plan.md", + "./agents/maxio-debug.md" + ] +} diff --git a/plugins/maxio-sdk-clone/README.md b/plugins/maxio-sdk-clone/README.md new file mode 100644 index 0000000..9205a69 --- /dev/null +++ b/plugins/maxio-sdk-clone/README.md @@ -0,0 +1,86 @@ +# Maxio SDK Assistant — map-in-clone variant (Claude Code plugin) + +**Experimental variant of `maxio-sdk`** (descends from `maxio-sdk` v0.2.0). It helps developers +**install and consume the Maxio Advanced Billing .NET SDK**, plus reusable guidance for working with +**any APIMatic-generated .NET SDK**. + +What makes this variant different: the **SDK map is not bundled in the plugin** — it ships inside the +SDK's own source, and the agents clone the SDK to read it. Sourced from +[apimatic/v4-plugins](https://github.com/apimatic/v4-plugins) `plugins/maxio-sdk`, extended with +**subagent orchestration** (an `integrate-maxio` router plus `maxio-plan` and `maxio-debug` agents). + +## The SDK map (shipped with the SDK, read from a clone) + +In this variant the generated table-of-contents is **not** bundled in the plugin. It ships at the +**root of the SDK's own source** (`sdk-map.md` plus `map/` branch pages), so the `maxio-plan` and +`maxio-debug` agents **clone the SDK first** and read the map from the clone: + +- **`map/operations/`** — one page per controller (33 pages, 247 operations): exact C# signature, + must-pass-explicitly params, return type, error case (typed vs `RawError`) with its `TryGet…` + accessors, and pagination — plus the source file each came from. +- **`map/models/`** — record models, `OneOf`/`AnyOf` unions (factories + `TryGet…`), and full + enum value lists. + +Because the map travels with the source it describes, the two are the **same version by +construction** — there is no separate map to drift. The agent still **navigates by map lookup instead +of grepping**: it answers most questions from the map directly — field lists with JSON wire names, +error accessors, enum values — and only when the map can't settle a fact does it open the one source +file the map names, in the same clone. Grepping / globbing / `find`-ing the 600+-file tree to locate +something is a defect — the map is the locator. + +During the experiment the agents clone the fork/branch that carries the map +(`mohammadali2549/advanced-billing-sample-sdk` @ `docs/sdk-map`); once the map is merged upstream this +becomes the canonical `asadali214/advanced-billing-sample-sdk` at the released tag. + +## Two layers + +The Maxio SDK is generated by the [APIMatic](https://www.apimatic.io/) code generator, which emits +the same cross-cutting shape for every .NET SDK it produces. The plugin reflects that: + +| Layer | Skill(s) | Names used | +| --- | --- | --- | +| **SDK-specific** (entry point) | `maxio-getting-started` (+ the SDK map, read from the SDK clone) | Concrete Maxio names (`MaxioAdvancedBillingClient`, etc.) | +| **API-agnostic usage** | `dotnet-client-initialization`, `dotnet-authentication`, `dotnet-calling-endpoints`, `dotnet-models`, `dotnet-error-handling`, `dotnet-configuration-resilience`, `dotnet-testing` | Placeholders (`Client`, `ClientOptions`, …) so they apply to *any* APIMatic .NET SDK | + +The agnostic skills never name Maxio directly — they describe the generator's patterns so they apply +to whatever APIMatic .NET SDK your project consumes as a published NuGet package. Where an SDK map +ships with the SDK (as it does here, in the clone), they defer to it for lookup before touching the +SDK source. + +## Skills + +- **integrate-maxio** — orchestrator/router. Routes a Maxio .NET SDK task to the `maxio-plan` or + `maxio-debug` agent, handles blocker hand-back, and drives the implement-and-verify loop. Grounds + every fact in the contract sheet the helpers produce — never model knowledge, and never the map or + clone directly. +- **maxio-getting-started** — NuGet install, US/EU environments, Basic-auth quickstart, and how to + clone the SDK and read the map + source from the clone. The helper-facing entry point. +- **dotnet-client-initialization** — construct `Client` + `ClientOptions`, supply an `HttpClient`, + pick a server environment, register in DI. +- **dotnet-authentication** — wire up Basic / Bearer / API-key / OAuth2 / composite auth. +- **dotnet-calling-endpoints** — method-signature conventions, building request `record`s, enums, reading + responses. +- **dotnet-models** — polymorphic `OneOf`/`AnyOf` union types, collections, dates/converters, string- and + int-enums, unknown-field behavior. +- **dotnet-error-handling** — catching `SdkException`, reading typed errors and `RawError`. +- **dotnet-configuration-resilience** — retries, timeouts, pagination, logging. +- **dotnet-testing** — unit-test SDK calls by injecting a fake `HttpClient` / `HttpMessageHandler`. + +## Agents + +- **maxio-plan** — planner. Clones the SDK, grounds every contract in the map that ships with the + SDK source (opening the one source file the map names only when the map can't settle a fact), and + writes a contract-grounded `maxio-plan.md` before any code is written. No MCP. +- **maxio-debug** — diagnoses and fixes Maxio code in place, map-first (its first step on an + SDK-name error is the map row, from the session clone), verifying with `dotnet build` / + `dotnet test`. No MCP. + +## Install + +``` +/plugin marketplace add apimatic/plugin-marketplace +/plugin install maxio-sdk-clone@apimatic +``` + +Then ask a usage question (e.g. *"how do I authenticate this SDK with an API key?"*) to trigger the +relevant skill. diff --git a/plugins/maxio-sdk-clone/agents/maxio-debug.md b/plugins/maxio-sdk-clone/agents/maxio-debug.md new file mode 100644 index 0000000..549d6a7 --- /dev/null +++ b/plugins/maxio-sdk-clone/agents/maxio-debug.md @@ -0,0 +1,75 @@ +--- +name: maxio-debug +description: Diagnoses and fixes Maxio Advanced Billing .NET SDK failures — compile errors on SDK names, runtime exceptions, provider errors, unexpected API responses. Its first step for a failing SDK name is the map row; it opens SDK source only when the map row already matches the code or genuine ambiguity remains. Use whenever a build or runtime error involves MaxioAdvancedBilling types or Maxio API behaviour. Reaches the SDK map and source via the session clone (the map ships inside the SDK source) — reuses the clone maxio-plan recorded in a shared temp .maxio-session.md, cloning the map-carrying ref itself when none exists. +color: orange +skills: + - maxio-getting-started +tools: Read, Write, Edit, Grep, Bash, Skill +--- + +You are the Maxio Advanced Billing .NET SDK debugging specialist. You fix Maxio failures +in place, grounded ONLY in the SDK **map and source in your clone** (the map ships with the +source, at the clone root) and the companion `dotnet-*` skills — never model memory, never +the installed NuGet package (no decompiling/reflection), never the SDK's `api-reference.md`, +never the web. + +## The procedure for SDK-name compile errors (non-negotiable order) + +For every compiler error naming an SDK symbol (`CS1061`, `CS0117`, `CS0234`, `CS0104`, +`CS1503`, `CS7036`, … on `MaxioAdvancedBilling.*`): + +1. **Map row first.** The map ships in the SDK source, so you need the clone to read it: + check the `## Session artifacts` section of the shared session file at + `/maxio-sdk-src/.maxio-session.md` — `maxio-plan` usually cloned already this + session, so reuse that path; if none is recorded, clone once per `maxio-getting-started`'s + *SDK source & map* section (see step 2). Then find the symbol's row in the SDK map + (`/sdk-map.md` → operations/records/enums pages). If the code contradicts the map + (wrong field name, missed response envelope, wrong param order), fix the code from the map + row. Response envelopes are the classic case: response types wrap their payload in a single + field (`ProductResponse.Product`, `SubscriptionResponse.Subscription`) — reads must go one + level down. +2. **If the map row matches the code, or ambiguity remains: open the source.** Reuse the + session clone recorded in `/maxio-sdk-src/.maxio-session.md`. If none exists yet, + clone the SDK yourself per `maxio-getting-started`'s *SDK source & map* section: a shallow + clone of the **map-carrying ref** into a fresh timestamped folder under + `/maxio-sdk-src/`; then create `.maxio-session.md` in `/maxio-sdk-src/` (NOT + the project repo) if absent and **record the clone path in its `## Session artifacts` + section**, so every later spawn (plan or debug) reuses it instead of re-cloning. The map + and source live in the same clone at the same ref, so they match by construction. The + session file and the clone live in temp, never in the project repo — the main agent must + never see the clone or its path. Then **open the exact file the map row names** — nothing + else, no directory-wide greps or scans. Fix the code from what the source actually declares. +3. **Never re-guess.** Rewriting the failing code from the same knowledge that produced + the error is prohibited — that is how the error happened. Each failing symbol gets a + map/source-grounded answer before its line changes. Never mutate payloads, field + names, or status handling speculatively to "see if it works". + +## Runtime / provider errors + +- Read the provider's error payload through the documented path — the operation's error + case and `TryGet…` accessors from its map row; `dotnet-error-handling` for the + Case A/B mechanics. Don't parse exception `.ToString()` text when an accessor exists. +- Config-shaped failures (401, wrong host, timeouts): check auth (Basic — username = + API key, password = literal `"x"`), the server-node/base-URL configuration, and retry + semantics via `dotnet-client-initialization` / `dotnet-configuration-resilience` + before touching call sites. +- Unexpected response *content* from the live API is evidence, not something to code + around silently — report it as a finding if it contradicts the map/spec. + +## Verify and return + +Rebuild (`dotnet build`) after your fixes; run the tests covering the touched code +(`dotnet test`) when they exist. **If you started any app/server process to diagnose (e.g. +`dotnet run`, a background host or listener), stop it before returning — never leave it +running for the main agent: a live `dotnet run` holds a lock on the build output, so the +main agent's next `dotnet build` fails with a file-in-use error.** Leave only the SDK clone +in place (the session may reuse it). + +Your final message is a tight report: **root cause** (one sentence per distinct cause) · +**fix applied** (what changed and why it's correct, citing the map row or the SDK source +file it is declared in — but NEVER the clone's filesystem path; the main agent must not +receive or use it) · **files touched** (project files only) · **unresolved blockers** +(empty if none — never invent certainty). +If you corrected contract-sheet rows in `maxio-plan.md`, include the corrected rows +VERBATIM in the report — the main agent works from your reply and must not re-read +the plan file. No transcript-style narration, no reference dumps. diff --git a/plugins/maxio-sdk-clone/agents/maxio-plan.md b/plugins/maxio-sdk-clone/agents/maxio-plan.md new file mode 100644 index 0000000..9c3e9ba --- /dev/null +++ b/plugins/maxio-sdk-clone/agents/maxio-plan.md @@ -0,0 +1,143 @@ +--- +name: maxio-plan +description: Produces a map-grounded Maxio Advanced Billing .NET SDK integration plan with a CONTRACT SHEET — exact signatures, wire names, envelope shapes, error accessors, and enum values for every operation in scope — before any code is written. Also answers single narrow SDK-contract questions directly. Clones the SDK (whose source ships the map) as its first step and grounds every fact in that map, opening the exact source file the map names only when the map genuinely cannot settle a fact. Use before implementing any Maxio feature, or whenever an SDK fact is needed mid-implementation. Main agent — never open SDK source, the NuGet cache, a decompiler, or the SDK clone yourself; route SDK-contract needs here. +color: blue +skills: + - maxio-getting-started +tools: Read, Grep, Skill, Write, Edit, Bash +--- + +You are the Maxio Advanced Billing .NET SDK planning specialist. Your single source of +truth is the **SDK map that ships inside the SDK source you clone** (`/sdk-map.md` + +`/map/operations/*.md` + `/map/models/*.md`) plus the companion `dotnet-*` +skills for usage traps. Your training data on this SDK is stale — every fact you emit must +come from a map page (or, under the guard below, a source file) you actually read this +session. You never guess, and you never open the SDK's `api-reference.md`. + +**Clone the SDK first.** The map is not bundled in the plugin — it travels with the source. +Follow `maxio-getting-started`'s *SDK source & map* section: reuse the clone recorded in the +shared session file `/maxio-sdk-src/.maxio-session.md` if one exists this session, +otherwise clone once (shallow, the map-carrying ref) into a fresh timestamped folder under +`/maxio-sdk-src/` and record its path there. The clone and its path stay in temp, +**never** in the project repo, and the clone path **never** goes into `maxio-plan.md` — the +main agent must not see it. + +**Map first; source only on a real map gap — the guard (non-negotiable).** + +- Open a source file only when the map genuinely can't settle the fact: it is absent or + ambiguous in the map, or a map-named type/member doesn't line up. +- When you do, open the **exact file the map's row names** (the map gives the path, e.g. + `Api/Customers.cs`, `Models/CreateCustomerRequest.cs`) and read it **scoped** — Read with + an offset/limit, or Grep on the one symbol. +- **Grepping, globbing, or `find`-ing over the clone tree to *locate* something is a + defect.** The map is the locator; you go map row → the named file, never search → file. + +**Your output never leaves a contract fact open for "whoever implements."** The map answers +nearly everything; for the rest you resolve it from the source in your clone. For the rare +in-scope fact even the source cannot settle: + +- if only live traffic could confirm it (e.g. whether the live wire payload really matches a + generated model), convert it into a concrete defensive-coding directive on the sheet — + "extract best-effort, fall back to the generic message" — and label the uncertainty + `UNVERIFIED`. (`SOURCE-LOOKUP NEEDED` punts stay abolished — an open row is how the main + agent ends up opening source itself; you resolve source-level facts here, from the clone.) + +When a brief asks how far a contract can be trusted, the trust judgment may cite ONLY +evidence visible in the map or SDK source (e.g. two generated definitions that disagree, +a suspicious shared model) — never training-data memory of this API, and never claims +about what the live wire "usually" sends. Anything only live traffic can confirm is +labeled unverified. + +Your Read/Grep operate on: the map and the map-named source files inside your SDK clone, the +`dotnet-*` companion skills, and `maxio-plan.md`. Never Read or Grep project code, or scan +anywhere else on the filesystem. Where a companion skill says "read/open the SDK source", +resolve the fact from the map first and open the one file the map names only under the guard +above. + +## Two modes + +**Narrow-question mode** — the spawn prompt (or a follow-up message to you after a +plan) asks one or more specific contract questions (a field name, a signature, an +enum's values, which error type an operation throws): clone the SDK if you haven't this +session (or reuse the session clone), look them up in the map, and answer in your reply. +No file, no plan, just the grounded answers, each with the map page (or source file, if the +guard sent you there) it came from. When several questions arrive batched, answer them all +in one reply. + +**Plan mode** — the spawn prompt describes implementation work: clone/reuse the SDK, ground +against the map, and produce `maxio-plan.md` (the only project-repo file you ever write) +**at the exact path your brief dictates** — never pick your own location. If the brief forgot +to dictate a path, default to `/maxio-plan.md` and say in your return that +you used the default. Return that path plus a one-paragraph summary. Do not modify project +code, run builds, survey the repo, or plan non-Maxio repo work — that is the main agent's job. +Your Bash is for cloning/reading the SDK only, not for building or touching the project. + +**Revision mode** — when messaged or re-spawned with a clarification, correction, or +gap: revise `maxio-plan.md` in place AND reply with ONLY the changed/added rows +verbatim (plus one sentence of context). The caller works from your reply and never +re-reads the file — a reply that says "see the updated file" defeats the design. +Revise with targeted **Edit** operations — edit the changed rows, append the new +section. Re-Writing the whole file to change a few rows is a defect: Write is for +the file's initial creation only. + +## How to ground (map-first, one pass) + +1. Load `maxio-getting-started`; clone the SDK per its *SDK source & map* section (reuse the + session clone if present); open `/sdk-map.md` (the index). +2. From the index, open the **operations pages** for every controller in scope — take + signatures (parameter order + types, nullables that must be passed), return types, + error case (A: typed `SdkException<{Op}Error>` with its `TryGet…` accessors and + payload type / B: `SdkException`), and pagination. +3. Open (or Grep, scoped) the **records pages** for every request/response model you + will reference — field names WITH wire names, required flags, nullability, and the + envelope shape (responses wrap their payload: e.g. `ProductResponse` has exactly one + field, `Product`). Get enum value lists from `/map/models/enums.md`; unions from + `unions.md`. +4. When the map can't settle a fact (absent/ambiguous, or a member's exact declared type the + row names but doesn't spell out), open the **one source file the map names** — under the + guard above — and resolve it. Never scan the tree to find it. +5. Pull the relevant traps from the companion skills for the features in scope (named + arguments for long parameter lists, envelope pattern on writes, `StringEnum` + read-back semantics, auth = Basic with username = API key / password = `"x"`, server + nodes and base-URL override, retry semantics) and fold them into the plan as one-line + notes at the step where they bite. +6. Collect everything in ONE pass — the whole point is that the implementer never has to + rediscover a contract mid-coding. + +## maxio-plan.md format (keep it tight — tables, not prose) + +1. **Scope & sequence** — the implementation steps in order, each naming the operations + it uses. +2. **CONTRACT SHEET** — open the section with these two literal warning lines: + > **Signatures are generated code, verbatim — every parameter name is the literal + > C# identifier. The cancellation-token parameter really is named `ct`: in named + > arguments write `ct:`, never `cancellationToken:`.** + > + > **Every SDK type is written fully-qualified with the namespace the map gives it** + > (e.g. `MaxioAdvancedBilling.Models.Enums.SubscriptionState`, + > `MaxioAdvancedBilling.Models.AnyOf.SubscriptionIdOrReference`, + > `MaxioAdvancedBilling.Core.Authentication.Basic.BasicAuthCredentials`, and the + > **client-config types**: `MaxioAdvancedBilling.Servers.ServerEnvironment`, + > `MaxioAdvancedBilling.Core.Configuration.RetryOptions`, + > `MaxioAdvancedBilling.Core.Configuration.ServerOptions`). The map carries these + > namespaces (a members table names the namespace, or a row gives the source path + > `Core/Configuration/…` ⇒ namespace `MaxioAdvancedBilling.Core.Configuration`) — do not + > drop them to the root or `.Models`, or the implementer guesses the wrong `using` and the + > build breaks. + Then one table row per operation: controller property · method + signature (params in order, types, required-but-nullable flags) · request model + + its fields (`Name (wire_name): type, required?`) · response envelope + the inner + fields the integration reads · error case A/B + accessors + payload type · + pagination. Below it: the enum value tables actually needed, and the client + construction/auth/server-node facts. +3. **Trap notes** — the one-line skill-derived warnings, attached to specific steps. +4. **Assumptions & Blockers** — anything you had to assume about the user's intent, and + anything that blocks planning. An empty section is a valid outcome; an invented fact + is not. +5. Every sheet row cites its map page by its logical name (e.g. `operations/Subscriptions.md`, + `records-4-Su-We.md`) — **never** the clone's filesystem path — so the implementer can ask + you for one targeted lookup if a detail is ever in doubt. + +Keep the file lean: no copied map pages, no full model dumps, and no clone path — only the +operations and fields the scope actually touches. Your final message: the file path, a +one-paragraph summary, and the Assumptions & Blockers list verbatim. diff --git a/plugins/maxio-sdk-clone/skills/dotnet-authentication/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-authentication/SKILL.md new file mode 100644 index 0000000..0688a72 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-authentication/SKILL.md @@ -0,0 +1,145 @@ +--- +name: dotnet-authentication +description: Configure authentication on an APIMatic-generated C#/.NET API client — each scheme is a nullable credentials property on the options class (set it before constructing the client, or inside the Add{Api}Client DI callback) — Basic (BasicAuthCredentials), Bearer token, API key (header/query/cookie), and OAuth 2.0 (client-credentials, authorization-code+PKCE, password) plus combined AND/OR schemes and no-auth. Use the moment you set credentials, an API key, a token, or OAuth on any APIMatic .NET SDK, or need to know which schemes its options class exposes — load it even after reading the options class in the source, since the property type doesn't tell you when to set it or that secrets belong in configuration. +--- + +# Authenticating an APIMatic .NET SDK client + +How you authenticate depends on the security scheme(s) the API uses. APIMatic surfaces each scheme as a +**nullable credentials property on the options class**; set the one(s) your API uses, then construct the +client (see `dotnet-client-initialization`). + +> Throughout this skill, `{...}` is a placeholder for a name you take from your SDK (e.g. `{RootNamespace}`, +> `{Api}ClientOptions`, `{BasicAuthProperty}`) — replace it with the concrete identifier from the source. + +To see which schemes a specific SDK accepts, read the **credentials properties on its `{Api}ClientOptions` +class** — those are the source of truth (read the class in the SDK source, not a decompiled or reflected +view of the installed package). The `{RootNamespace}.Core.Authentication` folder ships *every* +scheme class as shared runtime code regardless of what the API accepts, so rely on the options class rather +than that folder. (An SDK whose API uses only Basic, for instance, exposes a single +`options.{BasicAuthProperty}` of type `BasicAuthCredentials`.) + +The credential classes below live under `{RootNamespace}.Core.Authentication.*` and are the **same across +all APIMatic .NET SDKs**; only the **options property names** are generated per-API (hence the +`{...Property}` placeholders). + +## Basic auth + +```csharp +using {RootNamespace}.Core.Authentication.Basic; + +options.{BasicAuthProperty} = new BasicAuthCredentials +{ + Username = "...", + Password = "..." +}; +``` + +Sends `Authorization: Basic base64(username:password)`. + +## Bearer token + +Set the configured token property on the options class to your access-token string: + +```csharp +options.{BearerAuthProperty} = "ACCESS_TOKEN"; +``` + +Sends `Authorization: Bearer ACCESS_TOKEN`. + +## API key (header, query, or cookie) + +The key is sent as a header, query parameter, or cookie — its placement and name are fixed by the generated +scheme. Set the configured key property to your key string: + +```csharp +options.{ApiKeyProperty} = "API_KEY"; +``` + +## OAuth 2.0 — client credentials (machine-to-machine) + +```csharp +using {RootNamespace}.Core.Authentication.OAuth2.ClientCredentials; + +options.{OAuthProperty} = new OAuth2ClientCredentials +{ + ClientId = "...", + ClientSecret = "...", + Scope = "..." // optional +}; +``` + +The SDK fetches and caches the token, acquiring a fresh one when it expires; on a `401` it invalidates the +cached token and re-acquires. + +## OAuth 2.0 — authorization code (3-legged, with PKCE) + +```csharp +using {RootNamespace}.Core.Authentication.OAuth2.AuthorizationCode; + +options.{OAuthProperty} = new OAuth2AuthorizationCodeCredentials +{ + ClientId = "...", + ClientSecret = "...", // optional; needed only when PKCE is disabled (Pkce = null) + RedirectUri = "https://app.example.com/callback", + Scope = "...", // optional + State = "...", // optional CSRF token + Pkce = PkceMethod.S256, // default; RFC 7636 + PromptForAuthorizationCode = async (authorizationUrl, ct) => + { + // Open/redirect the browser to authorizationUrl, then return the + // authorization code your redirect endpoint received. + return await GetCodeFromUserAsync(authorizationUrl, ct); + } +}; +``` + +The SDK exchanges the code for a token and refreshes it when it expires; if the refresh fails, it invokes +`PromptForAuthorizationCode` again to re-authorize. + +## OAuth 2.0 — resource owner password + +```csharp +using {RootNamespace}.Core.Authentication.OAuth2.Password; + +options.{OAuthProperty} = new OAuth2PasswordCredentials +{ + ClientId = "...", + ClientSecret = "...", // optional + Username = "...", + Password = "...", + Scope = "..." // optional +}; +``` + +## Token caching & refresh (all OAuth2 grants) + +- Tokens are cached in-memory and reused until ~30s before expiry. +- Refreshable grants (those that return a refresh token) refresh automatically; otherwise a new token is + acquired. +- On `401`, the cached token is invalidated and re-acquired on the next call. + +## Combined / multiple schemes + +When an operation (or the whole API) requires more than one scheme, APIMatic composes them: + +- **AND** — all schemes are applied to every request (`AuthSchemeAll`). +- **OR** — the first scheme that succeeds is used; if all fail, an `AuthSchemeException` is thrown + (`AuthSchemeAny`). + +You configure this by setting the relevant credentials properties on the options class; the generated +client wires the AND/OR composition for you. + +## No auth + +Some endpoints/APIs need no credentials (`NoneAuthScheme`) — leave the credentials properties unset. + +## Notes + +- A given SDK only exposes the credentials properties for the schemes its API uses; those names are + generated per-API (hence the `{...Property}` placeholders above). +- Set credentials **before** constructing the client, or inside the `Add{Api}Client(options => ...)` + callback when registering via DI. +- Keep secrets out of source — load them from configuration (environment variables, a secret store, or any + other `IConfiguration` source) instead of hardcoding, either inside the `Add{Api}Client(options => ...)` + callback for the host or via a `ConfigurationBuilder()...Build()` chain for a console app. diff --git a/plugins/maxio-sdk-clone/skills/dotnet-calling-endpoints/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-calling-endpoints/SKILL.md new file mode 100644 index 0000000..d0920d1 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-calling-endpoints/SKILL.md @@ -0,0 +1,213 @@ +--- +name: dotnet-calling-endpoints +description: Call API operations on an APIMatic-generated C#/.NET SDK — method signature and parameter-order conventions, building request-model records, string-enums, passing path/query/body params + a CancellationToken, reading the varied response shapes, and the optional non-throwing result-style call. Use whenever invoking an endpoint, building a request body, working out parameter order or named arguments, or consuming a response from any APIMatic .NET SDK — load it even after reading the method signature in the source, since the signature doesn't warn you that list/search ops mis-bind positionally and need named arguments. +--- + +# Calling endpoints on an APIMatic .NET SDK + +Operations are **async methods** on the client. Most are **grouped under a controller property** and called +`client.{ApiGroup}.{Operation}(...)`; an operation that belongs to no group sits **directly on the +client**, called `client.{Operation}(...)`. Open the client class in the SDK source to see its controller +properties (and any direct operations), then open the relevant controller (or the client) for the +operation's exact signature. Operation names follow no fixed verb/resource pattern — take the real name from +the source. + +> Throughout this skill, `{...}` is a placeholder for a name you take from your SDK (e.g. `{ApiGroup}`, +> `{Operation}`, `{Resource}`, `{EnumType}`) — replace it with the concrete identifier from the source. + +## Method signature convention + +Every endpoint method is `async` (returns a `Task`) and lays its parameters out in a fixed order: + +```csharp +public Task<{ReturnType}> {Operation}( + {non-defaulted params}, // no C# default value — listed first + {defaulted params} = {default}, // have a C# default (e.g. = null, = 1d) — may be skipped + CancellationToken ct = default); // always last +``` + +- **Parameter order is fixed:** parameters **without a default value come first**, then parameters **with a + default value**, then `CancellationToken ct = default` last (C# requires defaulted parameters to follow + non-defaulted ones). +- **An optional parameter may still have no C# default.** Many nullable query params are generated without a + `= null` default (e.g. `string? startDate`), so they sit in the leading group and must be passed + explicitly (as `null`) in a positional call — which is why named arguments matter (see below). +- **The signature is the source of truth.** Whether a parameter is nullable, required, or defaulted — and + whether the operation takes a body — varies per operation. Path params are typically + non-nullable primitives listed first; query and body params may be required or optional. Read the actual + signature in the SDK source (`public Task<...> {Operation}(...)`) for each operation. +- **Return type** varies by operation — see [Reading the response](#making-the-call-and-reading-the-response). +- Methods are **async-only** (no sync overloads) and **throw `SdkException`** on API errors — see + `dotnet-error-handling`. + +## Use named arguments for list/search endpoints + +List/search operations can have **many** optional parameters in a **fixed positional order**, and many of +the leading nullable ones have **no default value** — so you cannot skip them positionally. + +Call these methods with named arguments. A positional call reconstructed from memory or an incomplete view +of the signature mis-binds arguments (wrong order, or a missing non-defaulted arg before the first defaulted +one), so it either fails to compile or sends the wrong request; named arguments are order-independent and +avoid this. When copying: + +- Copy parameter names and order from the C# method signature (`public Task<...> {Operation}(...)`), not + from the internal `new Param("...", ...)` builder list inside the method body — that list is ordered + differently and uses snake_case wire names. +- Copy each name verbatim from the signature; they are easy to misremember (singular vs plural, etc.). + +```csharp +// Named args: order-independent; omitted optionals fall through to null / their defaults. +var response = await client.{ApiGroup}.{Operation}( + status: {EnumType}.SomeConstant, + someFilterId: 12345d, + someFlag: true, + page: 1d, + perPage: 100d, + ct: ct); +``` + +## Building request models + +Request bodies are immutable `record`s built with object-initializer syntax (no builders). `required` +members must be set; optional ones are nullable and are omitted from the JSON when left null. The request +type is the type of the operation's `body` parameter — take its exact name from the method signature in the +SDK source: + +```csharp +var body = new {RequestType} +{ + RequiredProp = value, // 'required' members must be provided + OptionalProp = value // nullable; leave unset to omit from the request +}; +``` + +A request body's **shape varies**: some are **flat** (scalar members directly on the record), others **nest +an inner resource record** (whose type you likewise read from the source). Open +the request model (under `Models/`) to see its real `required`/optional members. A nested body looks like: + +```csharp +var body = new {RequestType} +{ + {Member} = new {InnerType} + { + RequiredProp = value, + OptionalProp = value + } +}; +``` + +## Enums + +Enums are type-safe string- **or int-**enums (`StringEnum` / `IntEnum`), not C# enums — use the +static constants, or `FromValue(...)` for a value not known at compile time. See **dotnet-models** for +read-back semantics (they convert to their underlying value; `==` compares by value; guard unknowns). + +```csharp +SomeProp = {EnumType}.SomeConstant; +SomeProp = {EnumType}.FromValue("server_provided_value"); +``` + +## Union types, collections, and dates + +Some properties are not plain scalars: polymorphic `OneOf`/`AnyOf` unions (built with **factory methods**, +not object-initializers, and read via `TryGet…`), `IReadOnlyList`/`IReadOnlyDictionary` collections, and +`DateTimeOffset` dates. If a request property or response field is one of these, see **dotnet-models** for +how to construct and read it. + +## Making the call and reading the response + +```csharp +var response = await client.{ApiGroup}.{Operation}(pathArg, queryArg: null, body: body, ct: ct); +``` + +> **Wrap the call in error handling — a non-2xx response *throws*, it is not signalled by the return value.** +> The bare `await` above shows only the happy path; on an API error the call throws `SdkException`. +> Before writing a real call, **load `dotnet-error-handling`** for how to wrap it — the `try/catch` shape and +> which `TError` to catch per operation — or use the non-throwing `{Operation}Result` variant (below). + +**Each operation's return type varies** — the shape, and even the type's name, differ by operation. Read the +method's return type in the SDK source and handle it accordingly. The cases you'll meet: + +- **An object that nests the resource** under a property (a record whose member holds the inner resource). + Unwrap that member: + ```csharp + var resource = response.{Resource}; // the property holding the inner resource + Console.WriteLine(resource?.SomeField); + ``` +- **The resource directly** — `Task<{Resource}>`: use it as-is, nothing to unwrap. + ```csharp + var resource = await client.{ApiGroup}.{Operation}(...); + ``` +- **An array** — `Task>`: iterate it, unwrapping each item too if the items are + themselves nesting objects. +- **An object that nests an array** — a record whose single member is an `IReadOnlyList<...>`. Read that + member first, then iterate. +- **Nothing** — non-generic `Task`: no body; just `await` it. + +Endpoints in the same family can differ — one nests the resource, another returns it directly — so let each +method's return type guide how you read it. + +An operation may also expose an optional **`{Operation}Result`** sibling that returns +`ApiResult` — the outcome (the response, or a typed/`RawError` error) instead of +throwing, with the HTTP status and headers available on both. It's optionally generated, so it may not +exist. See **dotnet-error-handling**. + +## Cancellation + +Every operation takes a `CancellationToken` as its last argument, passed as `ct:`. To bound an individual +call with a timeout, use the per-request cancellation pattern in **dotnet-configuration-resilience** (it +owns timeouts). + +## Worked example — a list/GET call + +```csharp +// Signature (illustrative): +// Task> {Operation}( +// {EnumType}? filter, string? startDate, string? q, +// double? page = 1d, double? perPage = 20d, CancellationToken ct = default); + +var results = await client.{ApiGroup}.{Operation}( + filter: {EnumType}.SomeConstant, + startDate: null, + q: "search text", + page: 1d, + perPage: 20d, + ct: ct); + +foreach (var item in results) +{ + var resource = item.{Resource}; + Console.WriteLine(resource?.Id); +} +``` + +> This operation returns an **array** directly, so you iterate and unwrap each item. Other operations nest +> the array inside an object (a record with one list member) — there you read that member first +> (`foreach (var item in response.{Items})`), then iterate. Check the method's return type. + +## Finding the right method in the SDK source + +Look the operation up in the SDK map (the `sdk-map.md` index the entry-point skill points you to, with its +per-controller pages): the map row already carries the exact signature, return type, error type, and +pagination, and the page header names the source file. Take that file path and open it directly in the +clone — do **not** grep or scan the clone to find it. Collect every operation you'll call in **one** map +pass and keep the contracts in your working notes, rather than returning to the map per call. + +When you open that file for a full body, read from the SDK **source** (clone the SDK's source repo to a temp +dir if you haven't), not by decompiling or reflecting over the installed package — the source has the +XML-doc comments and the internal `new Param(...)` builder list a compiled assembly drops, and reading a +`.cs` file is faster than running reflection. + +- Most operations are grouped on **controller properties** of the client (each defined in + `Api/{ApiGroup}.cs`); an operation in no group is defined **directly on the client class**. The map's + controller page names the exact file — open it directly. +- Each method's XML-doc comment documents its parameters and the endpoint path; read it to confirm which + params are required and what the body/return types are. +- Request/response/enum types live under `Models/` (and `Models/Enums/`, with unions under `Models/AnyOf/` + and `Models/OneOf/`); error types under `Errors/`. + +## Next + +- Errors and status codes → **dotnet-error-handling** +- Pagination, retries, timeouts → **dotnet-configuration-resilience** +- Union types, collections, dates, enums → **dotnet-models** diff --git a/plugins/maxio-sdk-clone/skills/dotnet-client-initialization/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-client-initialization/SKILL.md new file mode 100644 index 0000000..6b53651 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-client-initialization/SKILL.md @@ -0,0 +1,120 @@ +--- +name: dotnet-client-initialization +description: Initialize an APIMatic-generated C#/.NET API client — you construct it from an HttpClient you supply (the SDK doesn't own it; reuse one long-lived instance or an IHttpClientFactory, not one per request) plus an options object, choose a server environment/base URL, and DI-register via the generated Add{Api}Client extension. Use the moment you write `new {Api}Client(...)`, build its options, pick an environment, set up the HttpClient/client lifetime, or register the client in dependency injection — load it even after reading the constructor in the SDK source, since the signature shows the arguments but not the lifetime/reuse rules or DI wiring. +--- + +# Initializing an APIMatic .NET SDK client + +This applies to **any** APIMatic-generated .NET SDK. Replace placeholders with the real names from the +SDK you are using: + +- `{Api}Client` — the single public client class (e.g. `FooClient`). +- `{Api}ClientOptions` — its options class. +- `{RootNamespace}` — the SDK's root namespace, used in `using` directives. This can differ from the NuGet + package id (you install by the package id but `using` the namespace). + +## Shape of the client + +APIMatic .NET SDKs expose **one public client class** constructed from an `HttpClient` and an options +object: + +```csharp +public {Api}Client(HttpClient httpClient, {Api}ClientOptions options) +``` + +Operations are exposed on the client. Most are grouped under **controller properties** (one per API resource +group) and called `client.{ApiGroup}.{Operation}(...)` — for example, a `Widgets` controller's +`ListWidgets` operation is `client.Widgets.ListWidgets(...)`. An operation that belongs to no group sits +**directly on the client**, called `client.{Operation}(...)`. Open the client class **in the SDK source** +(not a decompiled or reflected view of the installed package) to see the available controller properties +(and any direct operations). See `dotnet-calling-endpoints`. + +The options class always carries these knobs (auth properties vary per API — see +`dotnet-authentication`): + +```csharp +public class {Api}ClientOptions +{ + public ServerEnvironment Environment { get; set; } = ServerEnvironment.Default(); + public RetryOptions Retry { get; set; } = RetryOptions.Default(); + public ServerOptions Server { get; set; } = new(); + // + one nullable credentials property per auth scheme the API declares +} +``` + +Tuning these knobs — `Retry` (retries, backoff, per-attempt timeout) and `Server` / `Environment` (server +selection and **overriding the base URL**), plus pagination and logging — is covered in +**dotnet-configuration-resilience**. + +## Direct instantiation + +```csharp +using {RootNamespace}; +using {RootNamespace}.Servers; + +var options = new {Api}ClientOptions +{ + Environment = ServerEnvironment.Default(), // pick the environment your API exposes + // ...set the auth credentials property your API uses (see dotnet-authentication) +}; + +var httpClient = new HttpClient(); // reuse a single long-lived instance +var client = new {Api}Client(httpClient, options); +``` + +### HttpClient lifetime + +The SDK does **not** own the `HttpClient` — you provide it. Reuse one instance for the app's lifetime +(or use `IHttpClientFactory`); do not create one per request. Attach custom `HttpMessageHandler`s here for +logging, proxies, or custom TLS (see `dotnet-configuration-resilience`). + +The client itself is also meant to be **long-lived** — construct it once and reuse it for the app's +lifetime (it's just lightweight controller wrappers over the shared HTTP pipeline). Don't build a new +client per request or per call. + +## Choosing the server / base URL + +Environments are modeled as a `ServerEnvironment` string-enum with one constant per environment the API +defines (e.g. `ServerEnvironment.Production`, or region constants). Select one on `options.Environment`. +Overriding a templated parameter or the base URL is nested **per server AND per environment** — +`options.Server.{ServerName}.{Environment}.BaseUrl` (with any templated params at the same level), NOT +directly on `ServerOptions`. **dotnet-configuration-resilience** documents this in full and owns +server / base-URL configuration. Inspect `Servers/ServerEnvironment.cs` and `Servers/{ServerName}Options.cs` +for the exact constants and template parameters of your API. + +## Dependency injection (ASP.NET Core / generic host) + +Every APIMatic .NET SDK ships a `ServiceCollection` extension named `Add{Api}Client`, which registers the +client (transient — fine, because the expensive `HttpClient`/handler pipeline it wraps stays long-lived +and shared via the factory) and wires an `IHttpClientFactory`-managed `HttpClient` (it resolves the **default, +unnamed** factory client, and the `options` you configure are captured once at registration): + +```csharp +using {RootNamespace}; + +builder.Services.Add{Api}Client(options => +{ + options.Environment = ServerEnvironment.Default(); + // options.{Scheme} = new {Scheme}Credentials { ... }; +}); +``` + +To attach custom `DelegatingHandler`s (logging, proxies, custom TLS) under this DI registration, configure +the **default, unnamed** factory client it resolves — e.g. +`services.AddHttpClient(Options.DefaultName).AddHttpMessageHandler(() => new MyHandler());`. See +**dotnet-configuration-resilience**. + +Then inject it: + +```csharp +public sealed class MyService({Api}Client client) +{ + public Task DoWork() => client.{ApiGroup}.{Operation}(/* ... */); +} +``` + +## Next + +- Configure authentication → **dotnet-authentication** +- Make your first call → **dotnet-calling-endpoints** +- Tune retries/timeouts/logging → **dotnet-configuration-resilience** diff --git a/plugins/maxio-sdk-clone/skills/dotnet-configuration-resilience/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-configuration-resilience/SKILL.md new file mode 100644 index 0000000..8afcbcd --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-configuration-resilience/SKILL.md @@ -0,0 +1,229 @@ +--- +name: dotnet-configuration-resilience +description: Tune an APIMatic-generated C#/.NET SDK client — RetryOptions retries/backoff (retries cover idempotent GET/HEAD/PUT/OPTIONS only by default, and Timeout is per-attempt, not total), per-request timeout/cancellation, auto-paginate list operations via IAsyncEnumerable, consume Server-Sent Events (SSE) streams with idle-timeout handling, override the base URL/server, and add request/response logging by attaching a DelegatingHandler (there's no built-in logging hook). Use whenever adjusting retry policy, timeouts, the base URL, paging through results, streaming/SSE, or adding logging to any APIMatic .NET SDK — load it even after reading the options in the source, since the fields don't reveal that POST/DELETE aren't retried, Timeout is per-attempt, or that only marked operations auto-paginate. +--- + +# Configuration & resilience for an APIMatic .NET SDK + +All types below live under `{RootNamespace}.Core.Configuration` / `.Servers` and are generic across +APIMatic .NET SDKs. + +## ServerOptions configuration for each Environment + +`options.Server` (a `ServerOptions`) holds the server configuration **per environment**. It exposes one +`{ServerName}Options` per server the API defines, and each of those carries a nested options object for +**every environment** the API declares (matching the `ServerEnvironment` constants). You configure the +server on the environment you select via `options.Environment` — only that environment's options are read. + +Each environment's options expose what the SDK substitutes into that server's URL: any **templated +parameters** the API declares (a region/subdomain/port — names vary, and some APIs have none) plus the +**`BaseUrl`** template itself (always present and settable). Set whichever you need: + +```csharp +using {RootNamespace}.Servers; + +options.Environment = ServerEnvironment.{Environment}; + +// Set a templated parameter the API declares (names vary per API — region, subdomain, port, ...): +options.Server.{ServerName}.{Environment}.{ServerParam} = "..."; + +// Or override the BaseUrl outright — e.g. a mock server, proxy, or self-hosted gateway. +// A literal URL with no {placeholders} is used as-is: +options.Server.{ServerName}.{Environment}.BaseUrl = "https://my-host.example.com"; +``` + +Open `Servers/{ServerName}Options.cs` for the real server names, per-environment options, and template +parameters. See **dotnet-client-initialization** for selecting the environment. + +## Retries + +`RetryOptions` (built on Polly) is set on the options class via `options.Retry`. Defaults: + +| Setting | Default | +| --- | --- | +| `StatusCodesToRetry` | `408, 429, 500, 502, 503, 504` | +| `HttpMethodsToRetry` | `GET, HEAD, PUT, OPTIONS` (idempotent only) | +| `MaxRetries` | `3` | +| `Delay` | `1s` | +| `BackOffFactor` | `2` | +| `UseExponentialBackoff` | `true` | +| `MaxJitter` | `500ms` | +| `Timeout` | `100s` (**per attempt**) | +| `OnRetry` | `null` | + +Customize: + +```csharp +using {RootNamespace}.Core.Configuration; + +options.Retry = RetryOptions.Default() with +{ + MaxRetries = 5, + Timeout = TimeSpan.FromSeconds(30), + OnRetry = attempt => Console.WriteLine( + $"retry #{attempt.AttemptNumber} after {attempt.Delay}") +}; +``` + +Notes: +- The *n*th retry waits `Delay * BackOffFactor^(n-1) + random(0, MaxJitter)` — so the 1st retry waits + `Delay` (1s), the 2nd `Delay * BackOffFactor` (2s), and so on. Set `UseExponentialBackoff = false` for a + constant `Delay` between attempts. +- Anything not in `HttpMethodsToRetry` — i.e. `POST`, `PATCH`, `DELETE` — is **not** retried by default; + add a verb only if the operation is idempotent. +- Multipart/form-data requests are never retried. +- `Timeout` is **per attempt**, not total — to cap a whole call, use a `CancellationToken` (below). It is + nullable: set `Timeout = null` to disable the per-attempt timeout entirely. +- `OnRetry`'s `RetryAttempt` also carries `Reason` — `RetryReason.Status(HttpStatusCode)` or + `RetryReason.Failure(Exception)` — log it to record *why* each retry fired. + +## Per-request timeout / cancellation + +Pass a `CancellationToken` to bound an individual call regardless of retry policy: + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); +var response = await client.{ApiGroup}.{Operation}(/* ... */, ct: cts.Token); +``` + +## Pagination + +Operations the API marks as paginated are generated as methods that **return +`IAsyncEnumerable>` and auto-paginate** — the SDK fetches each page and advances the +paging state for you (offset, cursor, `Link`-header, or page-number, depending on the operation). Seed the +first page with the paging arguments, then `await foreach` the pages: + +```csharp +// The paging args (e.g. offset/limit, cursor/limit, or page/size) seed the FIRST page; +// the SDK advances them and stops when the API signals the end. +await foreach (IReadOnlyList<{Item}> pageItems in + client.{ApiGroup}.{Operation}(/* offset: 0, limit: 100, ... */, ct)) +{ + foreach (var item in pageItems) + Process(item); +} +``` + +Each step yields **one page** (a list of items) — nest a loop to walk items, or flatten as you prefer. A +failed page fetch throws `SdkException` mid-enumeration (see **dotnet-error-handling**). + +**No-throw variant.** Where generated, a sibling `{Operation}Result` returns +`IAsyncEnumerable>` — the same streaming, but each page is an `ApiResult` +you inspect instead of it throwing: + +```csharp +await foreach (var result in client.{ApiGroup}.{Operation}Result(/* ... */, ct)) +{ + if (result.TryGetResponse(out var pageResponse)) // the page (items + any cursor/link metadata) + { + // process pageResponse + } + else if (result.TryGetError(out var error)) + { + // handle the failed page; break to stop early + } +} +``` + +> Not every list endpoint is paginated. An operation with no pagination metadata is a plain list call +> (returns a list or a wrapper — see **dotnet-calling-endpoints**); to page one of those, drive its own +> `page`/`perPage` query params yourself and stop when a page returns fewer than `perPage` items. + +## Streaming (Server-Sent Events) + +Operations the API marks as streaming (`text/event-stream`) are generated to **return +`Task>`** — `await` the call once to open the stream, then `await foreach` the +frames as the server emits them. `{Item}` is `string` for a plain-text stream, or a typed model for a JSON +event stream. + +```csharp +using {RootNamespace}.Core.Exceptions; // SseException, SseTimeoutException, SseDeserializationException + +// await once to open the stream (an opening error surfaces here — see "Errors" below): +IAsyncEnumerable<{Item}> stream = await client.{ApiGroup}.{Operation}(ct); + +try +{ + await foreach (var frame in stream.WithCancellation(ct)) // each frame as the server emits it + Process(frame); +} +catch (SseTimeoutException ex) // no frame arrived within the idle-timeout window +{ + // ex.IdleTimeout — the window that elapsed +} +catch (SseDeserializationException ex) // a JSON frame didn't match {Item} +{ + // ex.RawFrame (offending payload) + ex.InnerException (the JsonException) +} +``` + +**Idle timeout.** A stalled stream is bounded by an **idle timeout** — the maximum wait **between frames** — +which throws `SseTimeoutException` (rather than hanging) when it elapses. This is **not** a client-options +property (there is no `StreamReadTimeout`); the idle window is a `TimeSpan?` carried on the SSE response +itself, defaulting to **none** — a null window disables the check. When it does fire, +`SseTimeoutException.IdleTimeout` reports the window that elapsed. + +**Errors** (all under `{RootNamespace}.Core.Exceptions`): +- **Before the stream opens** — the opening `await` throws `SdkException`, with `TError` the same + two-case shape as any operation: a typed `{Operation}Error` (Case A) or `RawError` (Case B), per what the + operation declares (see **dotnet-error-handling**). +- **While enumerating** — both of the following derive from a common `SseException` base (catch `SseException` + to handle either): + - `SseTimeoutException` — no frame arrived within the idle-timeout window; carries `IdleTimeout`. + - `SseDeserializationException` — a frame couldn't be deserialized to `{Item}` (JSON streams); carries the + `RawFrame` text and the underlying `JsonException` as `InnerException`. +- Retries do **not** apply once the stream is open; cancel via the `CancellationToken` + (`stream.WithCancellation(ct)`) to stop early. + +## Logging + +There is **no built-in logging hook**. Add logging by attaching a custom `DelegatingHandler` to the +`HttpClient` you pass to the client: + +```csharp +public sealed class LoggingHandler : DelegatingHandler +{ + protected override async Task SendAsync( + HttpRequestMessage request, CancellationToken ct) + { + Console.WriteLine($"--> {request.Method} {request.RequestUri}"); + var response = await base.SendAsync(request, ct); + Console.WriteLine($"<-- {(int)response.StatusCode}"); + return response; + } +} + +var httpClient = new HttpClient(new LoggingHandler { InnerHandler = new HttpClientHandler() }); +var client = new {Api}Client(httpClient, options); +``` + +With DI, the SDK's `Add{Api}Client` resolves the **default (unnamed)** `IHttpClientFactory` client, so attach +the handler to that one — register it and configure the default client *before* (or alongside) the SDK +registration: + +```csharp +services.AddTransient(); +services.AddHttpClient(Options.DefaultName).AddHttpMessageHandler(); +services.Add{Api}Client(options => { /* ... */ }); // resolves CreateClient() → the default client +``` + +The handler then runs on every SDK call. The `OnRetry` callback above is also a convenient place to observe +retry activity. + +### Verify on the wire (first run of any new integration) + +The handler above is not just for production logging — **run it on the first execution of any new call and +inspect the output.** Path/template params are not type-checked against the route (internally the value is +`object?` and the URL is built by `value?.ToString()` substitution), and on a **successful** response the +SDK returns only the deserialized body — it never surfaces the request URL or status (see +**dotnet-error-handling**). So a wrong verb, a leftover `{placeholder}`, or a mis-serialized path segment +**compiles cleanly** and produces no in-band signal; the only symptom is a runtime `404`/`422`. + +Checklist for the first printed request: +1. the **verb** matches the operation (a `404` on a path you believe exists often means the wrong method); +2. the **path** has no literal `{placeholder}` left unsubstituted; +3. each **path-param segment** is the value the API expects (e.g. the lowercase enum **wire value**, not a + C# member name or a mis-cased `FromValue("...")` input); +4. the query params you set actually appear in the query string. + +Gate the handler behind a debug flag once verified. diff --git a/plugins/maxio-sdk-clone/skills/dotnet-error-handling/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-error-handling/SKILL.md new file mode 100644 index 0000000..79b73a1 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-error-handling/SKILL.md @@ -0,0 +1,264 @@ +--- +name: dotnet-error-handling +description: Handle errors from an APIMatic-generated C#/.NET SDK — calls throw the generic SdkException, where TError is either a typed per-operation {Operation}Error or RawError directly (RawError — common for read/list/find/delete ops — has no TryGet accessors; read status/body straight off it), or use the optional non-throwing ApiResult variant to get the status code and response headers without catching. Use the moment you write a try/catch around a call, handle a non-2xx/error response, read a status code or rate-limit/Link headers, or want a no-throw result-style call on any APIMatic .NET SDK (e.g. Maxio Advanced Billing) — load it even after reading the thrown type in the source, since the type alone won't warn you about the RawError/TryGetRawError traps that make catch blocks subtly wrong. +--- + +# Error handling for an APIMatic .NET SDK + +> Throughout this skill, `{...}` is a placeholder for a name you take from your SDK (e.g. `{Operation}`, +> `{ApiGroup}`, `{RootNamespace}`) — replace it with the concrete identifier from the source. + +Endpoint methods **throw on non-success responses** by default (for a non-throwing alternative, see the +**`ApiResult`** section below). The thrown type is always the generic `SdkException` — but `TError` +comes in **two shapes**, depending on the operation: + +- **Typed model (Case A)** — a per-operation `{Operation}Error` (subclass of `ApiError`) exists under + `Errors/` for the operation; `TError` is that type and you read it with typed `TryGet*` accessors. +- **`RawError` (Case B)** — when the operation has no `{Operation}Error` type, `TError` is `RawError` + *directly*. `RawError` is **not** an `ApiError` and has **no** `TryGet*` / `TryGetRawError` accessors; you + read the status and body straight off `ex.Error`. This is common — many operations have no typed error + model and so throw `SdkException`. + +`SdkException` is declared `public sealed class SdkException : Exception` with **no** +`where TError : ApiError` constraint — which is exactly why `TError` can be either an `ApiError` model or a +`RawError`. + +These types live in **distinct** namespaces — `Core.*` is **not** a single namespace, so don't assume +`ApiError` sits with `SdkException` under `Core.Exceptions`: + +- `SdkException` → `{RootNamespace}.Core.Exceptions` +- `ApiError` **and** `RawError` → `{RootNamespace}.Core.ErrorResponse` +- the per-operation `{Operation}Error` models (e.g. `CreateWidgetError`) → `{RootNamespace}.Errors` + +So catching a typed (Case A) exception needs **three** usings — `Core.Exceptions`, `Core.ErrorResponse`, +and `.Errors`; a Case B catch needs only the first two (`Core.Exceptions` + `Core.ErrorResponse`). These +types are identical in shape across APIMatic .NET SDKs. + +## Catch the exception + +`SdkException` exposes a single property — `public required TError Error { get; init; }`, the parsed +error model. What `Error` *is* depends on the case (below). + +**Read the error directly off the strongly-typed `SdkException` — never use reflection.** The +concrete `TError` is known right there at the `catch` (Case A: the typed `{Operation}Error`; Case B: +`RawError`), so `ex.Error` and the accessors on it are reachable directly. Do **not** dig the body out via +reflection (`ex.GetType().GetProperty("Error")`, then discovering and `Invoke`-ing the `TryGet*` methods): +it compiles, but it is brittle glue that reinvents what a per-operation typed `catch` gives you for free — +the concrete type is already known, so no runtime discovery is needed. Catch the concrete +`SdkException<{Operation}Error>` (or `SdkException`) and read `ex.Error` straight off it. + +### Which `TError` does an endpoint throw? + +Answer this from the SDK map by lookup: each operation row names the error case (typed `{Operation}Error` +vs `RawError`) and, for Case A, lists the exact `TryGet…` accessors with the HTTP status each maps to — no +need to grep the clone or open the error class at all. + +In the source itself the same fact lives in the method's XML doc `` line — on hover / in +IntelliSense, and visible when you open the file: + +```csharp +/// of when the server returns an error response. +``` + +`SdkException{TResult}` is boilerplate (identical on every method — `{TResult}` is the doc-comment's generic +placeholder, **not** the type you catch). The type named after **`of `** is the actual +`TError`: + +- `… of …` → catch `SdkException<{Operation}Error>` (Case A). +- `… of …` → catch `SdkException` (Case B). + +Equivalently, read the source — open the `.cs` files rather than decompiling or reflecting over the installed +package: a `{Operation}Error` type exists under `Errors/` **only** for Case-A +operations; if there is no `{Operation}Error`, the operation throws `SdkException`. Guessing wrong +is a **compile-time** error (`SdkException` won't compile — no such type), not a silent +bug — so the compiler keeps you honest. + +### Case A — operation has a typed `{Operation}Error` model + +Handling a Case-A error is a **two-step, source-driven** process — you cannot write the `catch` block from +memory: + +1. **List *every* `TryGet...` accessor the operation's `{Operation}Error` declares.** The operation's map + row already lists them (with the HTTP status each maps to) — take them from there; they are the + `public bool TryGet...(out ...)` methods on the `{Operation}Error` type under `Errors/` if you open the + source to confirm. These accessors are generated per operation — one per response the operation maps — + and their names embed the body type. Expect a mix of: + - **typed-body accessors** named after a model or scalar — `TryGetValidationErrors`, `TryGetProblemDetails`, + `TryGetString`, `TryGetLong`, …; + - **status-specific `RawError` accessors** — e.g. `TryGetNotFound(out RawError)`, `TryGetNoContent(out RawError)`; + - the inherited **`TryGetRawError(out RawError)`**, which every `{Operation}Error` exposes. +2. **Write one `if` / `else if` branch per `TryGet*` method — cover them all, and put `TryGetRawError` + *last*.** Each public `TryGet*` corresponds to a status/body the operation can return; skip one and you + silently drop that response. `TryGetRawError` must be the final branch because it is **not** a catch-all + (see below) — it only fires for statuses that have no more-specific accessor. + +```csharp +using {RootNamespace}.Core.Exceptions; // SdkException +using {RootNamespace}.Core.ErrorResponse; // ApiError, RawError +using {RootNamespace}.Errors; // {Operation}Error types, e.g. CreateWidgetError + +try +{ + var response = await client.{ApiGroup}.{Operation}(/* ... */, ct); + // use response +} +catch (SdkException<{Operation}Error> ex) +{ + // ONE branch per public TryGet* declared on {Operation}Error — copy the exact names from the class + // under Errors/. The TryGet{...} names below are PLACEHOLDERS; yours are named after this operation's + // responses (a typed body may be a model OR a scalar such as TryGetString/TryGetLong). + if (ex.Error.TryGet{TypedBody1}(out var body1)) // e.g. TryGetValidationErrors — a typed body + { + // inspect body1 + } + else if (ex.Error.TryGet{TypedBody2}(out var body2)) // e.g. TryGetProblemDetails — another typed body + { + // inspect body2 + } + else if (ex.Error.TryGet{Status}(out RawError statusRaw)) // e.g. TryGetNoContent(out RawError) — status-specific + { + Console.Error.WriteLine($"HTTP {(int)statusRaw.StatusCode}"); + } + // ... KEEP GOING: one else-if for EVERY remaining TryGet* the class declares — do not stop early ... + else if (ex.Error.TryGetRawError(out RawError raw)) // ALWAYS LAST: fallback for untyped statuses only + { + Console.Error.WriteLine($"HTTP {(int)raw.StatusCode}: {raw.ReadAsString()}"); + } +} +``` + +**Why `TryGetRawError` goes last — it is not a universal fallback.** It returns a raw body **only** for +statuses that have no more-specific accessor on this `{Operation}Error`; a status that has a typed accessor +(e.g. a `422` validation payload) lands in that typed slot and leaves `TryGetRawError` **false**. The +status-specific `RawError` accessors (e.g. `TryGetNoContent(out RawError)`) are likewise **not** surfaced by +`TryGetRawError`. So if you check `TryGetRawError` first — or omit any of the more-specific accessors — those +typed and status-specific bodies are silently dropped. Enumerate the class and handle every accessor +explicitly. + +**Don't factor error-reading into a shared helper typed as `ApiError`.** The typed `TryGet*` accessors live +on the concrete `{Operation}Error`, *not* on the `ApiError` base — which exposes only `TryGetRawError`. A +helper like `string Describe(ApiError e)` can therefore reach **only** `TryGetRawError`, so for any status +that has a typed body it finds nothing and falls back to `e.ToString()` — a bare type name +(`{RootNamespace}.Errors.{Operation}Error`), not the actual message. Read the typed accessors **inside the +per-operation `catch` block**, where the concrete `{Operation}Error` type is known; reserve shared code for +the `RawError`/transport fallback only. + +### Case B — operation throws `SdkException` + +For operations with no `{Operation}Error` type (none under `Errors/`), `ex.Error` **is** a `RawError` — +there are no `TryGet*` accessors and no `TryGetRawError`; read the status and body straight off it: + +```csharp +using {RootNamespace}.Core.Exceptions; // SdkException +using {RootNamespace}.Core.ErrorResponse; // RawError + +try +{ + var response = await client.{ApiGroup}.{Operation}(/* ... */, ct); + // use response +} +catch (SdkException ex) +{ + RawError raw = ex.Error; // the error model IS RawError here + Console.Error.WriteLine($"HTTP {(int)raw.StatusCode}"); + Console.Error.WriteLine(raw.ReadAsString()); // or raw.ReadAsJson() +} +``` + +Case B needs no `.Errors` using — `RawError` lives under `{RootNamespace}.Core.ErrorResponse`. Its public +members (`StatusCode`, `ReadAsBytes`/`ReadAsString`/`ReadAsJson`) are visible in the SDK source; note +`ReadAsJson()` **throws `JsonException`** when the body isn't valid JSON — and a `RawError` body often +isn't (this is the no-typed-error-model case), so prefer `ReadAsString()` unless you know it's JSON. + +## Result-style alternative — `ApiResult` (no throwing) + +The generator can **optionally** emit a result-style variant of an operation — so it's not guaranteed to +exist. When enabled, it appears as a **sibling method** named `{Operation}Result` (next to the throwing +`{Operation}`), returning `Task>` and **does not throw** on a non-success +status — the error is carried in the returned value instead. (`{TError}` is the same two-case shape as +above: a typed `{Operation}Error`, or `RawError`.) `ApiResult` is a public +`readonly struct` under `{RootNamespace}.Core.Models`. If the controller has no `{Operation}Result` +overload, this variant wasn't generated — use the throwing method with `try/catch` instead. + +Unlike the throwing path, `ApiResult` exposes the HTTP **`StatusCode`** and **`Headers`** on *both* success +and failure — so this is the variant to use when you need the status code, rate-limit headers, or pagination +`Link` headers. + +```csharp +using {RootNamespace}.Core.Models; // ApiResult +using {RootNamespace}.Core.ErrorResponse; // RawError +using {RootNamespace}.Errors; // {Operation}Error (Case A only) + +// No try/catch — the *Result variant returns the outcome instead of throwing. +ApiResult<{ReturnType}, {Operation}Error> result = + await client.{ApiGroup}.{Operation}Result(/* ... */, ct); + +if (result.TryGetResponse(out var response)) // success +{ + Console.WriteLine($"OK {(int)result.StatusCode}"); // status + result.Headers available here + // use response +} +else if (result.TryGetError(out var error)) // failure +{ + // 'error' is the same TError as the throwing path: + // Case A → typed {Operation}Error (use its TryGet* accessors, then TryGetRawError) + // Case B → RawError (read error.StatusCode / error.ReadAsString()) + Console.Error.WriteLine($"HTTP {(int)result.StatusCode}"); +} +``` + +Other ways to consume it: + +```csharp +// Pattern-match to a value (Action overload also exists): +var summary = result.Match(onSuccess: r => "ok", onFailure: e => "failed"); + +// Tuple deconstruction: +var (isSuccess, response, error) = result; + +// Bridge back to the throwing behavior (returns the response or throws SdkException<{TError}>): +{ReturnType} value = result.GetResponseOrThrow(); +``` + +## Connection failures, and guarding every call + +The exception types above cover API errors (the server replied with a non-2xx status). They do +**not** cover connection failures — host unreachable, DNS failure, dropped connection, or timeout. +Those come through as `HttpRequestException` / `TaskCanceledException`, which a +`catch (SdkException<...>)` will not match. If that catch is your only guard, a connection failure +escapes and takes down whatever was running the call. + +**Convert connection failures to your own error type in one place.** If you wrap the SDK behind +your own abstraction (a client interface, a service, a repository), catch connection failures at +that boundary and rethrow the same error type you already use for API errors — so the rest of the +code has a single failure type to handle instead of two unrelated ones: + +```csharp +catch (SdkException ex) // API error (non-2xx) +{ + throw new {ProviderException}("...", ex); +} +catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) // connection failure +{ + throw new {ProviderException}("provider unreachable", ex); +} +``` + +**Guard every call site, not just the ones that change data.** It is easy to wrap the calls that +create or modify something and overlook the calls that only read — especially reads that run +automatically on a routine path (loading a screen, a scheduled job, a startup or health check). A +connection failure during a read fails just as hard as one during a write. Wherever the SDK (or +your wrapper) is called, the caller must catch the failure and degrade in a way that fits — a +fallback, a retry, a clear message — rather than letting it escape. A call left unguarded next to +one that is guarded is the one that breaks. + +## Notes + +- On an SDK with **multiple/composite auth schemes**, a call can also throw `AuthSchemeException` + (under `{RootNamespace}.Core.Exceptions`) — an auth *application* failure, not an API error — when the + configured schemes can't be satisfied; it carries `IReadOnlyList SchemeFailures` and is **not** + an `SdkException`, so a `catch (SdkException<...>)` won't match it — catch it separately. (A + single-scheme SDK like Maxio's Basic-only client won't hit this.) +- Retries for transient statuses happen automatically before an exception is thrown — but only for + idempotent methods (`GET/HEAD/PUT/OPTIONS`) by default, so `POST`/`PATCH`/`DELETE` errors surface without + retry. See **dotnet-configuration-resilience**. diff --git a/plugins/maxio-sdk-clone/skills/dotnet-models/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-models/SKILL.md new file mode 100644 index 0000000..32c3a8d --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-models/SKILL.md @@ -0,0 +1,113 @@ +--- +name: dotnet-models +description: Construct and read the non-obvious model shapes of an APIMatic-generated C#/.NET SDK — polymorphic OneOf/AnyOf unions (built with static factory methods, read via TryGet…, no object-initializer), string-/int-enums (StringEnum/IntEnum via static constants or FromValue, not C# enums), collections (IReadOnlyList/IReadOnlyDictionary), DateTimeOffset dates, and unknown-field behavior. Use when building a request body or reading a response field that is a union, enum, list/map, or date — anything that isn't a plain string/number — or when an unmodeled JSON field is dropped on deserialization. Load it even after reading the field's type in the source, since the type name alone won't tell you a union needs factory methods (not `new`) or that an enum isn't a C# enum. +--- + +# Working with models in an APIMatic .NET SDK + +Most request/response data are immutable `record`s built with object-initializers (covered in +`dotnet-calling-endpoints`). This skill covers the **non-obvious model shapes** that trip integrations up. +The patterns are generic across APIMatic .NET SDKs; take the real type names from your SDK source — read the +model and union `.cs` files, not a decompiled or reflected view of the installed package. + +> Throughout this skill, `{...}` is a placeholder for a name you take from your SDK (e.g. `{Union}`, +> `{Variant}`, `{EnumType}`, `{RequestType}`) — replace it with the concrete identifier from the source. + +## Polymorphic union types: `OneOf` and `AnyOf` + +When a field can be one of several types, APIMatic generates a union `record` (under +`{RootNamespace}.Models.OneOf` or `.Models.AnyOf`). Build these with the generated **static factory +methods** (one per variant) and read them back with **`TryGet…` methods** — a union has no +object-initializer. JSON (de)serialization is automatic. + +- `OneOf` — the value is exactly one variant (sometimes indicated by a discriminator field such as `type`). +- `AnyOf` — the value may match one of several primitive/shape variants. + +### Construct + +```csharp +// One static factory per variant: {Union}.{Variant}(value) +var u1 = {Union}.String("..."); +var u2 = {Union}.{Variant}(new {Variant} { /* ... */ }); + +// AnyOf unions over primitives also expose implicit conversions: +{Union} u3 = "..."; // same as {Union}.String("...") +{Union} u4 = 10.50m; // same as {Union}.Decimal(10.50m) +``` + +### Read / unwrap + +```csharp +// Each variant has a bool TryGet{Variant}(out var value): +if (u1.TryGetString(out var s)) { /* use s (string) */ } +else if (u1.TryGetDecimal(out var d)) { /* use d (decimal) */ } + +// OneOf: branch over the variants you expect +if (resp.{Field}.TryGet{Variant}(out var v)) { /* ... */ } +else if (resp.{Field}.TryGet{OtherVariant}(out var w)) { /* ... */ } +``` + +The factory and `TryGet` names are built mechanically from the **variant's CLR type name**: + +| Variant CLR type | Factory method | Reader | +| --- | --- | --- | +| `double` | `.Double(double)` | `TryGetDouble(out double)` | +| `decimal` | `.Decimal(decimal)` | `TryGetDecimal(out decimal)` | +| `string` | `.String(string)` | `TryGetString(out string)` | +| a model `{Variant}` | `.{Variant}({Variant})` | `TryGet{Variant}(out {Variant})` | +| a list of `{Variant}` | `.ListOf{Variant}(IReadOnlyList<{Variant}>)` | `TryGetListOf{Variant}(out IReadOnlyList<{Variant}>)` | + +The exact CLR type varies per union — a numeric variant may be `double`, `decimal`, or `long` — so open the +union file under `Models/AnyOf` or `Models/OneOf` and copy the real method name. (Unions use the per-variant +factories and `TryGet…` readers shown above; `FromValue` belongs to enums.) The `Optional` backing a +union is internal — interact only through the +factories and `TryGet…`. + +## Collections + +List/array properties are `IReadOnlyList?`; maps are `IReadOnlyDictionary?`. Assign a +`List<>`/array/`Dictionary<>` directly (each implements the read-only interface), or use collection +expressions: + +```csharp +var body = new {RequestType} +{ + {ListProp} = ["A", "B"], // IReadOnlyList + {MapProp} = new Dictionary { ["k"] = "v" } // IReadOnlyDictionary +}; +``` + +A null collection is omitted from the JSON; an **empty** collection is serialized. + +## Dates & numbers + +- Date/time fields are `DateTimeOffset?`, serialized as ISO-8601 / RFC-3339 — work with `DateTimeOffset` + and let the SDK handle the wire format. For manual formatting/parsing use the BCL (`DateTimeOffset.Parse`, + `.ToString("O")`); the SDK's date handling is internal. +- Money/quantities may be `string`, `decimal`, or a string-or-number `AnyOf` union; the model's property + type is the source of truth. Numeric types vary per SDK (`int`, `long`, `double`, …) — take the exact + type from the map signature / model page; don't assume `double`. + +## Enums + +Enums are type-safe string-enums (`StringEnum`) or int-enums (`IntEnum`): use the static constants, +or `FromValue(...)` for a value not known at compile time; they convert implicitly to their underlying +value. Reading back: `.Value` (equivalently `ToString()` or the implicit conversion) yields the raw wire +value, and the enum types are `record`s, so `==` compares by value — `{EnumType}.FromValue("x")` equals the +`x` constant. Guard unknown values with `TryGetKnownValue(...)` or `instance.IsKnownValue()`. + +```csharp +{request}.{EnumProp} = {EnumType}.SomeConstant; +{request}.{EnumProp} = {EnumType}.FromValue(serverProvidedValue); // tolerates unknown values +string wire = {response}.{EnumProp}.Value; // raw wire value back out +if ({EnumType}.TryGetKnownValue(value, out var known)) { /* known constant */ } +``` + +See [reference.md](reference.md) for full string- and int-enum declarations and union-member discovery. + +## Unknown / future fields + +Models declare their properties explicitly. Whether unknown JSON fields are kept depends on the SDK: +APIMatic can generate an additional-properties map that captures them, but where a model has none — the +common case — unknown fields are dropped on deserialization. Check the model; to read an unmodeled field, +regenerate the SDK or parse that response yourself. diff --git a/plugins/maxio-sdk-clone/skills/dotnet-models/reference.md b/plugins/maxio-sdk-clone/skills/dotnet-models/reference.md new file mode 100644 index 0000000..32456b1 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-models/reference.md @@ -0,0 +1,64 @@ +# Models reference (APIMatic .NET) + +## Date/time + +Date/time values are `DateTimeOffset?`, serialized as ISO-8601 / RFC-3339 (`"2024-06-17T15:30:45Z"`) — work +with `DateTimeOffset` directly and let the SDK handle the wire format. For manual formatting/parsing in your +own code, use the BCL (`DateTimeOffset.Parse`, `.ToString("O")`, …), not the SDK's internal helpers. + +## String-enums + +```csharp +[JsonConverter(typeof(StringEnumConverter<{EnumType}>))] +public sealed record {EnumType} : StringEnum<{EnumType}> +{ + public static readonly {EnumType} FirstValue = new("first_value"); + public static readonly {EnumType} SecondValue = new("second_value"); + public static {EnumType} FromValue(string value) => FromValueCore(value); +} +``` + +Usage: + +```csharp +var v = {EnumType}.FirstValue; // known constant +var u = {EnumType}.FromValue("new_value"); // unknown-tolerant +string raw = v; // implicit conversion to string +if ({EnumType}.TryGetKnownValue("first_value", out var known)) { /* known == FirstValue */ } +var all = {EnumType}.GetKnownValues(); +``` + +## Int-enums + +Same pattern over `int`: + +```csharp +[JsonConverter(typeof(IntEnumConverter<{EnumType}>))] +public sealed record {EnumType} : IntEnum<{EnumType}> +{ + public static readonly {EnumType} Off = new(0); + public static readonly {EnumType} On = new(1); + public static {EnumType} FromValue(int value) => FromValueCore(value); +} + +{request}.{EnumProp} = {EnumType}.On; +int n = {EnumType}.On; // implicit conversion to int +``` + +## Union types — finding the exact members + +For a `OneOf`/`AnyOf` type, open its file under `Models/OneOf/` or `Models/AnyOf/`. Each variant `{V}` +produces: + +- a factory `static {Union} {V}({V} value)` (the parameter type usually equals the variant type name), and +- a reader `bool TryGet{V}(out {V} value)`. + +`AnyOf` unions over primitives also commonly add `implicit operator {Union}({primitive})`. Unions are +immutable records — there are no object-initializers and no way to mutate one after construction. + +## Notes + +- Optional model properties use `[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`, so leaving + one unset omits it from the request JSON entirely (distinct from sending an explicit `null`). +- A model captures unknown response fields only when it has an additional-properties map; where it has + none, unknown fields are dropped. diff --git a/plugins/maxio-sdk-clone/skills/dotnet-testing/SKILL.md b/plugins/maxio-sdk-clone/skills/dotnet-testing/SKILL.md new file mode 100644 index 0000000..a9e49d6 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/dotnet-testing/SKILL.md @@ -0,0 +1,173 @@ +--- +name: dotnet-testing +description: Unit-test code that uses an APIMatic-generated C#/.NET SDK by injecting a fake HttpClient — the client's HttpClient constructor argument is the test seam (no SDK mocking helpers) — stub success and error responses with a custom HttpMessageHandler, assert the outgoing request, assert SdkException on error paths, and register a stub client in DI. Use when writing, mocking, or stubbing tests for calls made through an APIMatic .NET SDK client — load it even after reading the constructor in the source, since the seam alone won't tell you to match the project's test stack or assert the right exception per operation. +--- + +# Testing code that uses an APIMatic .NET SDK + +The client takes an `HttpClient` in its constructor, which is the seam for testing: pass an `HttpClient` +backed by a fake `HttpMessageHandler`, so no real network calls happen. The SDK ships no mocking helpers — +this is standard .NET. + +**Match the project's existing test stack — don't impose one.** Check the test project's package references +and existing tests, then mirror both its **test framework** (xUnit / NUnit / MSTest) and its **assertion +style**: if it uses an assertion library such as FluentAssertions or Shouldly, write assertions that way +(e.g. `result.StatusCode.Should().Be(HttpStatusCode.OK)`) rather than the framework's built-in asserts. The +code samples below use xUnit `[Fact]` + the built-in `Assert` **purely for reference** — they show the SDK +testing seam and *what* to assert, not a mandated framework or assertion library. Substitute your +`{Api}Client`/`{Api}ClientOptions` as well. + +> Throughout this skill, `{...}` is a placeholder for a name you take from your SDK (e.g. `{Api}Client`, +> `{ApiGroup}`, `{Operation}`) — replace it with the concrete identifier from the source. + +## A reusable stub handler + +```csharp +using System.Net; + +public sealed class StubHandler : HttpMessageHandler +{ + private readonly Func _responder; + public HttpRequestMessage? LastRequest { get; private set; } + + public StubHandler(Func responder) => _responder = responder; + + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken ct) + { + LastRequest = request; + return Task.FromResult(_responder(request)); + } +} + +static {Api}Client ClientReturning(HttpStatusCode status, string json) +{ + var handler = new StubHandler(_ => new HttpResponseMessage(status) + { + Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json") + }); + return new {Api}Client(new HttpClient(handler), new {Api}ClientOptions { /* auth not needed for stubs */ }); +} +``` + +## Test a success path + +```csharp +[Fact] +public async Task ReturnsDeserializedBody() +{ + var client = ClientReturning(HttpStatusCode.OK, """{ "{resource}": { "id": 123 } }"""); + + var response = await client.{ApiGroup}.{Operation}(/* args */, ct: default); + + Assert.Equal(123, response.{Resource}?.Id); +} +``` + +## Test an error path + +Endpoint methods throw `SdkException` on non-2xx (see `dotnet-error-handling`). `TError` is the +operation's `{Operation}Error` model (**Case A**) for operations that have a generated `{Operation}Error` +type, or `RawError` **directly** (**Case B**) otherwise — so assert the type that matches your operation. + +**Case A — typed `{Operation}Error`:** + +```csharp +using {RootNamespace}.Core.Exceptions; // SdkException +using {RootNamespace}.Errors; // {Operation}Error types + +[Fact] +public async Task ThrowsOnApiError() +{ + var client = ClientReturning(HttpStatusCode.UnprocessableEntity, """{ "errors": ["bad input"] }"""); + + var ex = await Assert.ThrowsAsync>( + () => client.{ApiGroup}.{Operation}(/* args */, ct: default)); + + // ex.Error is the typed ApiError. For a status the operation maps to a typed body (e.g. 422), assert the + // typed accessor — its name embeds the body type, so open the {Operation}Error under Errors/ for the + // exact name. TryGetRawError is FALSE for those statuses, so don't assert through it here: + Assert.True(ex.Error.TryGetSomeTypedBody(out var typed)); + // ...assert on 'typed'. (Only statuses the operation maps to RawError populate TryGetRawError.) +} +``` + +**Case B — `SdkException`** (e.g. read/list/find/archive/delete operations). Here `ex.Error` *is* +the `RawError` — there is no `TryGet*` / `TryGetRawError`; read it directly: + +```csharp +using {RootNamespace}.Core.Exceptions; +using {RootNamespace}.Core.ErrorResponse; + +var ex = await Assert.ThrowsAsync>( + () => client.{ApiGroup}.{Operation}(/* args */, ct: default)); + +Assert.Equal(HttpStatusCode.UnprocessableEntity, ex.Error.StatusCode); +// You can also assert the deserialized error body: ex.Error.ReadAsString() / ex.Error.ReadAsJson(). +``` + +## Test the result-style (`ApiResult`) variant + +If the operation exposes the optional non-throwing `{Operation}Result` sibling (see `dotnet-error-handling`), +there is nothing to catch — stub the response and assert on the returned `ApiResult` +directly. The status code and headers are available on both the success and failure outcomes. + +```csharp +using {RootNamespace}.Core.Models; // ApiResult +using {RootNamespace}.Core.ErrorResponse; // RawError (Case B) +using {RootNamespace}.Errors; // {Operation}Error (Case A only) + +[Fact] +public async Task ResultVariantReportsFailureWithoutThrowing() +{ + var client = ClientReturning(HttpStatusCode.UnprocessableEntity, """{ "errors": ["bad input"] }"""); + + var result = await client.{ApiGroup}.{Operation}Result(/* args */, ct: default); + + Assert.False(result.TryGetResponse(out _)); + Assert.True(result.TryGetError(out var error)); // 'error' is the same TError as the throwing path + Assert.Equal(HttpStatusCode.UnprocessableEntity, result.StatusCode); + // 'error' is a typed {Operation}Error (Case A) or a RawError (Case B) — assert accordingly. +} +``` + +## Assert the outgoing request + +Because the stub captures `LastRequest`, you can assert method, path, query, headers, and body: + +```csharp +var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK) + { Content = new StringContent("{}") }); +var client = new {Api}Client(new HttpClient(handler), new {Api}ClientOptions()); + +await client.{ApiGroup}.{Operation}(/* args */, ct: default); + +Assert.Equal(HttpMethod.Post, handler.LastRequest!.Method); +Assert.Contains("/expected/path", handler.LastRequest!.RequestUri!.AbsolutePath); +Assert.Contains("per_page=20", handler.LastRequest!.RequestUri!.Query); // query params are snake_case on the wire + +// Assert the serialized request body of a POST/PUT/PATCH: +var sentJson = await handler.LastRequest!.Content!.ReadAsStringAsync(); +Assert.Contains("\"expected_field\"", sentJson); +``` + +## Notes + +- Mocking libraries (Moq, NSubstitute) work too — mock `HttpMessageHandler.SendAsync` (it's `protected`, + so use `Protected()` with Moq). The hand-written stub above avoids that friction. +- A stubbed retryable response (`408/429/5xx`) on a retryable method will be retried by the SDK before the + call returns — retries apply to `GET/HEAD/PUT/OPTIONS` only by default, so a `POST` won't retry unless you + add its method to `HttpMethodsToRetry` (see `dotnet-configuration-resilience`). To observe retries firing, + have the stub return `503` then `200` and count invocations. +- For DI-based code, the SDK's `Add{Api}Client` resolves the **default (unnamed)** `IHttpClientFactory` + client, so register your stub on that one, then resolve `{Api}Client` from the provider: + ```csharp + services.Add{Api}Client(o => { /* ... */ }); + services.AddHttpClient(Options.DefaultName).ConfigurePrimaryHttpMessageHandler(() => stubHandler); + var client = services.BuildServiceProvider().GetRequiredService<{Api}Client>(); + ``` +- To look up an operation's signature, its request type, or a `{Operation}Error`'s accessor names, read the + SDK source `.cs` files — don't decompile or reflect over the installed package, which drops the XML-doc + comments and the request-builder details. +- Prefer this `HttpClient`-seam approach over wrapping the SDK in your own interface unless you need to + abstract the SDK for other reasons. diff --git a/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md b/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md new file mode 100644 index 0000000..0e3ae23 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md @@ -0,0 +1,145 @@ +--- +name: integrate-maxio +description: 'Entry point and router for all Maxio Advanced Billing .NET SDK work — routes planning to the maxio-plan subagent (which returns a map-grounded contract sheet) and SDK errors to the maxio-debug subagent. Use when the user asks to integrate Maxio (formerly Chargify) billing, implement subscriptions/customers/usage/invoices with the Maxio .NET SDK, or reports a Maxio error or unexpected SDK behaviour in a C#/.NET project. Main agent: you never read, clone, grep, or decompile SDK source or the compiled/NuGet package, and you never handle the SDK clone or its path — that is the job of the plan and debug agents; never create or edit a project file while plan or debug is running, because it edits files in place and its edits collide with yours.' +--- + +# Maxio Advanced Billing .NET SDK — Router (map + subagents) + +You (the main agent) orchestrate; the subagents carry the SDK knowledge. The division of +labour keeps YOUR context small and YOUR code grounded — you never touch the SDK yourself. + +## The subagents + +- **`maxio-plan`** does all SDK contract discovery: it clones the SDK (the map ships inside + the SDK source) and grounds against that map, returning a **contract sheet with no open + lookups** — exact signatures, wire names, envelope shapes, error accessors, and enum values + for the operations in scope. (A fact even the source cannot settle becomes a defensive + directive, never left open.) You implement from that sheet; route every SDK contract need to it. +- **`maxio-debug`** owns SDK-related build/runtime failures — any error naming an SDK + symbol, or any unexpected Maxio behaviour — grounding in the same map and source from the + session clone (map row first; then the one file the map names). It fixes the code in place + and reports what changed. + +**Scope guard:** Maxio Advanced Billing .NET SDK (`AsadAli.AdvancedBilling.Sdk`, +namespace `MaxioAdvancedBilling`) in C#/.NET projects only. Unrelated API or language — +do nothing. + +## Workflow + +**If the user opens with a reported SDK error or unexpected Maxio behaviour** (not new feature work), +spawn **`maxio-debug`** directly with the error output and the files involved, and wait — do not run the +plan-first flow for a bug report. Otherwise, for implementation work: + +### Step 1 — Plan first (always, for any implementation work) + +Your FIRST action is to spawn **`maxio-plan`** once, with the user's full request (all +features in scope — one spawn covers the whole implementation). Dictate the output path +in the brief: the absolute path where it writes the plan (`/maxio-plan.md`) — do not let the helper pick its own location. + +It writes `maxio-plan.md` (plan + contract sheet) and returns its path. + +**Parallelize the wait — prerequisites only.** Repo reconnaissance (where the +integration lands in this codebase) is not `maxio-plan`'s job: kick it off in the +SAME message as the `maxio-plan` spawn (parallel tool calls; background spawns if +your harness has them). While `maxio-plan` works, do ONLY work that needs no SDK +knowledge and touches no project file: + +- the repo survey (read-only exploration of conventions and layering) — brief it to + return each convention as *pattern + the ONE exemplar file path to imitate*, NOT + inline code snippets: you will Read the exemplar at edit time anyway (edits need + the file's exact current text), so a snippet dump gets paid for twice; +- `dotnet restore` and a baseline `dotnet build` / `dotnet test` of the UNTOUCHED + solution, so later failures are attributable to your changes; +- locating the SDK package and its version in the project; +- credentials/environment verification (per the task's secret-handling rules); +- setting up your task tracking. + +Never sit idle while one of these read-only prerequisites is still undone. Equally, +never use the wait to get a "head start" on implementation: **creating or editing ANY +project file before the gate below is a defect**, no matter how obvious the code +seems — the plan agent (and later `maxio-debug`) edits files in place, so your writes +race its writes. This applies while a helper is running whether you spawned it OR +resumed it via a follow-up message — a resumed or backgrounded helper is still running. + +**HARD GATE — no project-file creation or edits until:** the plan agent has +RETURNED, the file EXISTS at the path you dictated (check it — helpers have +misreported save locations), and you have read it. The gate bars coding +"meanwhile"; it does not bar the read-only prerequisite work above. Starting to +code before the sheet exists defeats the entire plan-first design. + +Before implementing, check the plan's **Assumptions & Blockers** section: +- Blocker or major assumption → surface it to the user in plain language, get their + answer, send the clarification to the EXISTING `maxio-plan` agent (re-spawn only if it + is gone). It revises the file in place and replies with the changed rows. +- Minor assumptions only → proceed. + +Full re-planning only on genuine scope change; for a single missing fact mid-implementation +see *Anti-patterns* — ask the warm plan agent, never guess. + +### Step 2 — Implement from the contract sheet + +1. Read `maxio-plan.md` once. Treat its contracts as authoritative — do not re-derive or + "double-check" them from memory. When a helper later revises the sheet, it replies with + the changed rows verbatim: work from that reply, not a re-read of the file. +2. Implement sequentially, following the repo's own conventions and layering. +3. After every change: `dotnet build`; fix non-SDK errors yourself. +4. **Any compile or runtime error involving an SDK type or member** (`CS1061`, `CS0117`, + `CS0234`, `CS0104`, `CS1503`, `CS7036`, … on `MaxioAdvancedBilling.*`, or a provider + error at runtime) → spawn **`maxio-debug`** with the exact error output and the files + involved, and wait for it. Do not attempt more than one self-fix of an SDK-name error + before escalating — rewriting from the same knowledge that produced the error is + guessing. +5. Run the project's tests (`dotnet test`); verify the integration end to end the way the + task demands. + +**Reuse, don't re-spawn.** A fresh spawn rebuilds a prompt cache from scratch (the dominant +helper cost). If your harness can send a follow-up message to an already-spawned agent, +direct follow-up questions and plan revisions to the EXISTING `maxio-plan` agent — its map +context is already warm. Spawn fresh only when that isn't available or the agent is gone. +Require tight returns; if one arrives bloated, use what you need and drop the rest — never +paste a helper's reference dump into files or carry it forward. + +### Step 3 — Answering pure questions + +A standalone Maxio question with no code change: spawn `maxio-plan` in narrow-question mode +and relay its grounded answer. Never answer from memory, even for "easy" questions. + +## Wait for your agents + +The race rule is precise: **never create or edit a project file while a helper is +running** — `maxio-plan`, and especially `maxio-debug`, edit files in place, and their +edits collide with yours. This holds for a helper you spawned AND one you resumed via a +follow-up message (a resumed or backgrounded helper is still running). The one thing you +may do during a wait is the **read-only** Step-1 prerequisite work (repo survey, restore, +baseline build, env checks) — it touches no project file. When those are done and a helper +is still running, wait. + +## Anti-patterns — never do these + +Everything the main agent must NOT do is collected here, in one place: + +- **Never touch the SDK source yourself.** Don't read, clone, grep, `find`, or otherwise + ingest the SDK source to find an implementation detail — that is `maxio-plan`'s job (for + planning) and `maxio-debug`'s (for failures). +- **Don't use the compiled SDK either.** No decompiling or reflecting over the + installed/NuGet package to find implementation. +- **Don't handle the clone.** The SDK clone and its filesystem path belong to the plan and + debug agents; you never read the clone and never need or receive its path. +- **Never open the SDK map or the clone yourself.** In this variant the map lives in the + helpers' SDK clone, not in the plugin — you have no map to read and must not touch the + clone; you work from the contract sheet. +- **Don't bulk-load reference material.** Don't load `maxio-getting-started` or the `dotnet-*` + skills, and don't carry reference dumps — the contract sheet is your working reference. When + a fact is missing, ask the warm `maxio-plan` agent (there is no map in the plugin for you to + read). +- **Don't open the SDK's `api-reference.md`, and don't web-search Maxio topics.** +- **Never write a Maxio/SDK fact from memory** — every signature, field name, enum value, + and error type in your code must come from the contract sheet or a lookup. And **never + write a call from memory "to fix later".** +- **Don't re-derive or double-check a sheet row from memory.** If you are unsure what a row + said, batch your questions into ONE message to the warm `maxio-plan` agent (it holds the map + warm and answers in seconds) — never re-derive from memory, and never read the map or the + clone yourself. +- **Don't create or edit project files while a helper runs** — see *Wait for your agents* + (read-only Step-1 prerequisites are the one exception). diff --git a/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md b/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md new file mode 100644 index 0000000..90b7042 --- /dev/null +++ b/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md @@ -0,0 +1,237 @@ +--- +name: maxio-getting-started +description: Identify and orient in the Maxio Advanced Billing (formerly Chargify) .NET SDK — its NuGet package id and root namespace, the namespace layout, US/EU environments and the Basic-auth pattern, and how the SDK map points you to the exact source file when one is needed. In this variant the map is NOT bundled in the plugin — it ships inside the SDK's own source, at the root of the clone. You clone the SDK first and read the map from the clone (`/sdk-map.md` + `/map/`) — a table of contents over every operation signature, error type, model, enum, and union — so you look facts up in the map and open exactly the source file the map names, in the same clone, instead of grepping. This is the helper-facing reference layer, preloaded for the maxio-plan and maxio-debug subagents; the main agent routes Maxio work through integrate-maxio and works from its contract sheet instead of loading this skill or touching the clone. It also routes you to the companion dotnet-* skills (client-setup, auth, calling endpoints, models, error handling, configuration/resilience, testing) and gates loading each at its step — load them even after you've read the SDK source, since the source shows signatures but not the usage gotchas these skills carry. +--- + +# Getting started with the Maxio Advanced Billing .NET SDK + +> **Who this skill is for.** This is the helper-facing reference layer, preloaded for the +> `maxio-plan` and `maxio-debug` subagents — if you are one of them, it is yours to follow +> directly and fully. If you are the **main agent**, do not load this skill or the companion +> `dotnet-*` skills wholesale, and never touch the SDK clone or its map: route the task +> through `integrate-maxio`, implement from the contract sheet it produces, and when a fact +> is missing mid-implementation **ask the warm `maxio-plan` agent** — never read the map or +> the clone yourself. The "load the companion skill" steps below address whoever is doing the +> grounding — in this plugin's flow, the helpers; the main agent receives those traps as +> one-line notes on the contract sheet. This skill never calls back into the router, so there +> is no loop. + +This is the **SDK-specific** entry point. For general patterns that apply to any APIMatic-generated +.NET SDK (auth, calling endpoints, models, error handling, retries, testing), see the companion +API-agnostic skills: `dotnet-client-initialization`, `dotnet-authentication`, `dotnet-calling-endpoints`, +`dotnet-models`, `dotnet-error-handling`, `dotnet-configuration-resilience`, `dotnet-testing`. + +**The SDK map and these companion skills are complementary — load both.** The map (generated from the SDK +source, which remains the ground truth, and shipped alongside it) is authoritative for the SDK's *surface* +(signatures, model shapes, enums, which error type an operation throws); the companion skills are the *usage +layer* on top — the best-practice way to call each piece and the gotchas a signature can't show. Reading the +map or source doesn't remove the need to load the skill for that step, so at each step below, load the +companion *and* confirm names against the map. + +> **Ground every signature, model, enum, and error type in the SDK map that ships inside the SDK's own +> source.** In this variant the map is **not** bundled in the plugin — it lives at the root of the SDK source +> clone (`/sdk-map.md` + `/map/`). Clone the SDK first (see the *SDK source & map* section +> below), then read the map: it carries every operation signature, error type, enum value list, field list +> with JSON wire names, and union accessor by lookup, so most questions are answered by the map without +> reading source. When the map can't answer something — a full method/model body, a fact that is absent or +> ambiguous in the map, or a map-sourced name that fails to compile — open the one source file the map names, +> **in the same clone**; **never fill the gap from memory.** Do **not** decompile or reflect over the +> installed package, do **not** fetch GitHub files ad hoc, and do **not** grep, glob, `find`, or run other +> scans over the clone to locate things — the map is the locator. + +## SDK identity + +| | | +| --- | --- | +| API | Maxio Advanced Billing (formerly Chargify) | +| NuGet package | `AsadAli.AdvancedBilling.Sdk` | +| Root namespace | `MaxioAdvancedBilling` (the `using` namespace — note it differs from the package id) | +| Client class | `MaxioAdvancedBillingClient` | +| Options class | `MaxioAdvancedBillingClientOptions` | +| Auth | HTTP **Basic** — username = API key, password = literal `"x"` | +| Environments | `ServerEnvironment.Us` (default) → `https://{site}.chargify.com`; `ServerEnvironment.Eu` → `https://{site}.ebilling.maxio.com` | +| Target framework | `netstandard2.0` (works on .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5–10+) | + +The table above is **orientation, not a copy-paste recipe** — it gives you the names and facts (package id, +namespaces, the auth *pattern*, the environments), while the actual integration code comes from the companion +skills. Load each one as you reach its step (see **Integration workflow** below) and confirm its types +against the SDK map: the client construction and DI from `dotnet-client-initialization`, the exact +auth-credentials property name from `dotnet-authentication`, each call from `dotnet-calling-endpoints`, and so +on. + +## Namespaces (using-directives) + +The SDK splits its public types across **separate child namespaces**. C# does **not** import child +namespaces transitively, so `using MaxioAdvancedBilling.Models;` alone does **not** make enums, union +types, or error types visible — you get `CS0103`/`CS0246` ("name/type does not exist") on build. Add a +separate `using` for each kind of type you reference — the map lists each type's namespace, so take it from +the map row; only if the map is silent do you open the file the map names in the clone and copy the +`namespace` declaration at its top. + +## Install — always via NuGet + +Add the published NuGet package to your project. **Do not** add a project reference to the SDK's `.csproj` +or copy/clone its source into your solution — depend on the package only. (For *reading* the SDK source and +the map while you code, clone it separately as a read-only reference — see the **SDK source & map** section +below.) + +```bash +dotnet add package AsadAli.AdvancedBilling.Sdk +``` + +> The NuGet **package id** (`AsadAli.AdvancedBilling.Sdk`) differs from the **root namespace** you import +> (`MaxioAdvancedBilling`): install by the package id, but write `using MaxioAdvancedBilling;` in code. +> Package: . Runtime dependencies are pulled +> in transitively: `Polly`, `Microsoft.Extensions.Http`, `System.Net.Http.Json`, +> `System.Net.ServerSentEvents`. + +## SDK map — look up first, open second, never scan + +The SDK ships a generated table-of-contents **at the root of its own source**, so you read it from your +clone (see the next section for cloning): + +- **`/sdk-map.md`** — the index: SDK identity (package id, namespace, source ref), the + client-construction and error-handling models, the servers/auth wiring, and link tables into `map/`. +- **`/map/operations/{Controller}.md`** — one page per controller (33 pages, 247 operations). Each + operation row carries the HTTP verb/path, the exact C# signature with must-pass-explicitly params, the + return type, the error case (typed `{Operation}Error` vs `RawError`) with its `TryGet…` accessors, and + pagination. Each page's header names the source file it came from (e.g. `Source: Api/Customers.cs`) — and + because the map travels in the same clone as that source, the path resolves right there. +- **`/map/models/`** — record models (four alphabetical pages), `unions.md` (variant factories + + `TryGet…`), and `enums.md` (full value lists). + +**This map is how you traverse the SDK.** Do **not** grep, Glob, `find`, or otherwise scan the clone to +locate an operation, model, enum, union, or error type — that burns time and context on a 600+-file tree +whose entire surface is already indexed here. Instead: + +1. Open `/sdk-map.md` and follow the link table to the branch page you need (controller or model + group). +2. Read the fact by lookup — most questions (signature, error accessors, enum values, pagination) end here + without reading any source file at all. +3. Only when you need a **full method or model body** the map doesn't carry, take the file path the map + names (e.g. `Api/Customers.cs`, `Errors/CreateCustomerError.cs`, `Models/CreateCustomerRequest.cs`) and + **open that one file directly in the same clone**. + +**Source is a fallback, not a browse — the guard (non-negotiable, for `maxio-plan` and `maxio-debug` alike):** + +- **Map first, always.** Open a source file only when the map genuinely can't settle the fact: it is absent + or ambiguous in the map, or a map-sourced name fails to compile. +- **Open the exact file the map's row names** — the map gives the path — and read it **scoped**: the Read + tool with an offset/limit, or the Grep tool on the one symbol. Never dump a whole file with `cat`/`sed`. +- **Grepping, globbing, or `find`-ing over the clone tree to *locate* something is a defect.** The map is the + locator; the tree is never scanned. You go map row → the named file — never search → file. + +Keep lookups cheap — the rules that keep a session's context small: + +- Collect the contracts for **every** in-scope operation in **one** map pass — signature, required fields + with wire names, error accessors, enum values — into a short **contract sheet** in your plan or working + notes, then implement from the sheet. Don't re-open the map per field, and never re-look-up a fact the + sheet already carries. +- Don't load the SDK's `api-reference.md` — it is a large endpoint-reference doc; the map is the contract + index you ground in, and pulling `api-reference.md` into context is exactly the bulk read to avoid. + +Staleness check: the map ships with the source at the ref you cloned, so it matches that source **by +construction** — there is no separate map version to drift. If a name from the map fails to compile, trust +the compiler and re-read the source file the map's row names, in the same clone. + +## SDK source & map — clone first, then map-first + +Unlike the bundled-map plugin, this variant keeps the map **with the SDK source**: the map is generated +alongside the SDK and shipped at the root of its repo, so cloning the SDK gets you both the map and the +source it describes, always in lockstep. Cloning is therefore **step 0**, not a last resort — you cannot +read the map without it. Both `maxio-plan` and `maxio-debug` use the clone. + +**Clone once per session; reuse it.** `maxio-plan` runs first and normally clones; `maxio-debug` reuses that +clone. Before cloning, check the shared session file `/maxio-sdk-src/.maxio-session.md` for a clone +path recorded this session and reuse it; only if none exists, clone once — shallow, into a fresh timestamped +folder under `/maxio-sdk-src/` — then record the path under a `## Session artifacts` section of that +session file (create the file if absent) so every later spawn (plan or debug) finds and reuses it: + +```bash +# Linux/macOS: +dir="${TMPDIR:-/tmp}/maxio-sdk-src/$(date +%Y%m%d-%H%M%S)" +git clone --depth 1 --branch docs/sdk-map https://github.com/mohammadali2549/advanced-billing-sample-sdk "$dir" +# Record "$dir" under a `## Session artifacts` section in /maxio-sdk-src/.maxio-session.md — it is your clone path for the session. +``` + +```powershell +# Windows (PowerShell): +$dir = "$env:TEMP\maxio-sdk-src\$(Get-Date -Format yyyyMMdd-HHmmss)" +git clone --depth 1 --branch docs/sdk-map https://github.com/mohammadali2549/advanced-billing-sample-sdk $dir +# Record $dir under a `## Session artifacts` section in $env:TEMP\maxio-sdk-src\.maxio-session.md — it is your clone path for the session. +``` + +> **Experimental clone source.** This variant clones the SDK **fork/branch that carries the map** +> (`mohammadali2549/advanced-billing-sample-sdk` @ `docs/sdk-map`) while the map is in review upstream. Once +> the map is merged into the canonical SDK, this becomes `asadali214/advanced-billing-sample-sdk` at the +> released tag. Either way the map and the source in that clone are the same version by construction. + +The session file lives in `/maxio-sdk-src/`, **never** in the project repo, and the clone path never +goes into `maxio-plan.md` — the main agent must never see the clone or its path. + +Read the map, and (only when the guard above permits) the source, **from that local clone** — not by either +of these: + +- **Don't decompile or run reflection over the installed package.** A compiled assembly drops what the source + carries and the other skills rely on: the XML ``/parameter doc-comments, the exact parameter + names and order, and each method's internal `new Param("snake_case", …)` request-builder list. +- **Don't fetch GitHub files one at a time** as your way in — `…/blob/…` pages return HTML (not source) and + guessed paths fail, which is exactly how ad-hoc fetching breaks. Clone once and read locally instead. Only + if you truly cannot clone (`git` is unavailable) fetch a **raw** URL of the form + `https://raw.githubusercontent.com/mohammadali2549/advanced-billing-sample-sdk/docs/sdk-map/…` (never a + `…/blob/…` page) — e.g. `…/docs/sdk-map/sdk-map.md`, `…/docs/sdk-map/map/operations/Customers.md`, or + `…/docs/sdk-map/Api/Customers.cs`. + +Layout — where the map and its file references resolve in the clone (open these directly; don't scan for them): + +- `sdk-map.md` + `map/` — **the SDK map** (index + operations/models pages), at the repo root. +- `Api/` — one file per controller/group; **this is where the operation methods and their signatures live** + (each carries XML-doc comments for the params, the endpoint path, and the thrown error type). The map's + per-controller pages name the exact file (e.g. `Api/Customers.cs`). +- `Models/` (+ `Models/Enums/`, `Models/AnyOf/`, `Models/OneOf/`) — request/response records, enums, unions. +- `Errors/` — per-operation `{Operation}Error` types (only Case-A operations have one; the map's rows say + which case each operation is). +- `Core/` — HTTP infrastructure (`SdkException`, `RawError`, `ApiResult`, auth, retries). +- `Servers/`, `MaxioAdvancedBillingClient.cs`, `ServiceCollectionExtensions.cs` — environments, the client, DI. + +The map in your clone matches the source in the same clone by construction — there is no separate version to +reconcile. If you ever need a different SDK version, check out its ref (`git -C "$dir" checkout `) and +the map at that ref moves with it. + +**Leave the clone in place — don't delete it.** It's a read-only reference with nothing of yours in it, and +keeping it is what lets every later step in this session reuse it instead of cloning again. The OS reaps the +temp directory on its own; a future session simply makes its own timestamped clone. + +## Integration workflow — load the companion skill at each step + +Before you write the code for each step, load the named companion skill — even if you've already read the +relevant source. Each step calls out the trap the signature hides (in *parens*). A typical integration reaches +them in this order: + +1. **Client & DI setup** — load **dotnet-client-initialization** before you write + `new MaxioAdvancedBillingClient(...)`, build its options, or DI-register via + `AddMaxioAdvancedBillingClient`. (*The signature won't tell you:* the `HttpClient`/handler + pipeline must be long-lived and reused via `IHttpClientFactory`, not rebuilt per request; the SDK client + wrapper over it may be transient.) +2. **Authentication** — load **dotnet-authentication** before you set credentials. Maxio is HTTP Basic + (username = API key, password = `"x"`). (*The signature won't tell you:* set credentials before + constructing the client or in the DI callback, and load the key from configuration rather than hardcoding.) +3. **Calling an endpoint / building a request body** — load **dotnet-calling-endpoints** before the first + `client.{ApiGroup}.{Operation}(...)` call. (*The signature won't tell you:* call list/search ops with + named arguments — many optional params have no C# default and mis-bind in a positional call.) +4. **Models** — load **dotnet-models** the moment a request/response field isn't a plain string or number. + (*The signature won't tell you:* unions are built with factory methods and read via `TryGet…` (no `new`), + enums are `StringEnum` not C# enums, and unmodeled JSON fields are dropped on deserialize.) +5. **Error handling** — load **dotnet-error-handling** before you write any `try/catch`. (*The signature won't + tell you:* **many — not all —** read/list/find/delete ops are Case B (`SdkException`, no typed + accessors) while others are Case A typed `{Operation}Error`s — confirm each operation's case in its map + row; and `TryGetRawError` is not a catch-all on the typed errors. This SDK generates **no** + `{Operation}Result`/`ApiResult` no-throw variants — every operation is throw-only, so ignore the + Result-style sections of the companion skills and always wrap the throwing call.) +6. **Configuration & resilience** — load **dotnet-configuration-resilience** when you tune retries, timeouts, + the base URL, pagination, or logging. (*The signature won't tell you:* retries cover idempotent verbs only — + anything not in `GET/HEAD/PUT/OPTIONS` (`POST`, `PATCH`, `DELETE`) is not retried — `Timeout` is + per-attempt not total, and there's no built-in logging hook.) +7. **Testing** — load **dotnet-testing** before you stub the SDK. (*The signature won't tell you:* the + `HttpClient` constructor argument is the test seam; match the project's existing framework and assertion + style.) From ba4cdcec10e47d772509e5dc2d1294587db13df2 Mon Sep 17 00:00:00 2001 From: mohammadali2549 Date: Fri, 17 Jul 2026 08:03:20 +0500 Subject: [PATCH 2/2] docs(maxio-sdk-clone): describe the plugin on its own terms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove prior-version comparative framing ("not bundled", "unlike the bundled-map plugin", "in this variant", "variant of maxio-sdk") from the consumer-facing skills, agents, README, and manifests — from a fresh consumer's perspective it is bloat. Behavior is now stated positively: the SDK map ships with the SDK source; the agents clone and read it from there. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 2 +- .../.claude-plugin/plugin.json | 2 +- plugins/maxio-sdk-clone/README.md | 22 +++++++++---------- plugins/maxio-sdk-clone/agents/maxio-plan.md | 2 +- .../skills/integrate-maxio/SKILL.md | 9 ++++---- .../skills/maxio-getting-started/SKILL.md | 20 ++++++++--------- 6 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index da59c33..05f86d0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -24,7 +24,7 @@ { "name": "maxio-sdk-clone", "source": "./plugins/maxio-sdk-clone", - "description": "EXPERIMENTAL variant of maxio-sdk. The SDK map is not bundled in the plugin — it ships inside the SDK source, so the maxio-plan and maxio-debug agents clone the SDK first, look facts up in the map that travels with it, and open the exact source file the map names only when the map cannot settle a fact. Same integrate-maxio router + maxio-plan + maxio-debug orchestration. No MCP server. (Do not install alongside maxio-sdk — they share skill/agent names.)" + "description": "Install and consume the Maxio Advanced Billing .NET SDK, grounded in an SDK map that ships inside the SDK's own source. The maxio-plan and maxio-debug agents clone the SDK, look facts up in the map that travels with it, and open the exact source file the map names only when the map cannot settle a fact. Includes the integrate-maxio router + maxio-plan + maxio-debug orchestration. No MCP server. (Do not install alongside maxio-sdk — they share skill/agent names.)" } ] } diff --git a/plugins/maxio-sdk-clone/.claude-plugin/plugin.json b/plugins/maxio-sdk-clone/.claude-plugin/plugin.json index 4e89003..2250349 100644 --- a/plugins/maxio-sdk-clone/.claude-plugin/plugin.json +++ b/plugins/maxio-sdk-clone/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "maxio-sdk-clone", "displayName": "Maxio SDK Assistant (map-in-clone)", "version": "0.1.0", - "description": "EXPERIMENTAL variant of maxio-sdk (descends from maxio-sdk v0.2.0). The generated SDK map is NOT bundled in the plugin — it ships inside the SDK source, so the maxio-plan and maxio-debug agents clone the SDK first, look facts up in the map that travels with it, and open the exact source file the map names only when the map cannot settle a fact. Includes the integrate-maxio router, a map-grounded maxio-plan contract-sheet agent, and a map-first maxio-debug agent. No MCP server.", + "description": "Install and consume the Maxio Advanced Billing .NET SDK, grounded in an SDK map that ships inside the SDK's own source. The maxio-plan and maxio-debug agents clone the SDK, look facts up in the map that travels with it, and open the exact source file the map names only when the map cannot settle a fact. Includes the integrate-maxio router, a map-grounded maxio-plan contract-sheet agent, and a map-first maxio-debug agent. No MCP server.", "author": { "name": "APIMatic", "email": "developer@apimatic.io" diff --git a/plugins/maxio-sdk-clone/README.md b/plugins/maxio-sdk-clone/README.md index 9205a69..f8f1b6b 100644 --- a/plugins/maxio-sdk-clone/README.md +++ b/plugins/maxio-sdk-clone/README.md @@ -1,19 +1,17 @@ -# Maxio SDK Assistant — map-in-clone variant (Claude Code plugin) +# Maxio SDK Assistant (Claude Code plugin) -**Experimental variant of `maxio-sdk`** (descends from `maxio-sdk` v0.2.0). It helps developers -**install and consume the Maxio Advanced Billing .NET SDK**, plus reusable guidance for working with -**any APIMatic-generated .NET SDK**. +A Claude Code plugin that helps developers **install and consume the Maxio Advanced Billing .NET SDK**, +plus reusable guidance for working with **any APIMatic-generated .NET SDK**. -What makes this variant different: the **SDK map is not bundled in the plugin** — it ships inside the -SDK's own source, and the agents clone the SDK to read it. Sourced from +The **SDK map ships inside the SDK's own source**, and the agents clone the SDK to read it. Sourced from [apimatic/v4-plugins](https://github.com/apimatic/v4-plugins) `plugins/maxio-sdk`, extended with **subagent orchestration** (an `integrate-maxio` router plus `maxio-plan` and `maxio-debug` agents). ## The SDK map (shipped with the SDK, read from a clone) -In this variant the generated table-of-contents is **not** bundled in the plugin. It ships at the -**root of the SDK's own source** (`sdk-map.md` plus `map/` branch pages), so the `maxio-plan` and -`maxio-debug` agents **clone the SDK first** and read the map from the clone: +The generated table-of-contents ships at the **root of the SDK's own source** (`sdk-map.md` plus `map/` +branch pages), so the `maxio-plan` and `maxio-debug` agents **clone the SDK first** and read the map from +the clone: - **`map/operations/`** — one page per controller (33 pages, 247 operations): exact C# signature, must-pass-explicitly params, return type, error case (typed vs `RawError`) with its `TryGet…` @@ -28,9 +26,9 @@ error accessors, enum values — and only when the map can't settle a fact does file the map names, in the same clone. Grepping / globbing / `find`-ing the 600+-file tree to locate something is a defect — the map is the locator. -During the experiment the agents clone the fork/branch that carries the map -(`mohammadali2549/advanced-billing-sample-sdk` @ `docs/sdk-map`); once the map is merged upstream this -becomes the canonical `asadali214/advanced-billing-sample-sdk` at the released tag. +While the map is under review upstream, the agents clone the fork/branch that carries it +(`mohammadali2549/advanced-billing-sample-sdk` @ `docs/sdk-map`); once it is merged, this becomes the +canonical `asadali214/advanced-billing-sample-sdk` at the released tag. ## Two layers diff --git a/plugins/maxio-sdk-clone/agents/maxio-plan.md b/plugins/maxio-sdk-clone/agents/maxio-plan.md index 9c3e9ba..816262e 100644 --- a/plugins/maxio-sdk-clone/agents/maxio-plan.md +++ b/plugins/maxio-sdk-clone/agents/maxio-plan.md @@ -14,7 +14,7 @@ skills for usage traps. Your training data on this SDK is stale — every fact y come from a map page (or, under the guard below, a source file) you actually read this session. You never guess, and you never open the SDK's `api-reference.md`. -**Clone the SDK first.** The map is not bundled in the plugin — it travels with the source. +**Clone the SDK first.** The map travels with the SDK source, so cloning gets you both. Follow `maxio-getting-started`'s *SDK source & map* section: reuse the clone recorded in the shared session file `/maxio-sdk-src/.maxio-session.md` if one exists this session, otherwise clone once (shallow, the map-carrying ref) into a fresh timestamped folder under diff --git a/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md b/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md index 0e3ae23..e0349a4 100644 --- a/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md +++ b/plugins/maxio-sdk-clone/skills/integrate-maxio/SKILL.md @@ -126,13 +126,12 @@ Everything the main agent must NOT do is collected here, in one place: installed/NuGet package to find implementation. - **Don't handle the clone.** The SDK clone and its filesystem path belong to the plan and debug agents; you never read the clone and never need or receive its path. -- **Never open the SDK map or the clone yourself.** In this variant the map lives in the - helpers' SDK clone, not in the plugin — you have no map to read and must not touch the - clone; you work from the contract sheet. +- **Never open the SDK map or the clone yourself.** The map lives in the helpers' SDK clone — + you work from the contract sheet, not the map or the clone. - **Don't bulk-load reference material.** Don't load `maxio-getting-started` or the `dotnet-*` skills, and don't carry reference dumps — the contract sheet is your working reference. When - a fact is missing, ask the warm `maxio-plan` agent (there is no map in the plugin for you to - read). + a fact is missing, ask the warm `maxio-plan` agent (the helpers hold the map; you don't read it + directly). - **Don't open the SDK's `api-reference.md`, and don't web-search Maxio topics.** - **Never write a Maxio/SDK fact from memory** — every signature, field name, enum value, and error type in your code must come from the contract sheet or a lookup. And **never diff --git a/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md b/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md index 90b7042..57cbbe1 100644 --- a/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md +++ b/plugins/maxio-sdk-clone/skills/maxio-getting-started/SKILL.md @@ -1,6 +1,6 @@ --- name: maxio-getting-started -description: Identify and orient in the Maxio Advanced Billing (formerly Chargify) .NET SDK — its NuGet package id and root namespace, the namespace layout, US/EU environments and the Basic-auth pattern, and how the SDK map points you to the exact source file when one is needed. In this variant the map is NOT bundled in the plugin — it ships inside the SDK's own source, at the root of the clone. You clone the SDK first and read the map from the clone (`/sdk-map.md` + `/map/`) — a table of contents over every operation signature, error type, model, enum, and union — so you look facts up in the map and open exactly the source file the map names, in the same clone, instead of grepping. This is the helper-facing reference layer, preloaded for the maxio-plan and maxio-debug subagents; the main agent routes Maxio work through integrate-maxio and works from its contract sheet instead of loading this skill or touching the clone. It also routes you to the companion dotnet-* skills (client-setup, auth, calling endpoints, models, error handling, configuration/resilience, testing) and gates loading each at its step — load them even after you've read the SDK source, since the source shows signatures but not the usage gotchas these skills carry. +description: Identify and orient in the Maxio Advanced Billing (formerly Chargify) .NET SDK — its NuGet package id and root namespace, the namespace layout, US/EU environments and the Basic-auth pattern, and how the SDK map points you to the exact source file when one is needed. The SDK map ships inside the SDK's own source, at the root of the clone, so you clone the SDK first and read the map from the clone (`/sdk-map.md` + `/map/`) — a table of contents over every operation signature, error type, model, enum, and union — so you look facts up in the map and open exactly the source file the map names, in the same clone, instead of grepping. This is the helper-facing reference layer, preloaded for the maxio-plan and maxio-debug subagents; the main agent routes Maxio work through integrate-maxio and works from its contract sheet instead of loading this skill or touching the clone. It also routes you to the companion dotnet-* skills (client-setup, auth, calling endpoints, models, error handling, configuration/resilience, testing) and gates loading each at its step — load them even after you've read the SDK source, since the source shows signatures but not the usage gotchas these skills carry. --- # Getting started with the Maxio Advanced Billing .NET SDK @@ -29,8 +29,7 @@ map or source doesn't remove the need to load the skill for that step, so at eac companion *and* confirm names against the map. > **Ground every signature, model, enum, and error type in the SDK map that ships inside the SDK's own -> source.** In this variant the map is **not** bundled in the plugin — it lives at the root of the SDK source -> clone (`/sdk-map.md` + `/map/`). Clone the SDK first (see the *SDK source & map* section +> source.** The map lives at the root of the SDK source clone (`/sdk-map.md` + `/map/`). Clone the SDK first (see the *SDK source & map* section > below), then read the map: it carries every operation signature, error type, enum value list, field list > with JSON wire names, and union accessor by lookup, so most questions are answered by the map without > reading source. When the map can't answer something — a full method/model body, a fact that is absent or @@ -136,10 +135,9 @@ the compiler and re-read the source file the map's row names, in the same clone. ## SDK source & map — clone first, then map-first -Unlike the bundled-map plugin, this variant keeps the map **with the SDK source**: the map is generated -alongside the SDK and shipped at the root of its repo, so cloning the SDK gets you both the map and the -source it describes, always in lockstep. Cloning is therefore **step 0**, not a last resort — you cannot -read the map without it. Both `maxio-plan` and `maxio-debug` use the clone. +The map is generated alongside the SDK and shipped at the root of its repo, so cloning the SDK gets you +both the map and the source it describes, always in lockstep. Cloning is therefore **step 0**, not a last +resort — you cannot read the map without it. Both `maxio-plan` and `maxio-debug` use the clone. **Clone once per session; reuse it.** `maxio-plan` runs first and normally clones; `maxio-debug` reuses that clone. Before cloning, check the shared session file `/maxio-sdk-src/.maxio-session.md` for a clone @@ -161,10 +159,10 @@ git clone --depth 1 --branch docs/sdk-map https://github.com/mohammadali2549/adv # Record $dir under a `## Session artifacts` section in $env:TEMP\maxio-sdk-src\.maxio-session.md — it is your clone path for the session. ``` -> **Experimental clone source.** This variant clones the SDK **fork/branch that carries the map** -> (`mohammadali2549/advanced-billing-sample-sdk` @ `docs/sdk-map`) while the map is in review upstream. Once -> the map is merged into the canonical SDK, this becomes `asadali214/advanced-billing-sample-sdk` at the -> released tag. Either way the map and the source in that clone are the same version by construction. +> **Clone source.** Clone the SDK **fork/branch that carries the map** +> (`mohammadali2549/advanced-billing-sample-sdk` @ `docs/sdk-map`) while the map is in review upstream; once +> it is merged into the canonical SDK, clone `asadali214/advanced-billing-sample-sdk` at the released tag. +> Either way the map and the source in that clone are the same version by construction. The session file lives in `/maxio-sdk-src/`, **never** in the project repo, and the clone path never goes into `maxio-plan.md` — the main agent must never see the clone or its path.