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
Install
npx skills add https://github.com/ChronoAIProject/Ornn/tree/develop/examples/api-fetch-wrapper
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install chronoaiproject-ornn@llmmart
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_KEYonce at the top, pass it viaAuthorization: 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.
Reviews (0)
No reviews yet.
No comments yet.