# api-endpoint-tester

Adopt this skill: https://proskills.md/skills/other/lethe044-agentskill-kit-skills-api-endpoint-tester

Page: https://proskills.md/skills/other/lethe044-agentskill-kit-skills-api-endpoint-tester
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: skills/api-endpoint-tester/SKILL.md
size: 1875
sha: e00113fedcef4130b4cf4d8f829517f246fd772a
upstream: https://github.com/lethe044/agentskill-kit/blob/main/skills/api-endpoint-tester/SKILL.md

````text
---
name: api-endpoint-tester
description: Runs a list of HTTP checks against a base URL from a JSON file and reports status codes, response time and body assertions. Use when the user asks whether an API is up, wants a smoke test after a deploy, or needs to verify that endpoints return what the docs promise.
license: MIT
metadata:
  version: 1.0.0
  author: Lethe044
  tags: [api, http, testing, smoke-test]
---

# API endpoint tester

A tiny smoke test runner with no dependencies. It sends real HTTP requests, so only point it at services
the user owns or has permission to test.

## When to use

- After a deploy, to confirm the main endpoints respond.
- When the user suspects an endpoint changed shape or status code.
- To turn a list of curl commands into a repeatable check.

## How to run

```bash
python scripts/check_api.py checks.json --base-url https://staging.example.com
```

The checks file is a JSON list:

```json
[
  {"name": "health", "method": "GET", "path": "/health", "expect_status": 200, "expect_body_contains": "ok"},
  {"name": "create", "method": "POST", "path": "/items", "json": {"name": "demo"}, "expect_status": 201}
]
```

Optional fields: `headers` (object), `json` (request body), `expect_status` (default 200),
`expect_body_contains` (string) and `timeout` (seconds, default 10).

## Output

JSON with `base_url`, `passed`, `failed` and `results`. Each result has `name`, `ok`, `status`,
`elapsed_ms` and, when it failed, a `reason`. The exit code is 1 when any check fails.

## After running

Report failures first with the reason and status code. Mention slow endpoints (over one second). Do not retry
failing POST requests automatically, they may create duplicates.

## Errors

A missing checks file exits with code 2. Invalid JSON or a malformed check exits with code 3.
Connection errors are reported as failed checks, not crashes.
````

## File: skills/api-endpoint-tester/scripts/check_api.py
size: 2891
sha: 0345486871c78d64ed176d0ab161bd67f4dc5c28
upstream: https://github.com/lethe044/agentskill-kit/blob/main/skills/api-endpoint-tester/scripts/check_api.py

```text
#!/usr/bin/env python3
"""Run HTTP checks from a JSON file and report the results."""
import argparse
import json
import sys
import time
import urllib.error
import urllib.request

def run_check(base_url, check):
    method = str(check.get("method", "GET")).upper()
    url = base_url.rstrip("/") + "/" + str(check.get("path", "")).lstrip("/")
    headers = dict(check.get("headers") or {})
    data = None
    if "json" in check:
        data = json.dumps(check["json"]).encode("utf-8")
        headers.setdefault("Content-Type", "application/json")
    request = urllib.request.Request(url, data=data, headers=headers, method=method)
    started = time.time()
    status, body, reason = None, "", ""
    try:
        with urllib.request.urlopen(request, timeout=float(check.get("timeout", 10))) as resp:
            status = resp.status
            body = resp.read(200000).decode("utf-8", errors="replace")
    except urllib.error.HTTPError as exc:
        status = exc.code
        body = exc.read(200000).decode("utf-8", errors="replace")
    except (urllib.error.URLError, OSError) as exc:
        reason = "request failed: %s" % getattr(exc, "reason", exc)
    elapsed = int((time.time() - started) * 1000)
    if not reason:
        expected = int(check.get("expect_status", 200))
        if status != expected:
            reason = "expected status %d, got %d" % (expected, status)
        elif "expect_body_contains" in check and str(check["expect_body_contains"]) not in body:
            reason = "body does not contain %r" % check["expect_body_contains"]
    result = {"name": check.get("name") or "%s %s" % (method, check.get("path", "")), "ok": not reason,
              "status": status, "elapsed_ms": elapsed}
    if reason:
        result["reason"] = reason
    return result

def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("checks", help="JSON file with a list of checks")
    parser.add_argument("--base-url", required=True)
    args = parser.parse_args(argv)
    try:
        with open(args.checks, encoding="utf-8") as fh:
            checks = json.load(fh)
    except OSError:
        print("file not found: %s" % args.checks, file=sys.stderr)
        return 2
    except ValueError:
        print("invalid JSON in %s" % args.checks, file=sys.stderr)
        return 3
    if not isinstance(checks, list) or not all(isinstance(c, dict) and "path" in c for c in checks):
        print("checks must be a list of objects that each have a path", file=sys.stderr)
        return 3
    results = [run_check(args.base_url, c) for c in checks]
    failed = sum(1 for r in results if not r["ok"])
    print(json.dumps({"base_url": args.base_url, "passed": len(results) - failed, "failed": failed,
                      "results": results}))
    return 1 if failed else 0

if __name__ == "__main__":
    sys.exit(main())
```

