Agent skill
UK public data
Trains, the Tube, Parliament, Companies House and fuel prices from five free, JSON-first CLIs built for agents.
Use this skill
Install it into a project with the Skills CLI, or read the files below and adapt them to your own agent.
npx skills add shan8851/agent-skills --skill uk-public-dataSKILL.md
View this file on GitHub---
name: uk-public-data
description: Answer questions about UK trains, London transport, Parliament, companies and fuel prices using five free, JSON-first command-line tools built for agents (rail, tfl, parliament, ch, fuel) that read official public data. Use when the user asks things like "next train to Leeds", "is the 08:15 from King's Cross delayed", "is the Northern line running", "how do I get from Waterloo to Bank (step-free)", "how did MPs vote on…", "who is the MP for…", "status of the X Bill", "who are the directors of…", "who owns X Ltd", "is this company still active" or "cheapest diesel near SW1".
---
# UK public data
Five small command-line tools, one per data source. Each returns the same JSON envelope, so once you can read one you can read them all. They are free to install; three need a free API key.
## Which tool for which question
| Question | Tool (npm package) | Command to start with |
|---|---|---|
| National Rail departures, arrivals, delays, station codes | `rail` (`@shan8851/rail-cli`) | `rail departures <station> --to <station>` |
| Tube, Overground, DLR, Elizabeth line status; London journeys; live arrivals; Santander bikes | `tfl` (`@shan8851/tfl-cli`) | `tfl status <line>` / `tfl route <from> <to>` |
| Bills, Commons votes, MPs and peers, written questions | `parliament` (`@shan8851/parliament-cli`) | `parliament bill "<title>"` / `parliament divisions "<term>"` |
| Company profile, status, directors, owners (PSC), filings, charges, insolvency | `ch` (`@shan8851/companies-house-cli`) | `ch search "<name>"` then `ch info <number>` |
| Petrol and diesel prices near a postcode | `fuel` (`@shan8851/fuel-cli`) | `fuel near "<postcode>" --fuel B7_STANDARD` |
Mainline trains into a London terminus are `rail`; the Underground and other TfL services are `tfl`. Bus status is not covered by either.
## Before the first command
1. **Check the tool is installed:** `command -v rail` (macOS/Linux) or `where rail` (Windows). Swap in the binary you need.
2. **If it's missing, ask the user before installing anything.** Offer either a global install, `npm install -g @shan8851/<package>`, or a one-off run with `npx -y @shan8851/<package> <command>`. All five need Node.js 22 or later (`node --version`).
3. **Check for a key if the tool needs one** (table below). If the key is missing, the tool returns `AUTH_ERROR`. Tell the user exactly where to get one; don't guess or work around it.
| Tool | Environment variable | Needed? | Free sign-up |
|---|---|---|---|
| `rail` | `DARWIN_ACCESS_TOKEN` | Required for `departures` and `arrivals`; `search` works without | Rail Data Marketplace, https://raildata.org.uk/ (see `references/rail.md`: the old National Rail sign-up page is gone) |
| `tfl` | `TFL_APP_KEY` | Optional; raises TfL rate limits | https://api-portal.tfl.gov.uk/ |
| `parliament` | none | — | — |
| `ch` | `COMPANIES_HOUSE_API_KEY` | Required for every command | https://developer.company-information.service.gov.uk/ |
| `fuel` | `FUEL_FINDER_CLIENT_ID` and `FUEL_FINDER_CLIENT_SECRET` | Required to download data (a warm local cache can still be read) | https://www.developer.fuel-finder.service.gov.uk/ (needs a GOV.UK One Login) |
Keys go in the shell environment or in a `.env` file in the directory the command runs from (each tool loads `.env` automatically). Make sure that `.env` is git-ignored. Never print, echo, log or commit a key, and never paste one into a command line that ends up in shell history or chat.
## Reading the output
Always add `--json` when you are going to parse the result. Without it, output is text in a terminal and JSON when piped, which is easy to get wrong.
Every response is one envelope:
```json
{ "ok": true, "schemaVersion": "1", "command": "departures", "requestedAt": "…", "data": { … } }
{ "ok": false, "schemaVersion": "1", "command": "departures", "requestedAt": "…",
"error": { "code": "AMBIGUOUS_LOCATION", "message": "…", "retryable": false, "details": { … } } }
```
| Exit code | Meaning | What to do |
|---|---|---|
| 0 | Success | Answer from `data`. An empty list is a real answer ("no trains in the next window"), not an error. |
| 2 | Bad input, not found, or ambiguous | Read `error.details.candidates`, then re-run with the exact id or code from the candidate that fits. If more than one could fit, ask the user. |
| 3 | Missing or rejected key, rate limit, timeout, or upstream failure | If `error.retryable` is `true`, retry once after a short pause. If not, report the message. For `AUTH_ERROR`, tell the user how to get or fix the key. |
| 4 | Internal error in the tool | Report it; don't retry in a loop. |
Error codes you will see: `INVALID_INPUT`, `NOT_FOUND`, `AMBIGUOUS_LOCATION` (rail, tfl), `AMBIGUOUS_QUERY` (parliament, fuel), `AUTH_ERROR`, `RATE_LIMITED`, `TIMEOUT`, `UPSTREAM_API_ERROR`, `INTERNAL_ERROR`, and `UNSUPPORTED_MODE` (tfl).
`parliament` and `ch` list commands keep the query under `data.input` and paging under `data.pagination`. `tfl` (route, arrivals, bikes) and `fuel` take `--output <dot.path>` to return one value or subtree, for example `--output journeys.0.durationMinutes`; with `--json` it still comes back inside the envelope as `data`.
## Worked example
User: "When's the next train from King's Cross to Leeds? Is it on time?"
1. `command -v rail` finds the binary, and `DARWIN_ACCESS_TOKEN` is set.
2. Run `rail departures KGX --to LDS --limit 3 --json`. Station names work too (`"kings cross"`), but codes avoid ambiguity. If you only have a vague name, `rail search "kings cross" --json` returns `{ name, crs }` candidates.
3. Read the envelope. `ok` is `true`, so look at `data.services` (illustrative values):
```json
{ "station": { "crs": "KGX", "name": "London Kings Cross" },
"filter": { "crs": "LDS", "name": "Leeds", "type": "to" },
"services": [
{ "scheduledTime": "14:33", "status": "on-time", "statusLabel": "On time",
"platform": "5", "operatorName": "London North Eastern Railway", "counterpartName": "Leeds" },
{ "scheduledTime": "15:03", "expectedTime": "15:10", "status": "expected", "statusLabel": "Exp 15:10",
"operatorName": "London North Eastern Railway", "counterpartName": "Leeds" } ] }
```
`status` is one of `on-time`, `expected` (running late, with `expectedTime`), `delayed` (late, no estimate), `cancelled` or `unknown`. A missing `platform` means it hasn't been announced. Add `--expand` only if the user asks where it calls.
4. Answer in one or two lines: "The next direct train is the 14:33 LNER to Leeds, platform 5, on time. The one after, the 15:03, is expected at 15:10." The tool doesn't return delay reasons, so don't invent one.
If step 2 had returned exit code 2 with `AMBIGUOUS_LOCATION` (for example a bare `"london"`), pick the right CRS code from `error.details.candidates` or ask the user which station they meant. If it had returned `UPSTREAM_API_ERROR` with `retryable: true`, retry once, then tell the user the live feed is unavailable rather than guessing times.
## Limits worth knowing up front
- `parliament` looks members up by name or id, not by constituency, and divisions are Commons only and give totals, not how each MP voted. See `references/parliament.md` for how to handle "who is the MP for…".
- `rail` goes through a third-party Huxley2 server by default, with no uptime guarantee.
- `fuel near` needs a full postcode or coordinates (a district like `SW1` is rejected) and `--fuel` every time. `fuel station` matches names against its local copy of the national dataset.
- Companies House data is what companies filed. "Who owns it" means persons with significant control (`ch psc`), not the full ownership chain.
## References
Read the one you need; don't load them all.
- `references/rail.md`: `rail` commands, flags, token route and the Huxley2 backend.
- `references/tfl.md`: `tfl` status, disruptions, journeys (step-free options), arrivals, bikes.
- `references/parliament.md`: bills, divisions, members, written questions, paging.
- `references/companies-house.md`: search, advanced search, officers, PSC, filings, charges, insolvency.
- `references/fuel.md`: `fuel near`, `fuel station`, fuel types, cache and freshness.
- `references/examples.md`: edge cases (missing key, ambiguous names, upstream down, empty results).
references/companies-house.md
View this file on GitHub# ch: Companies House
Package `@shan8851/companies-house-cli`, binary `ch`. Node.js 22+. Homepage: https://ch-cli.xyz
Company search and discovery, profiles, officers, filing history, persons with significant control (PSC), charges and insolvency, from the Companies House Public Data API.
## Commands
| Command | Answers |
|---|---|
| `ch search "<name>"` | Find a company and its number. `--restrictions active-companies` hides dissolved ones. |
| `ch search-advanced <filters>` | Discovery by location, SIC code, status, type and dates (filters below). |
| `ch info <number>` | "Is this company still active?" Status, type, incorporation date, registered office, SIC codes, accounts and confirmation statement dates. |
| `ch officers <number>` | "Who are the directors?" Directors and secretaries, current and resigned. |
| `ch psc <number>` | "Who owns or controls it?" Persons with significant control and their nature of control. |
| `ch filings <number>` | Filing history. |
| `ch charges <number>` | Mortgages and other secured charges. |
| `ch insolvency <number>` | Insolvency history. No history is returned as an empty result, not an error. |
| `ch search-person "<name>"` | Officers matching a name, with their appointments across companies. |
Company numbers are zero-padded automatically: `ch info 9215862` becomes `09215862`. Scottish and other prefixed numbers (`SC123456`, `NI…`, `OC…` for LLPs) are passed as given.
## Flags
**Paging** (search, search-advanced, officers, psc, filings, charges, search-person): `--items-per-page <n>` (default 10), `--start-index <n>` (zero-based), `--all` (fetch every page). `--all` can't be combined with a non-zero `--start-index`. Paging details are in `data.pagination`, the query in `data.input`.
**officers**: `--order-by <field>` (for example `appointed_on`), `--register-type <type>` and `--register-view <true|false>` (passed straight to Companies House; register type is for example `directors`, `secretaries` or `llp-members`).
**psc**: `--register-view <true|false>`.
**filings**: `--category <categories>` (comma-separated, for example `accounts`, `confirmation-statement`, `officers`, `capital`, `incorporation`); `--type` is an alias. `--include-links` adds direct document download URLs.
**search-person**: `--match-limit <n>` (default 10) caps how many matching officers are enriched with their appointments. Each one costs an extra API call, so keep it low for common names.
**search-advanced filters** (at least one is required):
`--company-name-includes <text>`, `--company-name-excludes <text>`, `--company-status <status>` (`active`, `dissolved`…), `--company-type <type>` (`ltd`, `plc`, `llp`…), `--company-subtype <subtype>`, `--location <text>` (registered office), `--sic-codes <codes>` (comma-separated), `--incorporated-from` / `--incorporated-to` and `--dissolved-from` / `--dissolved-to` (`YYYY-MM-DD`).
Examples:
- `ch search-advanced --location Bristol --company-status active --sic-codes 62012 --items-per-page 20 --json`
- `ch search-advanced --company-name-includes bakery --company-name-excludes holdings --location York --json`
- `ch search-advanced --incorporated-from 2025-01-01 --incorporated-to 2025-03-31 --company-type ltd --json`
Results land under `data.companies`. Start with `--items-per-page`; only use `--all` when the full list is really needed.
**Output**: `--json`, `--text` on the subcommand (canonical) or at root (`ch --json search …`); `--no-color` or `NO_COLOR`.
## Key: `COMPANIES_HOUSE_API_KEY`
Required for every command. Free from https://developer.company-information.service.gov.uk/: create an account, create an application, and add a REST API key for the live environment. Set it in the environment or a git-ignored `.env`. The API uses HTTP Basic auth with the key as the username and a blank password; the tool handles that.
Rate limit: 600 requests per 5 minutes per key. `--all` and `search-person` can use many requests; a `RATE_LIMITED` error is retryable after a pause.
## Reading the answers carefully
- **Active or not**: report `companyStatus` exactly as given (`active`, `dissolved`, `liquidation`, `administration`…). "Active" on the register doesn't mean trading. Mention it if `accounts.overdue` or `confirmationStatement.overdue` is true.
- **Directors**: separate current officers from resigned ones (resigned ones have a `resignedOn` date).
- **Ownership**: PSC records show people or entities with over 25% of shares or votes, or significant influence. If the PSC is another company, say so; the chain stops there unless you look that company up too. PSC data is self-reported and may be out of date.
- Companies House data is what companies filed, not verified facts. For due diligence, say what each point is based on.
- `search` matches names loosely; confirm the company number with `ch info` before answering about "X Ltd", especially when several similar names exist.
## Gotchas
- There's no ambiguity error here: `search` returns a list. Pick the best match by exact name and status, or ask the user if several fit.
- Handled errors go to stdout as JSON in `--json` mode, not stderr.
- Exit codes: 0 success, 2 bad input or not found, 3 auth, rate limit or upstream failure, 4 internal error.
references/examples.md
View this file on GitHub# Edge cases
Short worked cases for the situations that go wrong most often. The JSON is trimmed to the parts that matter.
## 1. Missing key
User: "Who are the directors of Monzo?"
`ch search "Monzo" --json` returns:
```json
{ "ok": false, "command": "search",
"error": { "code": "AUTH_ERROR", "retryable": false,
"message": "Missing COMPANIES_HOUSE_API_KEY. Set it in your environment or add it to a local .env file." } }
```
Exit code 3, not retryable. Don't retry, and don't fall back to guessing from memory. Tell the user:
> I need a free Companies House API key for this. Create an account at https://developer.company-information.service.gov.uk/, create an application and add a REST API key, then set it as `COMPANIES_HOUSE_API_KEY` in your shell or a git-ignored `.env` file. Don't paste the key here; I don't need to see it.
The same pattern applies to `rail` (`DARWIN_ACCESS_TOKEN`, see `rail.md` for the current sign-up route) and `fuel` (`FUEL_FINDER_CLIENT_ID` and `FUEL_FINDER_CLIENT_SECRET`). `tfl` and `parliament` don't need a key.
## 2. Ambiguous station or name
User: "Next train from London to Brighton?"
`rail departures "london" --to "brighton" --json` returns exit code 2:
```json
{ "ok": false, "error": { "code": "AMBIGUOUS_LOCATION", "retryable": false,
"details": { "query": "london", "candidates": [
{ "crs": "BFR", "name": "London Blackfriars" }, { "crs": "LBG", "name": "London Bridge" },
{ "crs": "CST", "name": "London Cannon Street" }, { "crs": "CHX", "name": "London Charing Cross" },
{ "crs": "EUS", "name": "London Euston" } ] } } }
```
The candidate list is short (five here) and alphabetical, so the station the user wants may not be in it: Victoria is missing. Use `rail search "london" --limit 20 --json` to see more. Several London stations have direct trains to Brighton, so don't pick one silently. Either ask ("Victoria, London Bridge or Blackfriars?") or, if the user wants the next train from anywhere in London, run `departures` for each likely station (`VIC`, `LBG`, `BFR`) with `--to BTN --limit 3` and give the earliest, saying which station it leaves from.
When only one candidate fits the context (the user said "the Northern line at King's Cross", and `tfl` offered `HUBKGX` alongside Seven Kings and Kingsbury), re-run with that `id` without asking.
`parliament` and `fuel` use `AMBIGUOUS_QUERY` in the same way: re-run with the `billId`, member `id` or station node id from the candidates.
## 3. Upstream down
User: "Is my 17:42 from Leeds on time?"
`rail departures LDS --json` returns exit code 3:
```json
{ "ok": false, "error": { "code": "UPSTREAM_API_ERROR", "retryable": true, "details": { "status": 500 },
"message": "Rail departures lookup for \"LDS\" failed with HTTP 500. Set RAIL_API_URL to a working Huxley instance if the public service is unavailable." } }
```
`retryable` is true, so retry once after a few seconds. If it fails again:
1. Check `DARWIN_ACCESS_TOKEN` is set; the public Huxley2 server also answers HTTP 500 when no token is sent.
2. If the token is set, say the live feed isn't responding and suggest checking National Rail Enquiries directly. Mention that the default backend is a third-party server with no uptime guarantee, and that `RAIL_API_URL` can point at another Huxley2 instance.
Never fill the gap with a timetable guess.
## 4. Empty result
User: "How did MPs vote on assisted dying?"
`parliament divisions "assisted dying" --json` returns `ok: true`, exit 0, with `"results": []` and `totalResults: 0`.
An empty list is a valid answer, but here it's a search-term problem: division titles use the bill's formal name. Find it with `parliament search bills "terminally ill"` (it returns the Terminally Ill Adults (End of Life) Bill), then run `parliament divisions "Terminally Ill" --json`. Report the matching divisions with dates and aye and no totals, and say that the tool gives totals, not how individual MPs voted.
Other empty results to read correctly:
- `rail departures … --to …` with no services: no direct trains in the board's window. Say that; don't claim there are no trains at all.
- `ch insolvency <number>` with an empty list: no insolvency history on the register.
- `fuel near … --fuel HVO` with no stations: none within the radius sell it. Offer a larger `--radius`.
## 5. Something the tools can't answer
User: "Who is the MP for Leeds Central and Headingley?"
`parliament member "Leeds Central and Headingley"` returns `NOT_FOUND`, because `member` matches names, not constituencies. Say so plainly, and point to https://members.parliament.uk/ for a constituency search. If the user then names the MP, `parliament member "<name>"` confirms party and House.
references/fuel.md
View this file on GitHub# fuel: UK fuel prices
Package `@shan8851/fuel-cli`, binary `fuel`. Node.js 22+. Homepage: https://github.com/shan8851/fuel-cli
Forecourt prices from the UK government's Fuel Finder scheme, ranked by price, distance and how recently each price was updated.
## Commands
| Command | What it does |
|---|---|
| `fuel near "<postcode \| lat,lon>" --fuel <type>` | Stations near a UK postcode or coordinates, for one fuel type. `--fuel` is required. |
| `fuel station "<node id \| text>"` | One station's detail: prices for every fuel, opening times, address. |
## Fuel types
| Code | Fuel |
|---|---|
| `E10` | Standard unleaded petrol |
| `E5` | Super unleaded petrol |
| `B7_STANDARD` | Standard diesel |
| `B7_PREMIUM` | Premium diesel |
| `B10` | B10 biodiesel |
| `HVO` | HVO renewable diesel |
"Petrol" means `E10` unless the user says super; "diesel" means `B7_STANDARD`. Petrol and diesel are never ranked together.
## `near` flags and defaults
- `--radius <distance>`: default 5 miles. A bare number is miles; `8mi` or `8km` also work.
- `--limit <n>`: default 10, maximum 50.
- `--sort best | price | distance | freshest`: default `best`, which balances price, distance and price freshness. For "cheapest", use `--sort price`, but mention how far away it is and how old the price is.
- `--refresh`: download fresh data before answering.
- `--output <path>`, `--json`, `--text`; `fuel --no-color …` or `NO_COLOR`.
The location must be a full UK postcode (`SW1A 1AA`) or `lat,lon` coordinates. Postcode districts (`SW1`) and place names are rejected with `INVALID_INPUT`. If the user gave only a district or a place, either ask for a full postcode or pick a well-known full postcode in that district (say which one you used, and widen `--radius` if needed).
## `station`
Matches a node id exactly, or matches text against the station's name, brand, postcode and first address line. The text match runs against the national dataset the tool keeps in its local cache, not against a live search. Stations are refreshed at most hourly and prices every 15 minutes, and if a download fails the tool quietly falls back to the older cached copy.
So:
- If a station isn't found or its prices look old, run it again with `--refresh` (`fuel station "<query>" --refresh`), or run a `fuel near … --refresh` for the area first.
- Several matches return `AMBIGUOUS_QUERY` with candidates (name, brand, address, postcode, node id). Re-run with the node id.
- Project one value with `--output station.prices.0.pencePerLitre` or a subtree with `--output station.openingTimes`.
## Data quality
Read `data.quality` before answering:
- Each price has a freshness band: `fresh` (updated in the last 30 minutes), `aging` (up to 3 hours), `stale` (older) or `unknown` (no timestamp). `data.quality.freshnessCounts` totals them.
- `data.quality.advisories` lists warnings (stale prices, missing timestamps, excluded test stations). Pass relevant ones on, for example "this price was last updated two days ago".
- Likely test or demo forecourts are hidden when real stations exist; the count is in `data.quality.excludedLikelyTestStations`.
Prices are reported by retailers under the scheme. They can lag the pump, so present them as "reported" prices.
## Credentials: `FUEL_FINDER_CLIENT_ID` and `FUEL_FINDER_CLIENT_SECRET`
Required to download data. Free: sign in with a GOV.UK One Login at the Fuel Finder developer portal, https://www.developer.fuel-finder.service.gov.uk/, and create an application to get a client id and secret. GOV.UK's guidance page is https://www.gov.uk/guidance/access-the-latest-fuel-prices-and-forecourt-data-via-api-or-email.
```sh
export FUEL_FINDER_CLIENT_ID=your_client_id
export FUEL_FINDER_CLIENT_SECRET=your_client_secret
```
Or put the same two lines (without `export`) in a git-ignored `.env`. Without credentials, the tool can still answer from any copy already in its cache (possibly out of date; check freshness). With no cache, every command returns `AUTH_ERROR` (exit 3).
Optional: `FUEL_FINDER_BASE_URL` (API base, defaults to `https://www.fuel-finder.service.gov.uk`) and `FUEL_CACHE_DIR` (cache location; defaults to the platform cache folder).
## Gotchas
- Postcode lookups go through postcodes.io; a typo returns `INVALID_INPUT` or `NOT_FOUND`.
- An empty `stations` list means nothing selling that fuel within the radius. Offer a wider `--radius` rather than saying there's none.
- Exit codes: 0 success, 2 bad input, not found or ambiguous, 3 auth, rate limit, timeout or upstream failure, 4 internal error.
references/parliament.md
View this file on GitHub# parliament: UK Parliament
Package `@shan8851/parliament-cli`, binary `parliament`. Node.js 22+. No key needed. Homepage: https://www.parliament-cli.xyz
Bills, Commons divisions (votes), members of both Houses and written questions, from the official Parliament APIs (Bills, Commons Votes, Members, Written Questions).
## Commands
| Command | What it does |
|---|---|
| `parliament bill <id \| "title">` | One bill by id, or a title resolved to a single match. Returns `shortTitle`, `currentHouse`, `currentStage`, `isAct`, `isDefeated`, `lastUpdate`. |
| `parliament search bills "<query>"` | Search bills by title. `search` supports only `bills`. |
| `parliament divisions <id \| "term">` | One Commons division by id, or search division titles. `votes` is an alias. |
| `parliament member <id \| "name">` | One MP or peer by id, or a name resolved to a single match. Returns `nameDisplayAs`, `party`, `house`. |
| `parliament questions <id \| "term">` | One written question by id, or search question text. |
## Flags
- `--take <n>` (default 10) and `--skip <n>` (default 0) page through search results on `search bills`, `divisions` and `questions`. Paging details are in `data.pagination` (`totalResults`, `returnedCount`).
- `--json`, `--text`, `--no-color` work at root or on the subcommand. `NO_COLOR` is also respected.
## What it can and can't answer
- **"Status of the X Bill"**: `parliament bill "X"`. If it returns `AMBIGUOUS_QUERY`, pick the right `billId` from `error.details.candidates` (they include stage and whether it became an Act) and run `parliament bill <billId>`. A bill that received Royal Assent shows `isAct: true` and the Act's title.
- **"How did MPs vote on…"**: `parliament divisions "<words from the division title>"`, then `parliament divisions <divisionId>`. You get the title, date, and aye and no totals. You don't get how individual MPs voted, and there are no House of Lords divisions. Division titles use formal bill names, so search for "Terminally Ill Adults", not "assisted dying". If a topic search returns nothing, find the bill's formal title first with `parliament search bills`.
- **"Who is the MP for <constituency>?"**: not directly supported. `member` matches names, not constituencies, and the record doesn't include a constituency. Say so, and suggest https://members.parliament.uk/ for a constituency lookup. If the user gives a name, `parliament member "<name>"` confirms party and House.
- **Written questions**: search matches question text. Results include `uin` and `house`.
## Gotchas
- Short or common names ("smith") return `AMBIGUOUS_QUERY` with candidates from both Houses. Filter on `house: "Commons"` when the user asked about an MP.
- An empty `results` list with `ok: true` means nothing matched; try other words before saying it doesn't exist.
- Advanced: the API base URLs can be overridden with `PARLIAMENT_BILLS_API_BASE_URL`, `PARLIAMENT_COMMONS_VOTES_API_BASE_URL`, `PARLIAMENT_MEMBERS_API_BASE_URL` and `PARLIAMENT_QUESTIONS_API_BASE_URL`. Normally leave them alone.
- Exit codes: 0 success, 2 bad input, not found or ambiguous, 3 rate limit or upstream failure, 4 internal error.
references/rail.md
View this file on GitHub# rail: National Rail live boards
Package `@shan8851/rail-cli`, binary `rail`. Node.js 22+. Homepage: https://rail-cli.xyz
Live departures and arrivals for every National Rail station in Great Britain, plus station-name search. Data comes from National Rail's Darwin feed (the Live Departure Boards web service, LDBWS) through a Huxley2 proxy.
## Commands
| Command | What it does |
|---|---|
| `rail departures <station>` | Live departures. `<station>` is a name (`"kings cross"`) or a 3-letter CRS code (`KGX`). |
| `rail arrivals <station>` | Live arrivals. |
| `rail search <query>` | Find stations by name. Returns `{ name, crs }` candidates. No token needed. |
## Flags
| Flag | Applies to | Notes |
|---|---|---|
| `--to <station>` | departures | Only services that call at this station. |
| `--from <station>` | arrivals | Only services that called at this station. |
| `--expand` | departures, arrivals | Adds calling points to each service. Larger output; use only when needed. |
| `--limit <n>` | departures, arrivals, search | Default 10. |
| `--select name`, `crs` or `name,crs` | search | Return only those fields. |
| `--stdin` | search | Read newline-separated queries from stdin: `printf "waterloo\nvictoria\n" \| rail search --stdin --json`. Can't be combined with a positional query. |
| `--json` / `--text` | all | Force output format. |
| `--no-color` | root (`rail --no-color departures KGX`) | Or set `NO_COLOR`. |
## Output
`departures` and `arrivals` return `data.station`, `data.filter` (when `--to`/`--from` was used) and `data.services[]`. Each service has `scheduledTime`, `expectedTime` (only when different from scheduled), `status` (`on-time`, `expected`, `delayed`, `cancelled`, `unknown`), `statusLabel`, `platform` (absent until announced), `operatorName`, `counterpartName` (destination for departures, origin for arrivals) and, with `--expand`, `callingPoints`.
The tool does not pass on delay or cancellation reasons, or station messages. Don't make them up; point the user to National Rail Enquiries if they want the reason.
Ambiguous names return `AMBIGUOUS_LOCATION` with `error.details.candidates` (`{ crs, name }`). `"london"` is ambiguous; `"kings cross"` or `KGX` is not. Re-run with the CRS code.
## Token: `DARWIN_ACCESS_TOKEN`
Required for `departures` and `arrivals`. The tool sends it to Huxley2 as an `accessToken` query parameter, and Huxley2 passes it on to National Rail's LDBWS service.
**Where to get one now.** The old registration page (`realtime.nationalrail.co.uk/OpenLDBWSRegistration/...`) no longer exists; it returns 404. National Rail's developer page (https://www.nationalrail.co.uk/developers/darwin-data-feeds/) now sends developers to the Rail Data Marketplace, https://raildata.org.uk/. Register there (free), find the Live Departure Board Web Service (LDBWS) public product in the catalogue, subscribe, and copy the **consumer key** from the subscription's API details.
**Not yet confirmed:** whether a Rail Data Marketplace consumer key works through Huxley2 in the same way as the old tokens did. Third-party guides report that the consumer key works as an LDBWS access token, but National Rail lists the Marketplace product as a JSON API, and Huxley2 talks to the older interface. If the user has a key and `rail departures` returns `AUTH_ERROR` (HTTP 401/403) or a persistent `UPSTREAM_API_ERROR`, say plainly that the key or the backend may not be compatible, rather than retrying.
Third-party guides also say tokens issued under the old scheme still work for now. That isn't confirmed by National Rail, so if an old token starts failing, a Marketplace key is the route to try.
## Backend: `RAIL_API_URL`
Optional. Defaults to `https://huxley2.azurewebsites.net`, the public Huxley2 demo server. It is run by a third party, and its own README says it has no uptime guarantee and regularly goes down. If departures fail with `UPSTREAM_API_ERROR` (exit 3) while `rail search` still works, the demo server or the token is the likely cause. Users who need reliability can run their own Huxley2 instance and set `RAIL_API_URL` to it.
Note: without a token the demo server currently answers board requests with HTTP 500, which the tool reports as a retryable `UPSTREAM_API_ERROR`, not `AUTH_ERROR`. So check `DARWIN_ACCESS_TOKEN` is set before blaming the server.
## Gotchas
- CRS codes are three letters (`KGX` King's Cross, `LDS` Leeds, `EDB` Edinburgh, `MAN` Manchester Piccadilly). Use `rail search` when unsure.
- "Is the 08:15 delayed?": run `departures` from the origin, optionally with `--to`, and find the service whose `scheduledTime` is `08:15`. Boards only cover the next couple of hours, so a train much later in the day won't be there yet; say so.
- Times are local UK time as shown on station boards, with no date.
- Exit codes: 0 success, 2 bad input, not found or ambiguous, 3 auth, rate limit, timeout or upstream failure, 4 internal error.
references/tfl.md
View this file on GitHub# tfl: Transport for London
Package `@shan8851/tfl-cli`, binary `tfl`. Node.js 22+. Homepage: https://github.com/shan8851/tfl-cli
Line status, disruptions, journey planning, live arrivals, stop search and Santander Cycles docks, from the official TfL Unified API.
## Commands
| Command | What it does |
|---|---|
| `tfl status [line]` | Current status of all supported lines, or one (`northern`, `elizabeth`, `overground`). |
| `tfl disruptions [line]` | Current disruption notices. |
| `tfl route <from> <to>` | Journey planner. |
| `tfl arrivals <stop>` | Live arrivals at a stop or station. |
| `tfl search stops <query>` | Find stops and stations; returns ids you can reuse. |
| `tfl bikes <location>` | Nearby Santander Cycles docks with bikes and spaces. |
Locations can be station names, postcodes (`SE1 9SG`), coordinates (`51.50,-0.12`) or TfL stop ids (`940GZZLUWLO`, hub ids like `HUBKGX`).
## Flags
**status, disruptions**
- `--mode <modes>`: comma-separated, from `tube`, `overground`, `dlr`, `elizabeth-line`. Default is all four.
- `--detail` (status only): include TfL's free-text reason, for example the cause of "Minor Delays". Use it whenever a line isn't "Good Service".
- Bus status is out of scope. Trams and river services aren't covered by `status` either.
**route**
- `--date YYYY-MM-DD`, `--time HH:MM`; add `--arrive-by` to treat the time as an arrival time. Without them, it plans for now.
- `--via <location>`.
- `--mode <modes>`: comma-separated, for example `tube,walking`. Values include `public-bus`, `overground`, `train`, `tube`, `coach`, `dlr`, `cablecar`, `tram`, `river`, `walking`, `cycle`, `national-rail`, `elizabeth-line`.
- `--preference least-time | least-interchange | least-walking`.
- `--accessibility <values>`: comma-separated, from `no-requirements`, `no-solid-stairs`, `no-escalators`, `no-elevators`, `step-free-to-vehicle`, `step-free-to-platform`. For "step-free" requests use `step-free-to-platform`, or `step-free-to-vehicle` if the user needs level boarding.
- `--max-walk-minutes <n>`.
- `--output <path>`.
**arrivals**
- `--line <line>`, `--direction inbound | outbound | all`, `--limit <n>` (default 10), `--output <path>`.
**bikes**
- `--radius <metres>` (default 500), `--limit <n>` (default 10), `--output <path>`.
**search stops**
- `--limit <n>` (default 10).
**All commands:** `--json`, `--text`; `tfl --no-color …` or `NO_COLOR`.
## `--output` projection
Available on `route`, `arrivals` and `bikes`. Dot paths with zero-based indexes: `journeys.0.durationMinutes`, `journeys.0.legs`, `arrivals.0.timeToStationSeconds`, `bikePoints.0.bikes`. A malformed path returns `INVALID_INPUT`; a path that doesn't exist returns `NOT_FOUND`. With `--json`, the projected value is returned as `data` inside the normal envelope.
## Key: `TFL_APP_KEY`
Optional. Everything works without it; a key gives higher rate limits. Free from https://api-portal.tfl.gov.uk/ (sign up, then subscribe to a product to get a key). Set it in the environment or `.env`.
## Gotchas
- Ambiguous places return `AMBIGUOUS_LOCATION` with candidates `{ id, name, modes }`. "kings" matches King's Cross, Seven Kings, Kingsbury and more. Re-run with the candidate's `id`.
- A status `severity` of 10 is "Good Service"; lower numbers are worse. Read `description` rather than relying on the number.
- `UNSUPPORTED_MODE` (exit 2) means a mode outside the lists above.
- Journey times are TfL's estimate for the planned time, not a guarantee. Check `status` for the lines the route uses if the user is travelling now.
- Exit codes: 0 success, 2 bad input, not found, ambiguous or unsupported mode, 3 auth, rate limit, timeout or upstream failure, 4 internal error.