rq

A better curl, powered by collections.

Your requests are Markdown files in your repo. rq runs them, renders the response as something you can read, and converts whatever you have today — telling you plainly about anything it could not carry.

$ rq curl --save-as issues 'curl -H "Accept: application/vnd.github+json" \ "https://api.github.com/repos/anthropics/claude-code/issues?state=open"' ✓ saved issues $ rq r issues ▸ issues GET .../issues?state=open 200 OK 241ms #1287 Support for streaming responses 3 comments #1284 Windows path handling in --project 1 comment

That last part is a template in the file, not a feature request. A request document can render its own response, so the thing you look at fifty times a day is the thing you shaped.

github/issues.md
---
url: '{{api}}/repos/{{owner}}/{{repo}}/issues'
query:
  state: open
vars:
  GH_TOKEN: { env: GH_TOKEN, secret: true }
capture:
  first: response[0].number
---

-- view --

{% for i in response %}
- [**#{{ i.number }}**]({{ i.html_url }}) {{ i.title }}
{% endfor %}

Getting started

Pre-release. There are no packaged binaries yet — no Homebrew tap, no apt repo. Today it builds from source with a Rust toolchain, and that is the honest state of it.
$ git clone https://github.com/amitu/rq && cd rq $ cargo install --path crates/rq

You do not have to convert anything to try it. Point rq at what you already have:

$ rq l acme.postman_collection.json reading acme.postman_collection.json in place as postman — 24 requests └── Acme/ ├── health GET .../health └── users/ ├── list GET .../users?page=1 └── create POST .../users

The collection stays exactly as it is — this is the same converter rq import runs, in memory. When you want it as an rq project, import it.

What it does

Chains without JavaScript

A request declares what it depends on and what it takes from the response. The common case — log in, use the token — needs no code at all.

me.md
---
url: '{{api}}/me'
parents: [login]
headers:
  Authorization: 'Bearer {{token}}'
---

Scripts, in whatever dialect they were written in

Postman's pm.* and Bruno's bru.* run unmodified. The file keeps the script its author wrote; the runtime reconciles it at execution, every run. That covers Postman v2.1, v2.0, and the v1.0 collections still sitting in repositories — tests['ok'] = responseCode.code === 200 and all.

A network panel for the terminal

rq r issues -c opens the run you just did: arrow between steps, drill into the request, the response, every header, and where the milliseconds went. Nothing is re-sent — a panel that re-issued your POST to show you what it did would be a bug. With --log it reaches back past this process to everything you have sent.

Files people edit, so a checker

$ rq check ✗ error me `parents: [signin]` — no request named `signin` ✗ error login `capture: token: resposne.access_token` starts at `resposne` ✗ error me `{{TOKEN}}` — nothing provides a value 3 requests · 3 errors · 0 warnings

All three are errors, and the last one is the policy in miniature: a request still carrying {{TOKEN}} is not sent. Putting a placeholder on the wire is always a bug — Bearer {{TOKEN}} comes back 401 and reads like a credentials problem, so you go looking at the API instead of at your file, and the request has already gone out. Failing costs one command. Declaring a variable is how you say it may legitimately be empty.

Formats

Everything maps through one idealised model, so a new format is one importer or one exporter, not glue against every other format.

FormatImportExport
Postman v2.1 / v2.0 / v1.0yesv2.1
Bruno .bru v2yesyes
rq (Markdown project)yesyes
cURLyes—
Requestly (LOCAL_FS, MappedItems)—yes
OpenAPI, Insomnia, HAR, .http, Hurlplanned

Conversion is checked against collections other tools produced — usebruno's own 223-request test collection, Adyen's published Postman collections, newman's samples — and each gate asserts more than "it parsed": every request in the source has to survive into the model, and round-trip loss has to stay inside a documented list. A conversion that drops something says so.

Where this is

Version 0.1: the loop works end to end, and the packaging does not exist yet. There is a longer account of the thinking and the current state in the first post.