Claude Skill

api-fetch-wrapper

Wrap a public HTTP API (Open-Meteo weather as the demo) with credential handling, error normalisation, and a single retry on transient network failures. Demonstrates the production-shaped baseline for any "skill that calls an external service" — env-based secrets, structured erro

LLM Mart · 0 points · 0 views 5 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download chronoaiproject-ornn-examples_api-fetch-wrapper-e7e21e9.zip · 3 KB
Part of chronoaiproject/ornn — 6 skills

Install

skills CLI npx skills add https://github.com/ChronoAIProject/Ornn/tree/develop/examples/api-fetch-wrapper
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install chronoaiproject-ornn@llmmart
Git git clone https://github.com/ChronoAIProject/Ornn.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole chronoaiproject/ornn collection as a plugin from our marketplace. Git is the plain clone.

README

api-fetch-wrapper

Call a public HTTP API with retries, structured errors, and no-leak credential handling. Demo target is Open-Meteo (keyless, so the example runs out of the box).

Run: echo '{"latitude":52.52,"longitude":13.41}' | bun run src/index.ts

Adapt: swap the upstream URL + shape; add Authorization header reading from env. The retry-and-error-mapping skeleton is the load-bearing part — don't drop it.

See SKILL.md for the full contract.

Skill manifest

api-fetch-wrapper

The case that breaks first in production — a skill that hits an external service. This example covers the four things every such skill needs: secret handling, retry policy, error normalisation, and no-leak-on-failure logging.

Contract

Input (stdin, JSON):

{ "latitude": 52.52, "longitude": 13.41 }

Output (stdout, JSON):

{
  "temperatureC": 18.4,
  "windSpeedKmh": 12.6,
  "fetchedAt": "2026-05-19T08:00:00.000Z"
}

Errors — written to stderr as {"error": "...", "cause": "..."} and exit code 1. Error messages NEVER include the raw upstream response body (it might echo a secret); they include the upstream status code and a short canonical reason.

Required environment

Var Purpose
OPEN_METEO_URL Optional override of the upstream host. Defaults to https://api.open-meteo.com.
(none for auth) Open-Meteo is keyless. For an auth'd API, the same skeleton reads process.env.YOUR_API_KEY and passes it via Authorization header.

Run locally

cd examples/api-fetch-wrapper
bun install
echo '{"latitude":52.52,"longitude":13.41}' | bun run src/index.ts

Adapt this

  • Different API — replace the URL, query params, and response shape. The retry / error-mapping skeleton stays.
  • API-key auth — read process.env.YOUR_API_KEY once at the top, pass it via Authorization: Bearer ${apiKey}, and ensure the key never appears in error messages.
  • Stronger retry — bump MAX_ATTEMPTS, add exponential backoff with jitter, or distinguish 5xx (retry) vs 4xx (do not retry) explicitly.
  • Cache — wrap the fetch in a TTL cache when the upstream rate-limits.