## File: skills/api-endpoint-tester/tests/cases.json
size: 423
sha: 72e574414de9597acf6d3700daf76e2953c39e38
upstream: https://github.com/lethe044/agentskill-kit/blob/main/skills/api-endpoint-tester/tests/cases.json

```text
{
  "entry": "scripts/check_api.py",
  "cases": [
    {"name": "missing checks file", "kind": "error", "args": ["nope.json", "--base-url", "http://127.0.0.1:1"],
     "expect_exit": 2, "stderr_contains": "file not found"},
    {"name": "invalid json", "kind": "error", "args": ["bad.json", "--base-url", "http://127.0.0.1:1"],
     "files": {"bad.json": "{oops"}, "expect_exit": 3, "stderr_contains": "invalid JSON"}
  ]
}
```

## File: skills/api-endpoint-tester/tests/test_check_api.py
size: 3174
sha: b73e8ee6750a413975e66e84365c6c48be683cd0
upstream: https://github.com/lethe044/agentskill-kit/blob/main/skills/api-endpoint-tester/tests/test_check_api.py

```text
import json
import os
import subprocess
import sys
import tempfile
import threading
import unittest
from http.server import BaseHTTPRequestHandler, HTTPServer

SCRIPT = os.path.join(os.path.dirname(__file__), "..", "scripts", "check_api.py")

class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        return

    def _reply(self, code, body):
        data = body.encode()
        self.send_response(code)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self):
        if self.path == "/health":
            self._reply(200, "status ok")
        else:
            self._reply(404, "missing")

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        payload = json.loads(self.rfile.read(length) or b"{}")
        self._reply(201, "created " + payload.get("name", ""))

class CheckApiTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.server = HTTPServer(("127.0.0.1", 0), Handler)
        cls.url = "http://127.0.0.1:%d" % cls.server.server_address[1]
        threading.Thread(target=cls.server.serve_forever, daemon=True).start()

    @classmethod
    def tearDownClass(cls):
        cls.server.shutdown()
        cls.server.server_close()

    def run_script(self, checks, base=None):
        with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
            json.dump(checks, fh)
        try:
            proc = subprocess.run([sys.executable, SCRIPT, fh.name, "--base-url", base or self.url],
                                  capture_output=True, text=True, timeout=30)
        finally:
            os.unlink(fh.name)
        return proc

    def test_passing_checks(self):
        proc = self.run_script([
            {"name": "health", "path": "/health", "expect_body_contains": "ok"},
            {"name": "create", "method": "POST", "path": "/items", "json": {"name": "demo"}, "expect_status": 201,
             "expect_body_contains": "demo"}])
        self.assertEqual(proc.returncode, 0, proc.stderr)
        data = json.loads(proc.stdout)
        self.assertEqual((data["passed"], data["failed"]), (2, 0))

    def test_wrong_status_fails(self):
        proc = self.run_script([{"name": "gone", "path": "/nope"}])
        self.assertEqual(proc.returncode, 1)
        result = json.loads(proc.stdout)["results"][0]
        self.assertFalse(result["ok"])
        self.assertIn("expected status 200, got 404", result["reason"])

    def test_body_assertion_fails(self):
        proc = self.run_script([{"path": "/health", "expect_body_contains": "degraded"}])
        self.assertEqual(proc.returncode, 1)

    def test_connection_error_is_a_failed_check(self):
        proc = self.run_script([{"path": "/health", "timeout": 2}], base="http://127.0.0.1:1")
        self.assertEqual(proc.returncode, 1)
        self.assertIn("request failed", json.loads(proc.stdout)["results"][0]["reason"])

    def test_bad_checks_file(self):
        proc = self.run_script({"not": "a list"})
        self.assertEqual(proc.returncode, 3)

if __name__ == "__main__":
    unittest.main()
```

