mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-24 04:13:59 +02:00
Add a Yaak CLI skill for coding agents, installed by yaak agent install
Teaches agents to drive the CLI: workspaces, environments, requests, sending, response chaining, and importing. SKILL.md stays lean with four references loaded on demand. The skill is embedded in the binary and written to ~/.agents/skills plus any detected tool directory, so it ships with the CLI and refreshes on update. Reinstalls keep files edited locally unless --force. Also fixes the `request schema http` hint for URL path parameters, which said to omit the leading colon. Names without the colon are sent as query string parameters instead, leaving the placeholder literal in the path.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
---
|
||||
name: use-yaak
|
||||
description: >
|
||||
Build and run HTTP API requests with the Yaak CLI (`yaak`): create workspaces,
|
||||
folders, environments and variables, author HTTP requests, send them
|
||||
individually or a whole folder/workspace at once, chain one request's response
|
||||
into the next, and import existing APIs from OpenAPI, Postman, Insomnia, or
|
||||
cURL. Use this skill whenever the user mentions Yaak, a Yaak workspace, or the
|
||||
`yaak` command, and also when they ask to try, hit, 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 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.
|
||||
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
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
## Preflight
|
||||
|
||||
```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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
npm install -g @yaakapp/cli@latest && yaak agent install
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
${[ base_url ]}/pets/${[ pet_id ]}
|
||||
${[ response.body.path(request='rq_abc123', 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.
|
||||
|
||||
## Sending
|
||||
|
||||
```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 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:
|
||||
|
||||
```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) |
|
||||
|
||||
## 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.
|
||||
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.
|
||||
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,122 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,195 @@
|
||||
# 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 ]}"}
|
||||
```
|
||||
|
||||
The exact fields for each strategy, and which are required, are in the
|
||||
`authentication` property of `yaak request schema http --pretty`, which
|
||||
enumerates every installed strategy as a named variant. Read it rather than
|
||||
guessing — `oauth2` has fifteen fields and `jwt` requires four.
|
||||
|
||||
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.
|
||||
@@ -11,6 +11,7 @@ use std::path::PathBuf;
|
||||
- Template function syntax is ${[ namespace.my_func(a='aaa',b='bbb') ]}
|
||||
- View JSONSchema for models before creating or updating (eg. `yaak request schema http`)
|
||||
- Deletion requires confirmation (--yes for non-interactive environments)
|
||||
- Run `yaak agent install` to install the Yaak skill for AI coding agents
|
||||
"#)]
|
||||
pub struct Cli {
|
||||
/// Use a custom data directory
|
||||
@@ -39,6 +40,9 @@ pub struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Install Yaak skills for AI coding agents
|
||||
Agent(AgentArgs),
|
||||
|
||||
/// Authentication commands
|
||||
Auth(AuthArgs),
|
||||
|
||||
@@ -84,6 +88,35 @@ pub enum Commands {
|
||||
Environment(EnvironmentArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct AgentArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: AgentCommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum AgentCommands {
|
||||
/// Install the Yaak skill so coding agents know how to drive the CLI
|
||||
#[command(alias = "update", alias = "add")]
|
||||
Install {
|
||||
/// Overwrite skill files you have edited locally
|
||||
#[arg(long)]
|
||||
force: bool,
|
||||
|
||||
/// Install for specific agents instead of all detected ones
|
||||
#[arg(long = "agent", value_name = "AGENT")]
|
||||
agent: Option<Vec<String>>,
|
||||
},
|
||||
|
||||
/// Remove the Yaak skill
|
||||
#[command(alias = "uninstall", alias = "rm")]
|
||||
Remove {
|
||||
/// Remove for specific agents instead of all detected ones
|
||||
#[arg(long = "agent", value_name = "AGENT")]
|
||||
agent: Option<Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct SendArgs {
|
||||
/// Request, folder, or workspace ID
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
use crate::cli::{AgentArgs, AgentCommands};
|
||||
use crate::ui;
|
||||
use crate::version;
|
||||
use include_dir::{Dir, include_dir};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
static SKILL_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skills/use-yaak");
|
||||
|
||||
const SKILL_NAME: &str = "use-yaak";
|
||||
const MANIFEST_NAME: &str = ".yaak-skill.json";
|
||||
|
||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
/// Records what this CLI wrote, so a later install can tell its own output apart
|
||||
/// from edits the user made by hand.
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SkillManifest {
|
||||
cli_version: String,
|
||||
/// Relative file path -> SHA-256 of the contents this CLI wrote.
|
||||
files: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// A coding tool that reads skills from a directory in the user's home.
|
||||
struct Target {
|
||||
/// Display name used in output.
|
||||
label: &'static str,
|
||||
/// Directory holding all skills for this tool (`…/skills`).
|
||||
skills_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn run(args: AgentArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
AgentCommands::Install { force, agent } => install(force, agent),
|
||||
AgentCommands::Remove { agent } => remove(agent),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
ui::error(&error);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install(force: bool, agent: Option<Vec<String>>) -> CommandResult {
|
||||
let targets = resolve_targets(agent)?;
|
||||
|
||||
let mut installed = 0usize;
|
||||
for target in &targets {
|
||||
let dir = target.skills_dir.join(SKILL_NAME);
|
||||
match write_skill(&dir, force) {
|
||||
Ok(WriteOutcome::Written { skipped }) => {
|
||||
installed += 1;
|
||||
ui::success(&format!("{} -> {}", target.label, dir.display()));
|
||||
for path in skipped {
|
||||
ui::warning(&format!(" kept your edited {path} (use --force to overwrite)"));
|
||||
}
|
||||
}
|
||||
Err(error) => ui::warning_stderr(&format!("{}: {}", target.label, error)),
|
||||
}
|
||||
}
|
||||
|
||||
if installed == 0 {
|
||||
return Err("Failed to install the Yaak skill anywhere".to_string());
|
||||
}
|
||||
|
||||
ui::info("Restart your coding tool to pick up the skill");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove(agent: Option<Vec<String>>) -> CommandResult {
|
||||
let targets = resolve_targets(agent)?;
|
||||
|
||||
let mut removed = 0usize;
|
||||
for target in &targets {
|
||||
let dir = target.skills_dir.join(SKILL_NAME);
|
||||
if !dir.exists() {
|
||||
continue;
|
||||
}
|
||||
match fs::remove_dir_all(&dir) {
|
||||
Ok(()) => {
|
||||
removed += 1;
|
||||
ui::success(&format!("Removed {}", dir.display()));
|
||||
}
|
||||
Err(error) => {
|
||||
ui::warning_stderr(&format!("Failed to remove {}: {error}", dir.display()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removed == 0 {
|
||||
ui::info("No Yaak skill was installed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum WriteOutcome {
|
||||
Written { skipped: Vec<String> },
|
||||
}
|
||||
|
||||
fn write_skill(dir: &Path, force: bool) -> CommandResult<WriteOutcome> {
|
||||
let previous = read_manifest(dir);
|
||||
let mut manifest =
|
||||
SkillManifest { cli_version: version::cli_version().to_string(), ..Default::default() };
|
||||
let mut skipped = Vec::new();
|
||||
|
||||
for file in walk(&SKILL_DIR) {
|
||||
let relative = file.path().to_string_lossy().to_string();
|
||||
let destination = dir.join(file.path());
|
||||
let contents = file.contents();
|
||||
let digest = sha256(contents);
|
||||
|
||||
// Leave a file alone when the user has changed it since we wrote it.
|
||||
if !force
|
||||
&& destination.exists()
|
||||
&& let Ok(on_disk) = fs::read(&destination)
|
||||
&& let Some(written) = previous.files.get(&relative)
|
||||
&& sha256(&on_disk) != *written
|
||||
{
|
||||
skipped.push(relative.clone());
|
||||
manifest.files.insert(relative, written.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = destination.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("Failed to create {}: {e}", parent.display()))?;
|
||||
}
|
||||
fs::write(&destination, contents)
|
||||
.map_err(|e| format!("Failed to write {}: {e}", destination.display()))?;
|
||||
manifest.files.insert(relative, digest);
|
||||
}
|
||||
|
||||
let manifest_json = serde_json::to_string_pretty(&manifest)
|
||||
.map_err(|e| format!("Failed to serialize skill manifest: {e}"))?;
|
||||
fs::write(dir.join(MANIFEST_NAME), manifest_json)
|
||||
.map_err(|e| format!("Failed to write skill manifest: {e}"))?;
|
||||
|
||||
Ok(WriteOutcome::Written { skipped })
|
||||
}
|
||||
|
||||
fn read_manifest(dir: &Path) -> SkillManifest {
|
||||
fs::read_to_string(dir.join(MANIFEST_NAME))
|
||||
.ok()
|
||||
.and_then(|raw| serde_json::from_str(&raw).ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn sha256(bytes: &[u8]) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(bytes);
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
/// Flatten the embedded skill directory into its files, recursing into subdirectories.
|
||||
fn walk<'a>(dir: &'a Dir<'a>) -> Vec<&'a include_dir::File<'a>> {
|
||||
let mut files: Vec<_> = dir.files().collect();
|
||||
for child in dir.dirs() {
|
||||
files.extend(walk(child));
|
||||
}
|
||||
files
|
||||
}
|
||||
|
||||
/// `~/.agents/skills` is the cross-tool location and is always written. Tool-specific
|
||||
/// directories are written only when that tool is already set up on this machine, so
|
||||
/// installing never creates a config directory for a tool the user does not use.
|
||||
fn resolve_targets(requested: Option<Vec<String>>) -> CommandResult<Vec<Target>> {
|
||||
let home = dirs::home_dir().ok_or("Could not determine home directory")?;
|
||||
|
||||
let known: Vec<(&str, PathBuf, PathBuf)> = vec![
|
||||
("agents", home.join(".agents"), home.join(".agents").join("skills")),
|
||||
("claude-code", home.join(".claude"), home.join(".claude").join("skills")),
|
||||
("cursor", home.join(".cursor"), home.join(".cursor").join("skills")),
|
||||
("codex", home.join(".codex"), home.join(".codex").join("skills")),
|
||||
("opencode", home.join(".opencode"), home.join(".opencode").join("skills")),
|
||||
];
|
||||
|
||||
if let Some(requested) = requested {
|
||||
let mut targets = Vec::new();
|
||||
for name in requested {
|
||||
let found =
|
||||
known.iter().find(|(label, _, _)| *label == name.as_str()).ok_or_else(|| {
|
||||
let names: Vec<_> = known.iter().map(|(l, _, _)| *l).collect();
|
||||
format!("Unknown agent '{name}'. Known agents: {}", names.join(", "))
|
||||
})?;
|
||||
targets.push(Target { label: found.0, skills_dir: found.2.clone() });
|
||||
}
|
||||
return Ok(targets);
|
||||
}
|
||||
|
||||
let targets: Vec<Target> = known
|
||||
.into_iter()
|
||||
.filter(|(label, marker, _)| *label == "agents" || marker.exists())
|
||||
.map(|(label, _, skills_dir)| Target { label, skills_dir })
|
||||
.collect();
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod agent;
|
||||
pub mod auth;
|
||||
pub mod cookie_jar;
|
||||
pub mod environment;
|
||||
|
||||
@@ -119,7 +119,7 @@ fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
||||
if let Some(url_schema) = properties.get_mut("url").and_then(Value::as_object_mut) {
|
||||
append_description(
|
||||
url_schema,
|
||||
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names without `:` (for example `id`, `commentId`).",
|
||||
"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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ async fn main() {
|
||||
version_check::maybe_check_for_updates().await;
|
||||
|
||||
let exit_code = match command {
|
||||
Commands::Agent(args) => commands::agent::run(args),
|
||||
Commands::Auth(args) => commands::auth::run(args).await,
|
||||
Commands::Import(args) => {
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
|
||||
Reference in New Issue
Block a user