Skip to main content
Glama
0xSoftBoi

Mini+ Agent Kit MCP Server

by 0xSoftBoi
README.md
# Mini+ Agent Kit

Drive a **[BitRobot](https://bitrobot.ai)**-compatible ground robot with Claude — an
[Earth Rover Mini+](https://bitrobot.ai/miniplusagentkit) or a Waveshare UGV — and
turn its runs into **Verifiable Robotic Work** on-chain.

It's built on the three real specs in the BitRobot ecosystem, not bespoke glue:

| Layer | Conforms to | This kit |
|---|---|---|
| **Robot** | [LeRobot](https://huggingface.co/docs/lerobot/en/earthrover_mini_plus) robot interface (`action {linear_velocity, angular_velocity}`, observation features) | `WaveshareUGV` (LeRobot backend) ↔ upstream `EarthRoverMiniPlus` |
| **Agent** | [openClaw branch](https://github.com/frodobots-org/earth-rovers-sdk/tree/feature/openClaw) — instruction files + safe high-level **verbs** | `RoverVerbs` + `MiniPlusAgent` (Claude, Opus 4.8) |
| **Work** | [BitRobot subnet API](https://docs.bitrobot.ai) — Verifiable Robotic Work (`task_start/end/validate`) | `WorkSink`: `BitRobotSink` + `OnchainRoverSink` (Arc) + `SolanaRoverSink` (clanker5000) + `RaceProofSink`, fan-out via `MultiSink` |

## Architecture

> 📐 Full system architecture with 13 validated diagrams, control equations, and
> the capability matrix: **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**.

```
            instruction files (AGENTS/SOUL/IDENTITY/TOOLS) ── compose ──► system prompt
                                                                              │
 Claude (Opus 4.8) ──► openClaw VERBS (tools) ─────────────────────────────► RoverVerbs
   status_report · look · photo · move · turn · obstacle_check · track_color ·   │   │
   autonav · navigate · checkpoint_reached · speak · set_lamp · camera_move ·    │   │
   capture_work · finish                                                         │   │
                                                                ┌───────────────┘   └───────────────┐
                                                       EarthRoverVerbs                       HarnessVerbs
                                                  (delegates to openClaw                (closed-loop turn via yaw,
                                                   server /turn /track-color…)           lidar obstacle_check…)
                                                          │                                     │
                                                  EarthRoverClient                        HarnessClient
                                                  (FrodoBots SDK)                         (robot-harness, Waveshare)

 capture_work ─► store_artifact (Walrus URL + IPFS CID + sha256) ─► WorkSink
                                                                     ├─ BitRobotSink   → /subnets/{id}/events (VRW → Bolts)
                                                                     └─ OnchainRoverSink → sidecar /proof → settle.ts (Arc/Solana)
```

One agent, two robots (swap the client), one artifact, two ledgers.

```mermaid
flowchart TB
  subgraph FE["Front-ends (clients)"]
    AG["MiniPlusAgent<br/>Claude tool-use loop"]
    CH["RoverChat / TelegramBridge"]
    MC["MCP server<br/>any MCP client"]
  end
  subgraph CORE["Verb core — single source of truth"]
    REG["VERBS registry<br/>name · cap · schema · handler"]
    MT["make_tools(caps, has_work)"]
    DP["dispatch(verbs, name, args)"]
    REG --> MT
    REG --> DP
  end
  subgraph VB["RoverVerbs — openClaw verb surface"]
    ER["EarthRoverVerbs"]
    HV["HarnessVerbs"]
  end
  subgraph TR["Transports"]
    EC["EarthRoverClient<br/>FrodoBots SDK"]
    HC["HarnessClient<br/>robot-harness"]
  end
  subgraph WK["Work layer"]
    WS["WorkSink"]
    BR["BitRobotSink"]
    OR["OnchainRoverSink (Arc)"]
    SR["SolanaRoverSink (clanker5000)"]
    RP["RaceProofSink"]
    WS --- BR
    WS --- OR
    WS --- SR
    WS --- RP
  end
  AG --> MT
  CH --> MT
  MC --> MT
  AG --> DP
  CH --> DP
  MC --> DP
  DP --> ER
  DP --> HV
  ER --> EC --> MINI["EarthRover Mini+"]
  HV --> HC --> UGV["Waveshare UGV"]
  DP -. capture_work .-> WS
```

**GPS waypoint navigation** (Earth Rover Challenge — Urban track):

```mermaid
flowchart TD
  S["goto_checkpoint()"] --> N["navigate(): GPS + heading,<br/>next un-scanned checkpoint"]
  N --> D{all checkpoints<br/>scanned?}
  D -- yes --> OK["mission complete"]
  D -- no --> T{distance &le; 15 m?}
  T -- yes --> CR["checkpoint_reached()"]
  CR --> N
  T -- no --> H{abs heading_error<br/>&gt; 18&deg;?}
  H -- yes --> TU["turn(heading_error)"]
  H -- no --> MV["move(forward)"]
  TU --> N
  MV --> N
```

**Verifiable Robotic Work** — one content-addressed artifact, multiple ledgers:

```mermaid
flowchart TB
  CAP["capture_work / submit_work"] --> ART["store_artifact:<br/>Walrus blobId + IPFS CIDv1 + sha256"]
  ART --> MS["MultiSink (one artifact, many ledgers)"]
  MS --> BR["BitRobotSink"]
  MS --> OR["OnchainRoverSink"]
  MS --> SR["SolanaRoverSink"]
  MS --> RP["RaceProofSink"]
  BR --> E1["POST /subnets/{id}/events<br/>VRW points &rarr; Bolts"]
  OR --> E2["POST /proof + /give-feedback<br/>ReputationRegistry &rarr; Arc (Clanker 500)"]
  SR --> E4["POST /proof + /give-feedback<br/>clanker5000.give_feedback &rarr; Solana (Clanker 5000)"]
  RP --> E3["POST /race/settle<br/>settle.settleRaceOnChain &rarr; Arc"]
```

More figures (agent loop, kinematics, visual servo, VRW lifecycle, Waveshare
command stack, module graph, end-to-end, tests) in
**[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**.

## Why verbs, not raw control

The openClaw kit's key lesson: an agent should drive through **safe, high-level
verbs**, never raw `/control`. So `turn` uses heading feedback (and is the *only*
way to rotate), `move` is distance-calibrated and aborts on a blocked path,
`status_report` returns real sensors (never fabricated), and `track_color` /
`autonav` hand off to purpose-built loops — exactly the two flagship demos
("send stats via chat", "find and follow the yellow card"). The agent's behavior
comes from editable markdown in [`instructions/`](mini_plus_agent_kit/instructions/).

## Install

```bash
pip install -e .                  # core
pip install -e ".[mcp]"           # + expose the rover as an MCP server
pip install -e ".[track]"         # + Waveshare client-side track_color (Pillow+numpy)
pip install -e ".[lerobot]"       # + Waveshare LeRobot backend (datasets/policies)
pip install -e ".[vision]"        # + Gemini scene captions for the harness
cp .env.example .env              # fill in keys
```

You also need a robot backend running:
- **Earth Rover**: the Earth Rovers SDK (`feature/openClaw`) — `hypercorn main:app` (:8000)
- **Waveshare**: your `robot-harness` (or the sidecar adapter) (:8000)

## Quick start — Claude drives, work goes on-chain

```python
from mini_plus_agent_kit import HarnessClient, MiniPlusAgent, BitRobotSink, OnchainRoverSink, MultiSink

rover = HarnessClient("http://localhost:8000", speed_mode="medium")   # Waveshare
work  = MultiSink(BitRobotSink(), OnchainRoverSink())                 # VRW + your settle
agent = MiniPlusAgent(rover, work=work, resource_name="ugv_001", on_event=print)

result = agent.run(
    "Explore the room, find the package, capture_work it as proof, then finish. "
    "Check obstacles before moving forward."
)
```

Swap `HarnessClient(...)` for `EarthRoverClient(...)` and the same agent drives an
Earth Rover Mini+ (gaining `speak` and a server-side blocking `turn`). The toolset
auto-adapts to each backend's verb capabilities. `track_color` ("follow the yellow
card") works on both — server-side on the Earth Rover, and as a client-side HSV
visual-servo loop on the Waveshare (`[track]` extra: Pillow+numpy).

### Drive from any MCP client (the elegant core)

The bundled agent and Telegram bridge are conveniences — the **standard** way to
drive the rover is as an [MCP](https://modelcontextprotocol.io) server. Point any
MCP client (Claude Desktop, Claude Code, Cursor, the Agent SDK) at it and the
verbs become its tools — no kit-specific agent loop:

```bash
mpak mcp --backend waveshare         # stdio MCP server (verbs as tools)
```

```jsonc
// Claude Desktop / Code config
{ "mcpServers": { "rover": { "command": "mpak", "args": ["mcp", "--backend", "waveshare"] } } }
```

Crucially this isn't a fourth code path: the MCP tool list is `make_tools(...)` and
each call routes through `dispatch(...)` — **the same single source of truth** the
Claude agent and the Telegram chat use. One verb definition, three front-ends
(agent / chat / MCP), every robot backend.

### CLI

```bash
mpak mission "Find and follow the yellow card." --backend earthrover --bitrobot
mpak mission "Patrol and flag obstacles."        --backend waveshare  --onchain --bitrobot
mpak telegram --backend waveshare                # chat-drive the rover (openClaw demo)
mpak register ugv_001 --backend waveshare --owner <SOL>   # BitRobot Entity NFT
mpak mcp --backend waveshare                     # serve as an MCP server (see above)
mpak sim               # run the nav stack in the simulator — no hardware (see below)
mpak status            # telemetry + mission state
mpak checkpoints       # list mission checkpoints (Earth Rover)
mpak shot --map -o frames/                       # save camera frames
mpak speak "hello"     # text-to-speech (Earth Rover)
mpak teleop            # manual keyboard driving
```

### Simulate without a robot (`mpak sim`)

`mpak sim` drives the **real `NavController`** through the unified `RoverSim`
(`sim.py`) — the same closed-loop nav stack as on hardware, but with simulated
GPS/IMU/odometry instead of a wired robot. It needs no backend, no keys, and no
network: a quick way to watch the rover converge on a checkpoint (or run a small
domain-randomized sweep) on any laptop before pointing the kit at a real rover.

```bash
mpak sim                       # one run to a checkpoint, printing convergence
```

### Chat surface (Telegram — the openClaw flagship demo)

`RoverChat` is a conversational agent over the same verbs; `TelegramBridge`
long-polls the Bot API and pipes messages through it, replying with text and
inline camera frames.

```python
from mini_plus_agent_kit import HarnessClient, TelegramBridge
bridge = TelegramBridge(HarnessClient("http://localhost:8000"),
                        token="<TELEGRAM_BOT_TOKEN>")
bridge.run_forever()      # "what's your status?" → status_report; "look" → photo inline
```

Or just `mpak telegram --backend waveshare` (reads `TELEGRAM_BOT_TOKEN` +
`ANTHROPIC_API_KEY`). Add `--bitrobot`/`--onchain` to record VRW from chat-driven runs.

## Verifiable Robotic Work

`capture_work` (or `submit_work`) stores the frame once and runs the task
lifecycle on whichever sink(s) you configured:

```python
from mini_plus_agent_kit import submit_work, BitRobotSink
rec = submit_work(BitRobotSink(), open("clip.jpg","rb").read(),
                  label="delivery @ door", vrw_points=120, resource_name="frodobot_001")
print(rec.artifact.ipfs_cid, rec.artifact.walrus_url)
```

- **`BitRobotSink`** → `register_resource` → `task_start` → `task_end {raw_data_uri, raw_data_cid}` → `task_validate {vrw_points}` (Subnet Points → Bolts; Entity NFT on Solana). `raw_data_uri` is the Walrus URL; `raw_data_cid` is computed (`cid_v1_raw` for ≤1 MiB, the `ipfs` CLI for larger).
- **`OnchainRoverSink`** (Arc / EVM — Clanker 500) → `task_end` posts `/proof {blobId, sha256, label}` (the tracker); `task_validate` posts `/give-feedback {robot, skill, score, blobId, sha256}`, which your sidecar anchors on Arc via `settle.giveFeedback` → `ReputationRegistry.giveFeedback`. The robot's on-chain `agentId` is resolved sidecar-side; key custody stays in the sidecar. `OnchainRoverSink(robot="guard", skill="deliver", score=None, anchor=True)` — VRW points map to the 0–100 reputation score unless you pass an explicit `score`; set `anchor=False` to register the proof without the chain write. (The kit sends the bare hex sha256 since `giveFeedback` re-adds `0x`.)
- **`SolanaRoverSink`** (Solana — Clanker 5000) → the *same* `/proof` + `/give-feedback` surface, but the native-Solana sidecar (port 4021) anchors it on the **`clanker5000`** Anchor program: `give_feedback` writes a per-agent reputation PDA carrying `feedback_uri = walrus://blobId` and `feedback_hash = sha256` (32 bytes). A score `≥ 70` clears the program's `ATTESTATION_THRESHOLD`; the call returns a **Solana signature** surfaced with an `explorer.solana.com` link and a `verified` flag. `SolanaRoverSink(robot="guard", skill="deliver", cluster="devnet")`. Register the agent once via the sidecar's `/register-agent` → `clanker5000.register_agent`. One robot run can anchor on **both ledgers at once** via `MultiSink(BitRobotSink(...), SolanaRoverSink(...))`.
- **`RaceProofSink(winner_idx=..., race_id=None)`** → `task_validate` posts `/race/settle {raceId, winnerIdx, sha256, blobId}` → `settle.settleRaceOnChain` → `RaceMarket.settle` (judge = guard). Use when the agent is the race oracle and *its* captured finish frame should settle the parimutuel market (unlike `/race/finish`, which re-captures the guard's own photo). Needs the small `POST /race/settle` route added to the sidecar (mirrors `/give-feedback`). `race_id=None` lets the sidecar use its current `onChainRaceId`.

### Register a robot as an Entity NFT (earn VRW under its own resource)

```bash
mpak register ugv_001 --backend waveshare --owner <SOLANA_WALLET> --symbol UGV
```

```python
from mini_plus_agent_kit import BitRobotSink
sink = BitRobotSink(resource_subtype="waveshare_ugv", resource_name="ugv_001", owner="<sol>")
sink.register(symbol="UGV", description="Waveshare UGV", image="https://…/ugv.png")
# every subsequent submit_work/capture_work on `sink` attributes VRW to ugv_001
```

`BitRobotSink` carries `resource_name`/`resource_subtype` defaults so all work auto-attributes to the registered Entity NFT (Waveshare → `waveshare_ugv`, Earth Rover → `frodobot`).

## Earth Rover Challenge (Urban track) — Claude as a navigation policy

The [Earth Rover Challenge](https://earth-rover-challenge.github.io/) (IROS 2026)
runs off-board policies that take the rover's camera + GPS and drive to mission
checkpoints within a **15 m** tolerance, scored by difficulty × completion time —
pitting autonomous policies against human teleoperators (current AI ceiling ~57%).
This kit *is* a challenge-ready off-board policy (it speaks the same Remote Access
SDK), with real GPS waypoint navigation:

- `navigate` — great-circle distance, bearing, and signed turn to the next
  checkpoint from live GPS + heading (`geo.py`: haversine + initial bearing +
  heading error; verified against the Berkeley→Stanford route).
- `checkpoint_reached` — claim arrival within tolerance.
- `EarthRoverVerbs.goto_checkpoint()` — a deterministic turn-to-bearing + creep
  controller (the autonomous baseline), or let Claude drive via `navigate` +
  `look` + `move`/`turn` (the VLM-agent baseline — vision handles obstacles GPS
  can't see). See [`examples/earth_rover_challenge.py`](examples/earth_rover_challenge.py).
- `EarthRoverVerbs.goto_checkpoint_fused()` — the **closed-loop navigation stack**
  (`estimator.py` + `control.py`): a PI complementary heading filter with online
  gyro-bias estimation (a 1-axis Mahony filter), a **Kalman pose filter**
  (covariance-weighted GPS fusion + Mahalanobis outlier gating), pure-pursuit
  steering, and a safety envelope (battery / tilt / lidar time-to-collision),
  composed behind `NavController.step(telemetry) → twist`. In a noisy A/B sim
  *with GPS multipath spikes*, the bang-bang baseline *false-arrives 34.6 m* from
  the checkpoint (a miss at 15 m tolerance) while the fused stack *gates every
  outlier*, *truly arrives* (14.9 m), and tracks heading 2.2× better than the raw
  magnetometer. See [§7.1 of the architecture spec](docs/ARCHITECTURE.md#71-closed-loop-fused-navigation-sensor-fusion--pursuit--safety).
- `EarthRoverVerbs.goto_checkpoint_planned(costmap)` — the **global+local split**
  (Nav2-style): A\* over an inflated occupancy `Costmap` (`planner.py`) finds a path
  *around* obstacles, tracked by **regulated pure pursuit** (velocity-scaled
  lookahead + curvature/approach speed regulation). A straight-line seeker drives
  into anything between it and the goal; in the sim it ploughs through a building
  for 46 ticks while the planned route reaches the checkpoint with **0 incursions**.
  See [§7.2](docs/ARCHITECTURE.md#72-global-planning-around-obstacles-a--regulated-pure-pursuit).
- `NavController(use_dwa=True)` — a **Dynamic Window Approach** local planner
  (`control.py`): samples the reachable `(v, ω)` window, rolls each candidate out,
  discards colliding trajectories, and scores goal/clearance/speed to *steer around*
  a moving obstacle (the reactive layer beneath the global plan). In the sim a
  pedestrian crosses the corridor: plain pursuit closes to 0.16 m (collision) while
  DWA holds 2.87 m clearance and still reaches the goal.
  See [§7.3](docs/ARCHITECTURE.md#73-local-obstacle-avoidance-the-dynamic-window-approach).
- **Estimator refinements** ([§7.4](docs/ARCHITECTURE.md#74-estimator-refinements-course-fusion-mag-calibration-full-covariance)):
  speed-gated **GPS-course fusion** (down-weights a magnetically-disturbed compass —
  cuts heading RMSE 24.9° → 10.4° under a 25° hard-iron bias), **magnetometer
  hard/soft-iron calibration** (`MagnetometerCalibrator`, ellipsoid → sphere), and a
  full **2×2 covariance** pose filter with anisotropic process noise + GPS-latency
  rewind, and a full-ellipsoid **soft-iron calibration** (`fit_ellipsoid`, numpy)
  that recovers heading to ~0° on a *rotated* ellipsoid (vs 16.7° for diagonal).
- **Domain-randomized validation** (`sim.py`): a unified `RoverSim` with a fully
  parameterized `SensorModel` drives the **real `NavController`**; a Monte-Carlo over
  **150 randomized worlds** (GPS/IMU/odometry error drawn per trial) reaches the
  checkpoint within tolerance **99.3%** of the time — robustness across an error
  envelope, not one tuned point. See
  [§7.5](docs/ARCHITECTURE.md#75-closing-the-sim-to-real-gap-unified-simulator--domain-randomization).

The live tests drive a 2D kinematic rover sim over real HTTP to a GPS checkpoint
(`tests/live/test_live_navigate.py`), run the fused-vs-baseline A/B under sensor
noise + GPS multipath (`tests/live/test_live_navstack.py`), rescue a biased
magnetometer with GPS-course fusion (`tests/live/test_live_heading.py`), route around
an obstacle with A\* + regulated pursuit (`tests/live/test_live_planner.py`), and
dodge a moving pedestrian with DWA (`tests/live/test_live_dwa.py`).

## Waveshare as a LeRobot robot

```bash
lerobot-record --robot.type=waveshare_ugv --teleop.type=keyboard_rover \
    --dataset.repo_id=you/ugv-nav --dataset.single_task="Navigate"
```

`WaveshareUGV` presents the Mini+ action/observation schema (plus lidar) so
datasets and policies are cross-compatible with the upstream EarthRover Mini+.

## Layout

```
mini_plus_agent_kit/
  client.py          EarthRoverClient   — FrodoBots SDK transport (+ openClaw verbs)
  harness_client.py  HarnessClient      — Waveshare robot-harness transport
  rover.py           RoverVerbs         — openClaw verb surface (2 backends)
  estimator.py       HeadingFilter/PoseFilter/MagnetometerCalibrator — heading+pose fusion, mag cal
  control.py         NavController       — pursuit + RPP + DWA + PID + safety stack
  planner.py         Costmap + A*        — global path planning around obstacles
  sim.py             RoverSim + SensorModel — unified simulator for domain-randomized MC
  work.py            WorkSink           — BitRobot VRW + onchain-rover, artifacts, IPFS CID
  agent.py           MiniPlusAgent      — Claude loop + run manifest + safety supervisor/watchdog
  observability.py   Run                — structured event log + JSON run manifest
  safety.py          SafetySupervisor/Watchdog — mission budgets + deadman emergency-stop
  tools.py           verb tools + dispatch
  telegram.py        RoverChat + TelegramBridge — chat surface (openClaw demo)
  mcp_server.py      MCP server over the same make_tools()+dispatch() core
  lerobot_backend.py WaveshareUGV       — LeRobot robot (optional)
  instructions/      AGENTS/SOUL/IDENTITY/TOOLS.md
  cli.py             mpak
```

## Tests

A hermetic suite (no robot, no network, no real SDKs — `httpx`/`anthropic` are
stubbed) covers kinematics, telemetry mapping, IPFS CID, capability-filtered
tools, the openClaw verb→endpoint wiring, the navigation stack (filters,
controllers, safety, costmap + A* planner, closed-loop sim scenarios), all three
work sinks, and a full scripted agent-loop run:

```bash
python3 tests/run_all.py     # zero-dependency runner (exits non-zero on failure)
pytest tests/                # also works (conftest applies the same stubs)
```

And a **live** suite using real libraries and real I/O — no stubs (a local HTTP
server emulates the harness; Walrus is a public testnet; no robot or keys needed):

```bash
bash tests/live/run_live.sh   # installs real deps (httpx, mcp, Pillow, numpy) into .venv, then runs:
#  • real HarnessClient/HarnessVerbs over real sockets (twist→diff on the wire,
#    telemetry/lidar, JPEG bytes, closed-loop turn, /light, /camera/move mapping)
#  • the real MCP server: real protocol (initialize→list_tools→call_tool)→dispatch→HTTP
#  • real track_color: HSV blob detection + visual-servo steering on generated frames
#  • real GPS navigate: the goto_checkpoint controller reaches a checkpoint in a sim
#  • navstack A/B: fused NavController (Kalman + Mahalanobis gating) truly arrives and rejects
#    GPS multipath where the bang-bang baseline false-arrives 34.6 m out
#  • heading: speed-gated GPS-course fusion rescues a 25°-biased magnetometer (24.9°→10.4°)
#  • planner: A* over a costmap + regulated pure pursuit routes around a building (0 incursions)
#  • dwa: dynamic-window local planner steers around a moving pedestrian (holds clearance, arrives)
#  • montecarlo: real NavController over 150 domain-randomized worlds → 99.3% true-arrival
#  • ellipsoid: full hard+soft-iron mag calibration recovers heading on a rotated ellipsoid
#  • solana: real httpx → emulated clanker5000 sidecar (give_feedback signature + explorer)
#  • real Walrus testnet store + byte-identical retrieve + IPFS CIDv1
```

## Observability & mission safety

Every mission produces an auditable **run manifest** (`observability.Run`) — a
structured event timeline (objective → verb → artifact CID/sha → on-chain tx →
safety events) + counters/timers, attached to `RunResult.manifest` (save with
`MPAK_MANIFEST_PATH`; live stream with `MPAK_LOG_LEVEL=INFO`). Two safety layers wrap
the loop beyond the per-command `SafetyEnvelope`: a **`SafetySupervisor`** enforcing
mission budgets (runtime, cumulative distance, geofence, battery — latching on
breach), and a **deadman `Watchdog`** thread that emergency-stops the robot if a turn
hangs (e.g. a blocked LLM call) and the loop stops petting it. Configure via
`MiniPlusAgent(..., limits=MissionLimits(...), watchdog_timeout_s=120)`.

## Safety

`turn` uses heading feedback; `move` aborts on a lidar-blocked path; the agent is
prompted to avoid people/traffic/ledges and to `obstacle_check` before advancing;
the loop stops the robot on exit and on any verb error. This is research/hobbyist
tooling — keep a human in the loop.

## Sources

- [bitrobot.ai/miniplusagentkit](https://bitrobot.ai/miniplusagentkit) · [docs.bitrobot.ai](https://docs.bitrobot.ai)
- [Earth Rovers SDK — openClaw branch](https://github.com/frodobots-org/earth-rovers-sdk/tree/feature/openClaw)
- [LeRobot EarthRover Mini+](https://huggingface.co/docs/lerobot/en/earthrover_mini_plus)

MIT.