Move CLI facts out of the skill and into the CLI

The skill had grown into a reference manual: body type tables, auth
strategy lists, a template function table, field-by-field OAuth 2.0
config. All of it goes stale, because the user's CLI version and their
installed plugins decide what actually exists. On this machine a faker
plugin contributes 274 template functions; the skill listed eleven, and
got some names wrong.

The CLI should carry that knowledge, so:

- `yaak template-function list [filter]` and `template-function show
  <name>` report what the loaded plugins actually provide, the same way
  `request schema http` already merges in plugin auth strategies.
- `yaak folder schema` now exists, so folder payloads are discoverable
  like every other model. Required deriving JsonSchema on Folder.
- The request schema documents `bodyType` and `body` shapes, and states
  that setting a body type does not add a Content-Type header.

The skill drops to a single 135-line SKILL.md that teaches the model,
the workflows, and how to interrogate the CLI, and says outright that
the CLI wins when the two disagree.
This commit is contained in:
Gregory Schier
2026-08-13 21:45:43 -07:00
parent 37b143021d
commit 74369e8f23
12 changed files with 270 additions and 755 deletions
+84 -215
View File
@@ -11,256 +11,125 @@ description: >
call, exercise, or smoke test an HTTP or REST endpoint, to save or organize
API requests for reuse, to set up API requests for manual testing, to add auth
to a saved request, to turn an OpenAPI or Postman collection into runnable
requests, or to run a saved request suite against staging versus production. Prefer this over one-off `curl` commands whenever
the requests should be saved, reused, shared, or run as a set.
requests, or to run a saved request suite against staging versus production.
Prefer this over one-off `curl` commands whenever the requests should be
saved, reused, shared, or run as a set.
allowed-tools: Bash(yaak:*), Bash(which:*), Bash(command:*), Bash(npm:*), Bash(npx:*)
---
# Use Yaak
Yaak is a desktop API client. The `yaak` CLI reads and writes the **same local
database as the desktop app**, so anything created here shows up in the app
database as the desktop app**, so anything you create shows up in the app
immediately, and vice versa. There is no server and no sign-in: `yaak auth` is
only for publishing plugins to the Yaak registry, not for any workflow below.
only for publishing plugins to the Yaak registry.
Two consequences worth holding onto. Requests you create are permanent user data
in an app they use, not scratch files, so name them the way the user would and
clean up anything created just to test. And because the app is right there, the
CLI is usually the wrong place to *read* a response in detail; it is the right
place to build, organize, and run requests.
## The CLI describes itself
**This skill deliberately does not list fields, body types, auth strategies, or
template functions.** The user's CLI version and installed plugins decide what
exists, so any list written here would eventually be wrong. Ask the CLI:
```bash
yaak --help # commands, plus agent hints at the bottom
yaak <command> --help # flags for one command
yaak request schema http --pretty # full request model, with guidance per field
yaak template-function list [filter] # template functions from installed plugins
yaak template-function show <name> # one function's arguments
```
`request schema http` is generated from the real model and merges in the auth
strategies contributed by plugins, so it is the authoritative answer for what a
request payload may contain and what each auth strategy needs. `workspace`,
`environment`, and `folder` have `schema` subcommands too.
Read the relevant schema before writing a JSON payload you are not certain of.
That is faster than a failed send, and it stays correct as Yaak changes.
## Resource model
- **Workspace** (`wk_…`) is the top-level container. It owns everything else.
- **Folder** (`fl_…`) groups requests inside a workspace and can nest. Folders
carry headers and authentication that child requests inherit.
- **Request** (`rq_…`) is a single HTTP, gRPC, or WebSocket request.
- **Environment** (`ev_…`) holds variables. Every workspace has one base
environment ("Global Variables") plus any number of sub-environments; a
sub-environment overrides base variables of the same name.
- **Cookie jar** (`cj_…`) stores cookies per workspace. The oldest jar is used
by default; no setup needed.
- **Workspace** (`wk_…`) is the top-level container.
- **Folder** (`fl_…`) groups requests and can nest. Folders carry headers and
authentication that child requests inherit, which is the usual way to apply
one token to a whole group.
- **Request** (`rq_…`) is a single HTTP, gRPC, or WebSocket request. The CLI can
currently only create and send HTTP ones.
- **Environment** (`ev_…`) holds variables. Each workspace has a base
environment plus any number of sub-environments; a sub-environment overrides
base variables of the same name and is chosen per send with `-e`.
- **Cookie jar** (`cj_…`) stores cookies per workspace. The oldest is used by
default, so this normally needs no attention.
IDs are stable and prefix-typed, so you can always tell what an ID refers to.
Most commands take a workspace ID positionally and **infer it when the machine
has exactly one workspace**. Pass it explicitly once a second workspace exists.
IDs are prefix-typed, so you can always tell what an ID refers to. Commands that
take a workspace ID infer it when exactly one workspace exists.
## Preflight
## Getting oriented
```bash
yaak --version || npm install -g @yaakapp/cli
yaak workspace list
```
`workspace list` prints `wk_… - Name` per line, or `No workspaces found`. Pick
the workspace that matches the user's project before mutating anything; create
one only when nothing fits.
Pick the workspace matching the user's project before changing anything, and
create one only when nothing fits. If a documented command is unrecognized, the
CLI is older than this skill: update it with `npm install -g @yaakapp/cli@latest`
and re-run `yaak agent install`, then tell the user to restart their coding tool.
**When the CLI and this skill disagree, the CLI is right.**
If a subcommand documented here is not recognized, the CLI is older than this
skill. Update it, then refresh the skill so the two stay in lockstep:
## Core workflows
```bash
npm install -g @yaakapp/cli@latest && yaak agent install
```
**Start from a spec when one exists.** `yaak import <file>` auto-detects OpenAPI,
Swagger, Postman, Insomnia, cURL, and Yaak exports, and beats authoring requests
by hand every time.
Mention to the user that they need to restart their coding tool for the
refreshed skill to load; the current session keeps using the old copy.
**Make the host swappable.** Put the base URL in a base-environment variable,
reference it as `${[ base_url ]}`, then add a sub-environment per deployment
target. Now `yaak -e ev_staging send <wk_id>` runs everything against staging.
## Command map
| Goal | Command |
|---|---|
| List | `yaak {workspace,folder,request,environment,cookie-jar} list` |
| Inspect one | `yaak {workspace,folder,request,environment} show <id>` |
| Create | `yaak {workspace,folder,request,environment} create` |
| Update | `yaak {workspace,folder,request,environment} update --json '{"id":"…",…}'` |
| Delete | `yaak … delete <id> --yes` |
| Inspect the model | `yaak request schema http --pretty`, `yaak workspace schema`, `yaak environment schema` |
| Send one request | `yaak request send <rq_id>` |
| Send a folder or whole workspace | `yaak send <fl_id\|wk_id>` |
| Import an existing API | `yaak import <file>` |
| Export | `yaak export <file> [workspace_id…]` |
Global flags go anywhere but are clearest before the subcommand:
`-e/--environment <ev_id>`, `--cookie-jar <cj_id>`, `-v/--verbose`,
`--data-dir <path>` (point at an isolated database, useful for scratch work).
## Creating and updating
Simple requests take flags:
```bash
yaak request create wk_abc123 --name "List Pets" --method GET --url "https://api.example.com/pets"
```
Anything richer than name/method/URL takes a JSON payload, either positionally
or via `--json`. **Read the schema before writing a payload you are unsure
of** — it is generated from the real model and includes the plugin-provided
authentication variants:
```bash
yaak request schema http --pretty
```
That schema is also the authoritative list of **authentication strategies**,
including ones contributed by plugins. Each appears as a named variant under
`authentication`, with its own fields, required list, and enums, so there is
never a reason to guess auth config:
```bash
# every installed strategy, with the value to use for authenticationType
yaak request schema http | jq -r '.properties.authentication.oneOf[]
| select(.title) | "\(.title): \(.description)"'
# the full shape of one of them
yaak request schema http | jq '.properties.authentication.oneOf[]
| select(.title == "OAuth 2.0")'
```
The first prints lines like `OAuth 2.0: Authentication values for strategy
'oauth2'`. **The title is a display label, not the value**`authenticationType`
takes the quoted strategy name, so NTLM Auth is `windows` and AWS Signature is
`awsv4`.
The second prints the fields. OAuth 2.0 has fifteen of them plus an enum of valid
`grantType` values (`authorization_code`, `implicit`, `password`,
`client_credentials`), which is exactly the sort of thing that comes out wrong
when guessed. The command loads plugins, so it reflects what is actually
installed rather than a fixed list, and it returns in well under a second.
Rules that are easy to get wrong:
- **Setting `bodyType` does not add a `Content-Type` header.** The app adds one
when you pick a body type in the UI, but creating a request from the CLI skips
that step, and the body goes out untyped. Add the header yourself, using the
same value as `bodyType` (with `other``text/plain` and `graphql`
`application/json`). Multipart is the exception: leave it alone, the sender
supplies the boundary.
- **Path parameters must keep the leading colon.** For `/pets/:petId`, the
`urlParameters` entry is named `:petId`, not `petId`. Get it wrong and the
placeholder stays literal in the path while the value is appended to the query
string, which usually 404s with no error.
- **Create** payloads must omit `id` (or set it to `""`).
- **Update** payloads must include `id`, and are applied as a JSON merge patch:
keys you omit are left alone, and a key set to `null` is deleted. There is no
need to send the whole object.
- Flags and JSON cannot be combined on the same command.
- `request create` and `request list` are HTTP-only. gRPC and WebSocket requests
exist in the model and can be sent from the app, but the CLI cannot yet create
or send them.
The first two fail silently, so verify a new request with `yaak -v request send
<id>` and check the `> ` lines actually show the path and headers you intended.
```bash
yaak request create wk_abc123 --json '{
"name": "Create Pet", "method": "POST", "url": "${[ base_url ]}/pets",
"bodyType": "application/json",
"body": {"text": "{\"name\":\"Rex\"}"},
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
}'
```
See [requests.md](references/requests.md) for bodies, headers, authentication,
path parameters, and folder inheritance.
## Template variables
Yaak's template syntax is `${[ … ]}`, **not** `{{ … }}`. It works in URLs,
headers, bodies, and auth fields:
**Chain instead of shell-plumbing.** A request can read another request's
response directly, and Yaak sends the dependency first if it needs to:
```
${[ base_url ]}/pets/${[ pet_id ]}
${[ response.body.path(request='rq_abc123', path='$.token') ]}
${[ response.body.path(request='rq_login', path='$.token') ]}
```
Referencing a variable that no active environment defines is a hard error and
the request is not sent, so an unresolved variable can never silently reach the
network. See [environments.md](references/environments.md) for variable scoping
and [chaining.md](references/chaining.md) for pulling values out of earlier
responses.
Run `yaak template-function show response.body.path` for its arguments,
including how to control when the upstream request re-sends. Chain when a
request genuinely depends on another's response; to merely run requests in
order, `yaak send <fl_id>` already does that.
## Sending
**Run a set.** `yaak send` accepts a folder or workspace ID, with `--fail-fast`
and `--parallel`. Workspace and request IDs survive an export/import, so a
committed `yaak export` plus `--data-dir ./.yaak` gives a runnable suite in CI.
```bash
yaak request send rq_abc123 # body only, on stdout
yaak -e ev_staging request send rq_abc123 # against a sub-environment
yaak -v request send rq_abc123 # request/response metadata too
yaak send fl_abc123 --fail-fast # every request in a folder
yaak send wk_abc123 --parallel # every request in a workspace
```
## Reading results
**Reading the result.** A plain send writes only the response body to stdout,
with no trailing newline. Like `curl`, the exit code reflects whether the
request completed, not the HTTP status — a 404 or 500 exits 0. Use `-v` when the
status matters:
A plain send writes only the response body to stdout. Add `-v` for the request
and response metadata, where lines are prefixed `*`, `>`, and `<`:
```bash
yaak -v request send rq_abc123 2>&1 | grep '^< HTTP'
```
Under `-v`, connection/request/response lines (`*`, `>`, `<`) and the body all
go to stdout, with the body following the last `<` header line. Grep for the
prefixes you need rather than assuming a clean split.
Exit code 1 means the send itself failed: an unresolved template variable, an
unreachable host, a TLS failure. For folders and workspaces the last line is
`Send summary: N succeeded, M failed`, per-request errors follow on stderr, and
the exit code is 1 if any request failed.
## Running a suite in CI
Workspace and request IDs survive an export/import, so a committed export gives
a stable, runnable suite on a machine that has never seen the app:
```bash
npm install -g @yaakapp/cli
yaak --data-dir ./.yaak import ./api-export.json
yaak --data-dir ./.yaak -e ev_ci send wk_abc123 --fail-fast
```
`--data-dir` keeps the run isolated from any real Yaak install, and the IDs in
the export are the same ones you used locally. Produce the export with
`yaak export ./api-export.json wk_abc123`, adding
`--include-private-environments` only if the suite needs values you are willing
to commit — otherwise keep secrets in a CI-only environment and inject them.
**The caveat that matters here:** a failing *assertion* is not a concept Yaak
has, and HTTP error statuses do not fail the run. A workspace of requests that
all return 500 exits 0. The exit code catches unreachable hosts, TLS failures,
and unresolved variables only. To gate CI on status codes, run with `-v` and
check the `< HTTP` lines yourself. Say this plainly rather than implying a green
run means the API is healthy.
## Importing
`yaak import` auto-detects OpenAPI/Swagger, Postman, Insomnia, cURL, and Yaak
exports, and creates a new workspace by default:
```bash
yaak import ./openapi.yaml
yaak import ./collection.json --workspace-id wk_abc123 # merge into an existing one
```
This is almost always faster than authoring requests by hand when a spec exists.
See [import-export.md](references/import-export.md).
## Routing
| Task | Reference |
|---|---|
| Request bodies, headers, auth, path/query params, folder inheritance | [requests.md](references/requests.md) |
| Environment hierarchy, variables, per-environment runs | [environments.md](references/environments.md) |
| Using one response inside the next request; template functions | [chaining.md](references/chaining.md) |
| OpenAPI/Postman/Insomnia/cURL import, exporting workspaces | [import-export.md](references/import-export.md) |
Exit code 1 means the send did not complete: an unresolved template variable, an
unreachable host, a TLS failure. **HTTP error statuses are not failures.** Like
`curl`, a 404 or 500 exits 0, and a folder of requests that all return 500
reports success. Never tell the user an API is healthy based on a clean exit;
check the status yourself with `-v`.
## Execution rules
1. Resolve the workspace before mutating. Do not create a second workspace when
an existing one matches the user's project.
2. Read the schema before writing a non-trivial JSON payload. Do not guess field
names.
3. Prefer `update` merge patches over re-sending whole objects.
4. Deletes require `--yes` in a non-interactive shell; otherwise they block on a
prompt. Confirm intent with the user before deleting anything.
1. Resolve the workspace before mutating, and prefer an existing one.
2. Read the schema rather than guessing field names, auth fields, or body shapes.
3. `update` takes a JSON merge patch keyed by `id`: send only what changes, and
note that arrays are replaced wholesale, not merged.
4. Deletes need `--yes` in a non-interactive shell. Confirm with the user first.
5. Never write a real secret into an environment variable on the user's behalf.
Reference one (`${[ api_token ]}`) and let the user fill in the value — see
[environments.md](references/environments.md).
6. After creating requests, verify by sending one, and report the actual HTTP
status from `-v` rather than inferring success from exit code 0.
7. Requests you create are permanent user data in their app, not scratch. Name
them the way the user would, and clean up anything you created purely to test.
Reference one and let them fill in the value.
6. Verify what you built by sending it, and report the real HTTP status.
@@ -1,115 +0,0 @@
# Chaining requests
The usual way to feed one response into the next request is a shell pipeline:
send, pipe through `jq`, stash in a variable, interpolate into the next command.
Yaak does not need that. A request can reference another request's response
directly, and Yaak resolves the dependency at send time — including sending the
upstream request first if it has to.
This is the highest-leverage thing the CLI offers, because the chain is stored
in the workspace. The user can re-run it from the app, and it keeps working
after the shell session is gone.
## Reading a value out of another response
```
${[ response.body.path(request='rq_login', path='$.token') ]}
```
`request` is the upstream request's ID. `path` is JSONPath for JSON responses
and XPath for XML. So a login-then-call-the-API pair is two requests and no glue:
```bash
yaak request create wk_abc123 --json '{
"name": "Login",
"method": "POST",
"url": "${[ base_url ]}/auth/login",
"bodyType": "application/json",
"body": {"text": "{\"user\":\"demo\",\"pass\":\"${[ password ]}\"}"},
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
}'
# -> Created request: rq_login
yaak request create wk_abc123 --json '{
"name": "List Orders",
"method": "GET",
"url": "${[ base_url ]}/orders",
"authenticationType": "bearer",
"authentication": {"token": "${[ response.body.path(request='rq_login', path='$.token') ]}"}
}'
```
Sending "List Orders" now sends "Login" first when it needs to, extracts
`$.token`, and puts it in the `Authorization` header.
Note the quoting: template function arguments use **single quotes**, so inside a
single-quoted shell string write the payload to a file, or escape as above, or
switch the outer shell quoting to double quotes and escape the inner JSON.
Writing the JSON payload to a file and using `--json "$(cat payload.json)"` is
the least error-prone for anything complex.
## Controlling when the upstream request re-sends
The `behavior` argument decides whether the dependency is actually sent:
| `behavior` | Meaning |
|---|---|
| `smart` (default) | Send only if there is no stored response yet |
| `always` | Send every time |
| `ttl` | Send if the newest response is older than `ttl` seconds (`0` never expires) |
```
${[ response.body.path(request='rq_login', path='$.token', behavior='ttl', ttl='300') ]}
```
`smart` is right for a token you fetch once. `ttl` matches a real token lifetime
and is usually the best choice for auth. `always` is for values that must be
fresh on every call, like a nonce.
## Other response accessors
```
${[ response.header(request='rq_login', header='X-Request-Id') ]}
${[ response.body.raw(request='rq_login') ]}
```
`response.body.path` also accepts `behavior`/`ttl`, and has an alias of plain
`response`.
## Other useful template functions
These come from bundled plugins and work anywhere a value is rendered:
| Function | Use |
|---|---|
| `uuid.v4()`, also `v1`, `v3`, `v5`, `v6`, `v7` | Idempotency keys, unique record names |
| `timestamp.unix()`, `timestamp.unixMillis()`, `timestamp.iso8601()` | Timestamps in bodies or signatures |
| `timestamp.format(...)`, `timestamp.offset(...)` | Formatted or relative times |
| `random.range(min='1', max='100', decimals='0')` | Sample data |
| `hash.sha256(input='…', encoding='hex')` | Digests — also `md5`, `sha1`, `sha512` |
| `hmac.sha256(input='…', key='…', encoding='hex')` | Signed request signatures |
| `base64.encode(input='…')`, `base64.decode(...)` | Encoded values |
| `url.encode(input='…')`, `url.decode(...)` | Escaping values for URLs |
| `fs.readFile(path='/abs/path', trim='true')` | Pull a value from a file on disk |
| `cookie.value(name='session')` | Read a cookie from the jar |
| `1password.item(...)` | Fetch a secret from 1Password rather than storing it |
Note the shapes that are easy to misremember: the encoders are `base64.encode`
and `url.encode`, not `encode.base64`; the random function is `random.range`,
not `random.number`; and there is no `timestamp.now`. Argument names vary per
function, and a wrong name renders as an error rather than an empty string, so
check the function in the app's template editor when unsure.
## When to chain and when not to
Chain when the dependency is part of the API's real shape: log in, then call;
create a resource, then fetch it by the returned ID. The workspace becomes a
runnable description of the API, which is the point.
Do not chain to smuggle in shell logic. If a value needs a conditional, a
computation, or a retry, do that in the shell and set an environment variable.
And do not build a long chain just to run a group of requests — `yaak send
<fl_id>` already sends every request in a folder sequentially, with
`--fail-fast` to stop at the first failure and `--parallel` when order does not
matter. Reach for a chain when a request genuinely *depends* on another's
response, not merely when it should run after it.
@@ -1,122 +0,0 @@
# Environments and variables
## The hierarchy
Every workspace gets a base environment named **Global Variables** for free. Its
`parentModel` is `workspace`, and its variables apply to every send regardless of
which environment is selected.
Sub-environments (`parentModel: "environment"`) sit under the base and are chosen
per send with `-e`. A sub-environment variable overrides a base variable of the
same name; names it does not define fall through to the base.
```bash
yaak environment list wk_abc123
# ev_staging - Staging (environment)
# ev_production - Production (environment)
# ev_base - Global Variables (workspace)
```
The trailing parenthetical is `parentModel`, which is how you tell the base
environment from the rest.
## Setting variables
Variables are an array on the environment, so an update replaces the whole list.
Read first, then write the full array back:
```bash
yaak environment show ev_base
yaak environment update --json '{
"id": "ev_base",
"variables": [
{"name": "base_url", "value": "https://api.example.com", "enabled": true},
{"name": "api_version", "value": "v1", "enabled": true}
]
}'
```
Create a sub-environment with its variables in one step:
```bash
yaak environment create wk_abc123 --json '{
"name": "Staging",
"parentModel": "environment",
"variables": [{"name": "base_url", "value": "https://staging.example.com", "enabled": true}]
}'
```
`enabled: false` keeps a variable defined but inert, which is how the app models
a commented-out value.
## Using them
`${[ name ]}` resolves anywhere a value is rendered — URL, headers, body, and
authentication fields:
```bash
yaak request create wk_abc123 --json '{
"name": "List Pets",
"method": "GET",
"url": "${[ base_url ]}/${[ api_version ]}/pets",
"authenticationType": "bearer",
"authentication": {"token": "${[ api_token ]}"}
}'
```
Then run the same request against different targets:
```bash
yaak request send rq_abc123 # base environment only
yaak -e ev_staging request send rq_abc123 # staging overrides base_url
yaak -e ev_production send fl_smoke_tests # whole folder against production
```
`-e` is global, so it applies to `send`, `request send`, folder sends, and
workspace sends alike.
## Unresolved variables fail loudly
Referencing a name no active environment defines aborts before anything is sent:
```
Error: Failed to render request templates: Render Error: Variable "api_token" is not defined in active environment
```
Exit code 1. This is a feature worth relying on — a typo in a variable name can
never quietly send a request to `https:///pets`. When a send fails this way, the
fix is either the variable name in the request or the `-e` environment, not a
retry.
## Secrets
Do not write real credentials into environment variables on the user's behalf.
Create the reference and let the user supply the value:
```bash
yaak environment update --json '{"id":"ev_base","variables":[{"name":"api_token","value":"","enabled":true}]}'
```
Then tell the user which variable to fill in, and in which environment.
Environments have a `public` flag that mirrors the app's Sharable/Private toggle.
`public: false` (the default) marks the environment Private, which keeps it out
of `yaak export` unless `--include-private-environments` is passed. Secrets
belong in a private environment; values safe to commit or share belong in a
sharable one.
## Cookies
Cookie jars are per-workspace and require no setup — the oldest jar is used
automatically. Sending cookies is off by default per request; enable it with the
inherited setting, on the folder if it should apply to a whole group:
```json
"settingSendCookies": {"enabled": true, "value": true},
"settingStoreCookies": {"enabled": true, "value": true}
```
With both on, a login request stores its `Set-Cookie` and later requests in the
same jar send it back, which is often simpler than threading a token by hand.
Use `--cookie-jar cj_abc123` to select a non-default jar, and
`yaak cookie-jar list <wk_id>` to see the jars and their cookie counts.
@@ -1,71 +0,0 @@
# Importing and exporting
## Import
```bash
yaak import ./openapi.yaml
```
One command, one positional file path. The format is auto-detected — OpenAPI 3,
Swagger 2, Postman collections, Postman environments, Insomnia exports, cURL
commands, and Yaak's own export format are all supported by bundled importer
plugins. Output is a one-line summary:
```
Imported 1 workspace, 1 environment, 1 folder, 3 HTTP requests
```
By default this creates a **new workspace**. To merge into one that already
exists:
```bash
yaak import ./collection.json --workspace-id wk_abc123
```
Not every importer honours `--workspace-id`; the flag applies where the importer
supports it, and otherwise a new workspace is still created. Run
`yaak workspace list` afterwards to see what you actually got.
**Reach for this first.** When the user has an OpenAPI spec, a Postman
collection, or even a directory of `curl` commands in a README, importing beats
authoring requests by hand — it is one command, it preserves names and grouping,
and it will not typo a URL. Author requests by hand when there is no spec, or
when the user wants a small hand-picked set rather than every endpoint.
A cURL import is a fast way to turn something the user already has into a saved
request:
```bash
echo "curl -X POST https://api.example.com/pets -H 'Content-Type: application/json' -d '{\"name\":\"Rex\"}'" > /tmp/req.txt
yaak import /tmp/req.txt
```
## After importing
An imported spec gives you requests pointing at whatever `servers` the spec
declared. The usual follow-up is to make the host swappable:
1. `yaak request list <wk_id>` to see what landed.
2. Put the host in a base environment variable (`base_url`).
3. Update the imported requests to use `${[ base_url ]}`, then add a
sub-environment per deployment target.
See [environments.md](environments.md). At that point `yaak -e ev_staging send
<wk_id>` runs the whole imported API against staging.
## Export
```bash
yaak export ./backup.json # the only workspace, when there is one
yaak export ./backup.json wk_abc123 # a specific workspace
yaak export ./backup.json wk_abc wk_def # several
yaak export ./backup.json --all # everything
```
Private environments are **excluded** unless you pass
`--include-private-environments`. That default exists so an export can be
committed to a repository without leaking credentials — do not add the flag
just to make an export look complete. Add it only when the user explicitly wants
a full backup, and say so when you do.
The output is Yaak's own format, so `yaak import ./backup.json` round-trips it.
@@ -1,231 +0,0 @@
# Authoring requests
Everything here is a field on the HTTP request model. Run
`yaak request schema http --pretty` to see the full, current schema, including
the authentication variants contributed by installed plugins.
## Two ways to create
Name, method, and URL have flags. Everything else needs JSON:
```bash
yaak request create wk_abc123 --name "List Pets" --method GET --url "https://api.example.com/pets"
```
```bash
yaak request create wk_abc123 --json '{
"name": "Create Pet",
"method": "POST",
"url": "https://api.example.com/pets",
"bodyType": "application/json",
"body": {"text": "{\"name\":\"Rex\",\"species\":\"dog\"}"},
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
}'
```
Flags and JSON cannot be mixed on the same command. The workspace ID may be
omitted when only one workspace exists, and may also be carried inside the
payload as `workspaceId`.
## Bodies
`bodyType` decides how `body` is encoded onto the wire, and `body` is a
free-form object whose shape depends on that type. For text-ish types the
content lives in `body.text` as a **string**, so a JSON payload is
double-encoded: a JSON string containing JSON.
| `bodyType` | `body` |
|---|---|
| `application/json` | `{"text": "{\"key\":\"value\"}"}` |
| `text/xml` | `{"text": "<root/>"}` |
| `other` | `{"text": "…"}` |
| `graphql` | `{"query": "{ pets { id } }", "variables": "{\"n\":1}", "operationName": ""}` |
| `application/x-www-form-urlencoded` | `{"form": [{"name":"a","value":"1","enabled":true}]}` |
| `multipart/form-data` | `{"form": [{"name":"file","file":"/abs/path","enabled":true}]}` |
| `binary` | `{"filePath": "/abs/path"}` |
| `null` (omitted) | no body |
For `graphql`, note that `variables` is a **string** of JSON, not an object, and
that a GraphQL request sent with method `GET` moves query/variables/operationName
into the query string and sends no body at all.
For `multipart/form-data`, each entry is either a text field (`value`) or a file
(`file`, an absolute path), and may carry its own `contentType`.
**Add the `Content-Type` header yourself.** In the desktop app, choosing a body
type also writes a matching `Content-Type` into the request's headers, so it is
stored on the request rather than inferred at send time. Creating a request from
the CLI skips that step: `bodyType` alone controls how the body is *encoded*, and
nothing adds the header. A JSON body with no `Content-Type` goes out as untyped
bytes, which many APIs answer with 400 or 415.
```json
"bodyType": "application/json",
"body": {"text": "{\"name\":\"Rex\"}"},
"headers": [{"name": "Content-Type", "value": "application/json", "enabled": true}]
```
Use the same value as `bodyType`, with two exceptions the app also makes: `other`
pairs with `text/plain`, and `graphql` pairs with `application/json`. Multipart is
the one case to leave alone — the sender replaces that header with one carrying
the generated boundary.
Requests created this way end up identical to app-created ones, which matters
because the user will open them in the app afterwards.
## Headers
```json
"headers": [
{"name": "Accept", "value": "application/json", "enabled": true},
{"name": "X-Debug", "value": "1", "enabled": false}
]
```
`enabled: false` keeps a header in the app for the user to toggle without
sending it. Values accept template variables.
## URL parameters
One array covers both query string entries and path placeholders. A parameter
fills a path placeholder only when its **name starts with a colon** and matches
the placeholder in the URL. Everything else becomes a query string entry:
```json
"url": "https://api.example.com/pets/:petId/visits",
"urlParameters": [
{"name": ":petId", "value": "42", "enabled": true},
{"name": "limit", "value": "10", "enabled": true}
]
```
That sends `https://api.example.com/pets/42/visits?limit=10``:petId` is
substituted into the path and dropped from the query string, `limit` is not.
This is the single easiest thing to get wrong here, and it fails **silently**.
Naming the parameter `petId` instead of `:petId` leaves `/pets/:petId/visits` in
the path as literal text and appends `?petId=42`, which most servers answer with
a 404. Always include the colon, and confirm with `yaak -v request send <id>`
that the `> GET …` line shows a substituted path.
## Authentication
`authenticationType` names a strategy and `authentication` holds its values. The
strategy names are not always what you would guess — the built-ins are `basic`,
`bearer`, `apikey`, `jwt`, `oauth1`, `oauth2`, `awsv4` (not "aws"), and
`windows` (not "ntlm"). Installed plugins can add more.
```json
"authenticationType": "bearer",
"authentication": {"token": "${[ api_token ]}"}
```
```json
"authenticationType": "basic",
"authentication": {"username": "admin", "password": "${[ admin_password ]}"}
```
```json
"authenticationType": "apikey",
"authentication": {"location": "header", "key": "X-Api-Key", "value": "${[ api_key ]}"}
```
OAuth 2.0 is the one to look up rather than attempt from memory. It has fifteen
fields, six of them required, and `grantType` is an enum:
```json
"authenticationType": "oauth2",
"authentication": {
"grantType": "client_credentials",
"clientId": "${[ client_id ]}",
"clientSecret": "${[ client_secret ]}",
"accessTokenUrl": "https://auth.example.com/oauth/token",
"scope": "read:pets",
"credentials": "body",
"tokenName": "access_token",
"headerName": "Authorization",
"usePkce": false,
"useExternalBrowser": false
}
```
`grantType` accepts `authorization_code`, `implicit`, `password`, or
`client_credentials`, and which other fields matter depends on which you pick:
`authorization_code` also wants `authorizationUrl` and `redirectUri`, while
`client_credentials` does not.
The exact fields for every strategy, and which are required, come from the
schema, which enumerates each installed strategy as a named variant under
`authentication`:
```bash
# display name plus the value to use for authenticationType
yaak request schema http | jq -r '.properties.authentication.oneOf[]
| select(.title) | "\(.title): \(.description)"'
# the full shape of one strategy
yaak request schema http | jq '.properties.authentication.oneOf[]
| select(.title == "OAuth 2.0")'
```
Because the list is built by loading plugins, it covers plugin-contributed
strategies too, not just the built-ins. Read it rather than guessing.
Set `authenticationType` to `null` to send no auth and stop inheriting from the
parent folder.
## Folders and inheritance
Folders are containers *and* a place to put shared configuration. Headers and
authentication set on a folder apply to every request inside it, so the common
pattern is one folder per API surface holding the auth:
```bash
yaak folder create wk_abc123 --name "Admin API"
yaak folder update --json '{
"id": "fl_abc123",
"authenticationType": "bearer",
"authentication": {"token": "${[ admin_token ]}"},
"headers": [{"name": "X-Api-Version", "value": "2024-01-01", "enabled": true}]
}'
yaak request create wk_abc123 --json '{"name":"List Users","method":"GET","url":"${[ base_url ]}/users","folderId":"fl_abc123"}'
```
The request above sends both the folder's bearer token and its version header
without repeating either. A request that sets its own `authenticationType`
overrides the folder's.
Nest folders by setting a folder's `folderId`. `yaak send <fl_id>` sends every
request in the folder recursively.
## Per-request settings
Each `setting*` field is an inherited toggle shaped
`{"enabled": bool, "value": …}`, where `enabled` means "override the inherited
value" rather than "turn the feature on":
```json
"settingFollowRedirects": {"enabled": true, "value": false},
"settingRequestTimeout": {"enabled": true, "value": 5000}
```
Available: `settingFollowRedirects`, `settingRequestTimeout` (ms, `0` for none),
`settingValidateCertificates`, `settingSendCookies`, `settingStoreCookies`.
## Updating
Updates are JSON merge patches keyed by `id`. Send only what changes:
```bash
yaak request update --json '{"id":"rq_abc123","method":"PATCH"}'
```
Arrays are replaced wholesale, not merged — to add one header, read the current
list with `yaak request show rq_abc123` and write the full new array back.
Setting a key to `null` removes it.
## Ordering
`sortPriority` (a float) controls display order in the app sidebar. Leave it at
`0` unless the user cares; requests created with the same priority fall back to
creation order.
+37
View File
@@ -86,6 +86,36 @@ pub enum Commands {
/// Environment commands
Environment(EnvironmentArgs),
/// Template function commands
#[command(alias = "func")]
TemplateFunction(TemplateFunctionArgs),
}
#[derive(Args)]
pub struct TemplateFunctionArgs {
#[command(subcommand)]
pub command: TemplateFunctionCommands,
}
#[derive(Subcommand)]
pub enum TemplateFunctionCommands {
/// List template functions provided by installed plugins
List {
/// Only show functions whose name contains this text
#[arg(value_name = "FILTER")]
filter: Option<String>,
},
/// Show a template function's arguments as JSON
Show {
/// Template function name (for example: response.body.path)
name: String,
/// Pretty-print JSON output
#[arg(long)]
pretty: bool,
},
}
#[derive(Args)]
@@ -361,6 +391,13 @@ pub enum FolderCommands {
workspace_id: Option<String>,
},
/// Output JSON schema for folder create/update payloads
Schema {
/// Pretty-print schema JSON output
#[arg(long)]
pretty: bool,
},
/// Show a folder as JSON
Show {
/// Folder ID
@@ -5,7 +5,9 @@ use crate::utils::json::{
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
parse_required_json, require_id, validate_create_id,
};
use crate::utils::schema::append_agent_hints;
use crate::utils::workspace::resolve_workspace_id;
use schemars::schema_for;
use yaak_models::models::Folder;
use yaak_models::util::UpdateSource;
@@ -14,6 +16,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
let result = match args.command {
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
FolderCommands::Schema { pretty } => schema(pretty),
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
FolderCommands::Create { workspace_id, name, json } => {
create(ctx, workspace_id, name, json)
@@ -31,6 +34,18 @@ pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
}
}
fn schema(pretty: bool) -> CommandResult {
let mut schema = serde_json::to_value(schema_for!(Folder))
.map_err(|e| format!("Failed to serialize folder schema: {e}"))?;
append_agent_hints(&mut schema);
let output =
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
.map_err(|e| format!("Failed to format folder schema JSON: {e}"))?;
println!("{output}");
Ok(())
}
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
let folders =
+1
View File
@@ -7,4 +7,5 @@ pub mod import_export;
pub mod plugin;
pub mod request;
pub mod send;
pub mod template_function;
pub mod workspace;
@@ -122,6 +122,20 @@ fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names that keep the leading `:` (for example `:id`, `:commentId`). A name without the `:` is sent as a query string parameter instead, leaving the placeholder in the path.",
);
}
if let Some(body_type_schema) = properties.get_mut("bodyType").and_then(Value::as_object_mut) {
append_description(
body_type_schema,
"Known values: `application/json`, `text/xml`, `application/x-www-form-urlencoded`, `multipart/form-data`, `graphql`, `binary`, `other`, or null for no body. This selects how `body` is encoded; it does NOT add a `Content-Type` header. Add that header yourself, matching the body type (`other` pairs with `text/plain` and `graphql` with `application/json`). Multipart is the exception: its header is generated at send time to carry the boundary.",
);
}
if let Some(body_schema) = properties.get_mut("body").and_then(Value::as_object_mut) {
append_description(
body_schema,
"Shape depends on `bodyType`. Text-ish types (`application/json`, `text/xml`, `other`) use `{\"text\": \"...\"}` where the value is a string, so JSON bodies are a JSON string containing JSON. Form types use `{\"form\": [{\"name\": \"a\", \"value\": \"1\", \"enabled\": true}]}`, and a multipart entry may use `file` (an absolute path) and `contentType` instead of `value`. `binary` uses `{\"filePath\": \"/abs/path\"}`. `graphql` uses `{\"query\": \"...\", \"variables\": \"{}\", \"operationName\": \"\"}` where `variables` is a string of JSON.",
);
}
}
fn append_description(schema: &mut Map<String, Value>, extra: &str) {
@@ -0,0 +1,111 @@
use crate::cli::{TemplateFunctionArgs, TemplateFunctionCommands};
use crate::context::CliContext;
use yaak_plugins::events::{PluginContext, TemplateFunction};
type CommandResult<T = ()> = std::result::Result<T, String>;
pub async fn run(ctx: &CliContext, args: TemplateFunctionArgs) -> i32 {
let result = match args.command {
TemplateFunctionCommands::List { filter } => list(ctx, filter.as_deref()).await,
TemplateFunctionCommands::Show { name, pretty } => show(ctx, &name, pretty).await,
};
match result {
Ok(()) => 0,
Err(error) => {
eprintln!("Error: {error}");
1
}
}
}
/// Template functions come from plugins, so the only accurate list is the one the
/// installed plugins report right now.
async fn all(ctx: &CliContext) -> CommandResult<Vec<TemplateFunction>> {
let plugin_context = PluginContext::new_empty();
let summaries = ctx
.plugin_manager()
.get_template_function_summaries(&plugin_context)
.await
.map_err(|e| format!("Failed to list template functions: {e}"))?;
let mut functions: Vec<TemplateFunction> =
summaries.into_iter().flat_map(|summary| summary.functions).collect();
functions.sort_by(|a, b| a.name.cmp(&b.name));
Ok(functions)
}
async fn list(ctx: &CliContext, filter: Option<&str>) -> CommandResult {
let mut functions = all(ctx).await?;
if let Some(filter) = filter {
let needle = filter.to_lowercase();
functions.retain(|f| f.name.to_lowercase().contains(&needle));
}
if functions.is_empty() {
match filter {
Some(filter) => println!("No template functions matching '{filter}'"),
None => println!("No template functions found"),
}
return Ok(());
}
for function in functions {
let args = function.args.iter().filter_map(arg_name).collect::<Vec<_>>().join(", ");
match function.description {
Some(description) if !description.is_empty() => {
println!("{}({}) - {}", function.name, args, description)
}
_ => println!("{}({})", function.name, args),
}
}
Ok(())
}
async fn show(ctx: &CliContext, name: &str, pretty: bool) -> CommandResult {
let functions = all(ctx).await?;
let function = functions
.iter()
.find(|f| {
f.name == name
|| f.aliases.as_ref().is_some_and(|aliases| aliases.iter().any(|a| a == name))
})
.ok_or_else(|| {
let names = functions.iter().map(|f| f.name.as_str()).collect::<Vec<_>>();
format!("No template function named '{name}'. Available: {}", names.join(", "))
})?;
let output = if pretty {
serde_json::to_string_pretty(function)
} else {
serde_json::to_string(function)
}
.map_err(|e| format!("Failed to serialize template function: {e}"))?;
println!("{output}");
Ok(())
}
fn arg_name(arg: &yaak_plugins::events::TemplateFunctionArg) -> Option<String> {
use yaak_plugins::events::{FormInput, TemplateFunctionArg};
let TemplateFunctionArg::FormInput(input) = arg;
let base = match input {
FormInput::Text(v) => &v.base,
FormInput::Editor(v) => &v.base,
FormInput::Select(v) => &v.base,
FormInput::Checkbox(v) => &v.base,
FormInput::File(v) => &v.base,
FormInput::HttpRequest(v) => &v.base,
FormInput::KeyValue(v) => &v.base,
// Layout-only inputs have no value of their own
FormInput::Accordion(_)
| FormInput::HStack(_)
| FormInput::Banner(_)
| FormInput::Markdown(_) => return None,
};
if base.name.trim().is_empty() { None } else { Some(base.name.clone()) }
}
+7
View File
@@ -37,6 +37,13 @@ async fn main() {
let exit_code = match command {
Commands::Agent(args) => commands::agent::run(args),
Commands::TemplateFunction(args) => {
let mut context = CliContext::new(data_dir.clone(), app_id);
context.init_plugins(CliExecutionContext::default()).await;
let exit_code = commands::template_function::run(&context, args).await;
context.shutdown().await;
exit_code
}
Commands::Auth(args) => commands::auth::run(args).await,
Commands::Import(args) => {
let mut context = CliContext::new(data_dir.clone(), app_id);
+1 -1
View File
@@ -949,7 +949,7 @@ pub struct ParentHeaders {
pub headers: Vec<HttpRequestHeader>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
#[enum_def(table_name = "folders")]