## File: README.md
upstream: https://github.com/lethe044/agentskill-kit/blob/main/README.md

````text
<p align="center">
  <img src="docs/assets/banner.svg" alt="agentskill-kit" width="100%">
</p>

<p align="center">
  <a href="https://github.com/Lethe044/agentskill-kit/actions/workflows/ci.yml"><img src="https://github.com/Lethe044/agentskill-kit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://pypi.org/project/agentskill-toolkit/"><img src="https://img.shields.io/pypi/v/agentskill-toolkit" alt="PyPI"></a>
  <a href="https://pypi.org/project/agentskill-toolkit/"><img src="https://img.shields.io/pypi/pyversions/agentskill-toolkit" alt="Python versions"></a>
  <a href="LICENSE"><img src="https://img.shields.io/github/license/Lethe044/agentskill-kit" alt="License"></a>
</p>

**agentskill-kit** is a command line toolkit for agent skills, the `SKILL.md` folders that coding agents such as
Claude Code, Cursor, Codex, Gemini CLI and Hermes load when a task matches. It checks a skill before you trust it,
generates new ones, and installs the same skill into every agent you use.

- **Lint** a skill: name, description, structure. Works offline.
- **Scan** it for prompt injection, hidden unicode, secrets and dangerous commands.
- **Test** it: run the tests that ship with the skill in an isolated folder.
- **Forge** a new skill from a task description with any model, including free and local ones.
- **Mine** your shell history for routines that should be skills.
- **Export** one skill to Claude Code, Cursor, Windsurf, Copilot, Codex and Gemini CLI.
- **Share** skills through a static registry with checksums and a searchable website.

No runtime dependencies. No paid service required. Python 3.9 or newer on Linux, macOS and Windows.

<p align="center">
  <img src="docs/assets/demo.svg" alt="Terminal session: lint, scan, test and forge" width="100%">
</p>

## Install

```bash
pip install agentskill-toolkit
```

The PyPI package is named `agentskill-toolkit` (the shorter name was not available). It installs the `askit` command and the longer alias `agentskill`. The repository, the Python module (`agentskill_kit`) and the commands keep the `agentskill-kit` name.

## Quick start

Check a skill without any API key:

```bash
askit lint skills/my-skill
askit scan skills/my-skill
askit test skills/my-skill
askit score skills/my-skill
```

Create one with a free model (a local Ollama model, or a Groq or Gemini key on the free tier):

```bash
askit forge --task "turn meeting notes into action items with owners and due dates"
```

Use it in your agents:

```bash
askit export skills/meeting-notes-to-actions --target claude
askit export skills/meeting-notes-to-actions --target cursor
askit export skills/meeting-notes-to-actions --target all
```

Install something from the community registry:

```bash
askit search docker
askit install dockerfile-linter
```

Try everything offline with the built-in demo model: `askit forge --task "anything" --provider mock`.

## Why this exists

Skills are plain text that an agent obeys, so they deserve the same care as code you run: a review, a linter, a
test and a look at what they do. At the same time every agent wants a slightly different file layout, and writing
good skills by hand is slow. agentskill-kit covers both: quality gates for skills, and tools that do the tedious
parts.

## Commands