Files (ornn)
  • src
    • index.ts 3.6 KB
      /**
       * api-fetch-wrapper example skill.
       *
       * Reads `{ latitude, longitude }` from stdin, fetches the current
       * conditions from Open-Meteo, writes `{ temperatureC, windSpeedKmh,
       * fetchedAt }` to stdout. One retry on transient network failure.
       *
       * Demonstrates the four production-shaped patterns every "skill that
       * calls an external service" needs:
       *
       *   1. Secrets via env (none here — Open-Meteo is keyless — but the
       *      reading pattern is shown via OPEN_METEO_URL).
       *   2. Retry policy: one bounded retry on transient failure, no
       *      exponential blow-up.
       *   3. Error normalisation: stderr always carries a structured
       *      `{ error, cause }` blob, never the raw upstream body.
       *   4. No-leak on failure: the raw upstream response is never logged.
       */
      
      const OPEN_METEO_URL = process.env.OPEN_METEO_URL ?? "https://api.open-meteo.com";
      const MAX_ATTEMPTS = 2;
      const RETRY_DELAY_MS = 500;
      
      interface Input {
        latitude: number;
        longitude: number;
      }
      
      interface OpenMeteoResponse {
        current?: { temperature_2m?: number; wind_speed_10m?: number };
      }
      
      async function readStdin(): Promise<string> {
        const chunks: Buffer[] = [];
        for await (const chunk of process.stdin) chunks.push(chunk as Buffer);
        return Buffer.concat(chunks).toString("utf8");
      }
      
      function isTransient(status: number): boolean {
        // 408 timeout, 429 throttling, 5xx server side — all worth one retry.
        return status === 408 || status === 429 || (status >= 500 && status < 600);
      }
      
      async function fetchWeather(input: Input): Promise<OpenMeteoResponse> {
        const url = new URL("/v1/forecast", OPEN_METEO_URL);
        url.searchParams.set("latitude", String(input.latitude));
        url.searchParams.set("longitude", String(input.longitude));
        url.searchParams.set("current", "temperature_2m,wind_speed_10m");
      
        let lastStatus = 0;
        for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
          let res: Response;
          try {
            res = await fetch(url);
          } catch (err) {
            if (attempt === MAX_ATTEMPTS) {
              throw new Error("network failure after retry", { cause: err });
            }
            await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
            continue;
          }
      
          if (res.ok) {
            return (await res.json()) as OpenMeteoResponse;
          }
      
          lastStatus = res.status;
          if (!isTransient(res.status) || attempt === MAX_ATTEMPTS) {
            // Drain the body so the socket can be reused, but DO NOT include
            // it in the thrown error — upstream APIs sometimes echo secrets.
            await res.text().catch(() => undefined);
            throw new Error(`upstream returned ${res.status}`);
          }
          await res.text().catch(() => undefined);
          await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
        }
        throw new Error(`upstream returned ${lastStatus}`);
      }
      
      async function main(): Promise<void> {
        const raw = (await readStdin()).trim();
        if (!raw) throw new Error("expected JSON `{ latitude, longitude }` on stdin");
        const input = JSON.parse(raw) as Input;
        if (typeof input.latitude !== "number" || typeof input.longitude !== "number") {
          throw new Error("`latitude` and `longitude` must be numbers");
        }
      
        const upstream = await fetchWeather(input);
        const current = upstream.current ?? {};
      
        process.stdout.write(
          JSON.stringify({
            temperatureC: current.temperature_2m ?? null,
            windSpeedKmh: current.wind_speed_10m ?? null,
            fetchedAt: new Date().toISOString(),
          }) + "\n",
        );
      }
      
      main().catch((err: unknown) => {
        const message = err instanceof Error ? err.message : String(err);
        const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined;
        process.stderr.write(JSON.stringify({ error: message, cause }) + "\n");
        process.exit(1);
      });
      
  • package.json 282 B
    {
      "name": "@ornn-examples/api-fetch-wrapper",
      "version": "1.0.0",
      "private": true,
      "description": "Minimal HTTP-API-wrapping skill — example for the Ornn skill format",
      "type": "module",
      "main": "src/index.ts",
      "scripts": {
        "start": "bun run src/index.ts"
      }
    }
    
  • README.md 470 B
    # api-fetch-wrapper
    
    Call a public HTTP API with retries, structured errors, and no-leak credential handling. Demo target is Open-Meteo (keyless, so the example runs out of the box).
    
    **Run:** `echo '{"latitude":52.52,"longitude":13.41}' | bun run src/index.ts`
    
    **Adapt:** swap the upstream URL + shape; add `Authorization` header reading from env. The retry-and-error-mapping skeleton is the load-bearing part — don't drop it.
    
    See `SKILL.md` for the full contract.
    
  • SKILL.md 2.4 KB
    ---
    name: api-fetch-wrapper
    description: Wrap a public HTTP API (Open-Meteo weather as the demo) with credential handling, error normalisation, and a single retry on transient network failures. Demonstrates the production-shaped baseline for any "skill that calls an external service" — env-based secrets, structured error output, no leaked API keys in logs, and a deliberate retry policy. The Open-Meteo endpoint used here is keyless on purpose so the example runs without setup; replace `OPEN_METEO_URL` with your own host and add `process.env.MY_API_KEY` for an auth'd version.
    version: "1.0"
    license: MIT
    metadata:
      category: integration
      tag:
        - example
        - http
        - external-api
        - typescript
    ---
    
    # api-fetch-wrapper
    
    The case that breaks first in production — a skill that hits an external service. This example covers the four things every such skill needs: secret handling, retry policy, error normalisation, and no-leak-on-failure logging.
    
    ## Contract
    
    **Input** (stdin, JSON):
    
    ```json
    { "latitude": 52.52, "longitude": 13.41 }
    ```
    
    **Output** (stdout, JSON):
    
    ```json
    {
      "temperatureC": 18.4,
      "windSpeedKmh": 12.6,
      "fetchedAt": "2026-05-19T08:00:00.000Z"
    }
    ```
    
    **Errors** — written to stderr as `{"error": "...", "cause": "..."}` and exit code `1`. Error messages NEVER include the raw upstream response body (it might echo a secret); they include the upstream status code and a short canonical reason.
    
    ## Required environment
    
    | Var | Purpose |
    |---|---|
    | `OPEN_METEO_URL` | Optional override of the upstream host. Defaults to `https://api.open-meteo.com`. |
    | *(none for auth)* | Open-Meteo is keyless. For an auth'd API, the same skeleton reads `process.env.YOUR_API_KEY` and passes it via `Authorization` header. |
    
    ## Run locally
    
    ```bash
    cd examples/api-fetch-wrapper
    bun install
    echo '{"latitude":52.52,"longitude":13.41}' | bun run src/index.ts
    ```
    
    ## Adapt this
    
    - **Different API** — replace the URL, query params, and response shape. The retry / error-mapping skeleton stays.
    - **API-key auth** — read `process.env.YOUR_API_KEY` once at the top, pass it via `Authorization: Bearer ${apiKey}`, and ensure the key never appears in error messages.
    - **Stronger retry** — bump `MAX_ATTEMPTS`, add exponential backoff with jitter, or distinguish 5xx (retry) vs 4xx (do not retry) explicitly.
    - **Cache** — wrap the fetch in a TTL cache when the upstream rate-limits.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related