| Command | What it does |
| --- | --- |
| `askit init NAME` | Create a skill from a template, optionally with a script and tests |
| `askit lint [PATH]` | Check SKILL.md structure (`--strict`, `--format json\|github`) |
| `askit scan [PATH]` | Security scan (`--fail-on high\|medium\|low\|never`) |
| `askit test [PATH]` | Run `tests/cases.json` and `tests/test_*.py` of each skill |
| `askit score [PATH]` | Combine lint, scan and tests into one score and a pass or fail gate |
| `askit forge` | Generate a skill with a model, verify it and refine it until it passes |
| `askit eval PATH` | Check how well a model follows a skill, with an optional baseline run |
| `askit mine` | Find repeated workflows in shell history and draft skills |
| `askit export PATH` | Convert and install a skill for another agent |
| `askit search`, `install`, `uninstall`, `list` | Work with the registry, GitHub folders and local folders |
| `askit pack`, `publish` | Bundle a skill, run the quality gate, prepare a pull request |
| `askit registry build\|site` | Build a registry index or a static website |
| `askit serve` | Browse the skills on your machine in a local web page |
| `askit providers`, `config`, `doctor` | Models, settings and environment checks |

Every command has `--help`. Add `--lang tr` for a Turkish interface.

## Free first, paid optional

| Provider | Cost | How |
| --- | --- | --- |
| Ollama, LM Studio | free, local | run a local model, no key |
| Groq | free tier | `GROQ_API_KEY` |
| Google Gemini | free tier | `GEMINI_API_KEY` |
| OpenRouter | free models | `OPENROUTER_API_KEY`, pick a `:free` model |
| Any OpenAI compatible server | yours | `ASKIT_BASE_URL`, `ASKIT_MODEL` |
| OpenAI, Anthropic | paid, optional | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` |

`askit providers` shows what is ready. Details in [docs/providers.md](docs/providers.md).

`forge` does not rely on tool calling. The loop is controlled by code, so small local models work. The model only
writes files; lint, scan and tests decide whether they are good enough.

## How forge decides

```
task or notes
      |
      v
  model drafts SKILL.md, scripts and tests/cases.json
      |
      v
  lint + scan + run tests in a clean folder  ->  score
      |                                           |
      |  below threshold: failures go back        |  at or above threshold,
      |  to the model (up to 3 rounds)            |  no high findings
      v                                           v
  best attempt kept                          written to skills/<name>/
```

The score is not the model's opinion. It comes from [real checks](docs/skill-format.md#quality-score).
Nothing is written when the best attempt misses the threshold, unless you pass `--force`.

## Mine your history

```bash
$ askit mine --file examples/shell-history-sample.txt
-- read 20 commands
Found 1 repeated workflow(s)

#1  git-status-add-commit  (4x)
     git status
     git add -A
     git commit -m <str>
     git push origin main

$ askit mine --file examples/shell-history-sample.txt --draft 1
```

Secrets are masked before anything is shown. See [docs/mining.md](docs/mining.md).

## Export targets

| Target | Result |
| --- | --- |
| `claude` | `.claude/skills/<name>/` or `~/.claude/skills/` with `--global` |
| `hermes` | `.hermes/skills/<name>/` or `~/.hermes/skills/` |
| `cursor` | `.cursor/rules/<name>.mdc` |
| `windsurf` | `.windsurf/rules/<name>.md` |
| `copilot` | `.github/instructions/<name>.instructions.md` |
| `codex` | managed block in `AGENTS.md` |
| `gemini` | managed block in `GEMINI.md` |

Exports are repeatable: managed blocks are replaced, not duplicated. More in [docs/export-targets.md](docs/export-targets.md).

## Starter skills

The registry ships 16 skills that pass every check. Nine of them include scripts with tests, so you can read how a
tested skill is built.

| Skill | What it does |
| --- | --- |
| `log-analyzer` | severity counts, time range and frequent errors of a log |
| `python-code-smells` | bare except, mutable defaults, missing docstrings and long functions |
| `csv-profiler` | column types, missing values and ranges for a CSV |
| `json-diff` | structural diff of two JSON files |
| `dockerfile-linter` | unpinned images, root user, ADD vs COPY, secrets in ENV |
| `env-file-auditor` | `.env` against `.env.example`, without printing values |
| `todo-extractor` | TODO, FIXME and HACK inventory of a source tree |
| `changelog-generator` | conventional commits to grouped release notes |
| `api-endpoint-tester` | smoke test an API from a JSON file |
| `commit-message-writer` | clear conventional commit messages |
| `pr-description-writer` | pull request descriptions reviewers can 
````
