diff --git a/crates-cli/yaak-cli/skills/use-yaak/SKILL.md b/crates-cli/yaak-cli/skills/use-yaak/SKILL.md index 2fa8b830..d36bf11f 100644 --- a/crates-cli/yaak-cli/skills/use-yaak/SKILL.md +++ b/crates-cli/yaak-cli/skills/use-yaak/SKILL.md @@ -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 --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 # 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 ` 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 ` 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 ` | -| Create | `yaak {workspace,folder,request,environment} create` | -| Update | `yaak {workspace,folder,request,environment} update --json '{"id":"…",…}'` | -| Delete | `yaak … delete --yes` | -| Inspect the model | `yaak request schema http --pretty`, `yaak workspace schema`, `yaak environment schema` | -| Send one request | `yaak request send ` | -| Send a folder or whole workspace | `yaak send ` | -| Import an existing API | `yaak import ` | -| Export | `yaak export [workspace_id…]` | - -Global flags go anywhere but are clearest before the subcommand: -`-e/--environment `, `--cookie-jar `, `-v/--verbose`, -`--data-dir ` (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 -` 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 ` 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. diff --git a/crates-cli/yaak-cli/skills/use-yaak/references/chaining.md b/crates-cli/yaak-cli/skills/use-yaak/references/chaining.md deleted file mode 100644 index 98241750..00000000 --- a/crates-cli/yaak-cli/skills/use-yaak/references/chaining.md +++ /dev/null @@ -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 -` 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. diff --git a/crates-cli/yaak-cli/skills/use-yaak/references/environments.md b/crates-cli/yaak-cli/skills/use-yaak/references/environments.md deleted file mode 100644 index 971bab27..00000000 --- a/crates-cli/yaak-cli/skills/use-yaak/references/environments.md +++ /dev/null @@ -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 ` to see the jars and their cookie counts. diff --git a/crates-cli/yaak-cli/skills/use-yaak/references/import-export.md b/crates-cli/yaak-cli/skills/use-yaak/references/import-export.md deleted file mode 100644 index 4399f6cd..00000000 --- a/crates-cli/yaak-cli/skills/use-yaak/references/import-export.md +++ /dev/null @@ -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 ` 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 -` 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. diff --git a/crates-cli/yaak-cli/skills/use-yaak/references/requests.md b/crates-cli/yaak-cli/skills/use-yaak/references/requests.md deleted file mode 100644 index 54d34426..00000000 --- a/crates-cli/yaak-cli/skills/use-yaak/references/requests.md +++ /dev/null @@ -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": ""}` | -| `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 ` -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 ` 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. diff --git a/crates-cli/yaak-cli/src/cli.rs b/crates-cli/yaak-cli/src/cli.rs index 27a50f85..c80b34b9 100644 --- a/crates-cli/yaak-cli/src/cli.rs +++ b/crates-cli/yaak-cli/src/cli.rs @@ -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, + }, + + /// 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, }, + /// 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 diff --git a/crates-cli/yaak-cli/src/commands/folder.rs b/crates-cli/yaak-cli/src/commands/folder.rs index 926c76b8..71eab72c 100644 --- a/crates-cli/yaak-cli/src/commands/folder.rs +++ b/crates-cli/yaak-cli/src/commands/folder.rs @@ -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 = std::result::Result; 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 = diff --git a/crates-cli/yaak-cli/src/commands/mod.rs b/crates-cli/yaak-cli/src/commands/mod.rs index aad62221..65ed8f57 100644 --- a/crates-cli/yaak-cli/src/commands/mod.rs +++ b/crates-cli/yaak-cli/src/commands/mod.rs @@ -7,4 +7,5 @@ pub mod import_export; pub mod plugin; pub mod request; pub mod send; +pub mod template_function; pub mod workspace; diff --git a/crates-cli/yaak-cli/src/commands/request.rs b/crates-cli/yaak-cli/src/commands/request.rs index 4fc2b9d2..57a98aaa 100644 --- a/crates-cli/yaak-cli/src/commands/request.rs +++ b/crates-cli/yaak-cli/src/commands/request.rs @@ -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, extra: &str) { diff --git a/crates-cli/yaak-cli/src/commands/template_function.rs b/crates-cli/yaak-cli/src/commands/template_function.rs new file mode 100644 index 00000000..c2666e1a --- /dev/null +++ b/crates-cli/yaak-cli/src/commands/template_function.rs @@ -0,0 +1,111 @@ +use crate::cli::{TemplateFunctionArgs, TemplateFunctionCommands}; +use crate::context::CliContext; +use yaak_plugins::events::{PluginContext, TemplateFunction}; + +type CommandResult = std::result::Result; + +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> { + 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 = + 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::>().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::>(); + 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 { + 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()) } +} diff --git a/crates-cli/yaak-cli/src/main.rs b/crates-cli/yaak-cli/src/main.rs index 05d7a712..133d7bf9 100644 --- a/crates-cli/yaak-cli/src/main.rs +++ b/crates-cli/yaak-cli/src/main.rs @@ -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); diff --git a/crates/yaak-models/src/models.rs b/crates/yaak-models/src/models.rs index 2148650e..74a02020 100644 --- a/crates/yaak-models/src/models.rs +++ b/crates/yaak-models/src/models.rs @@ -949,7 +949,7 @@ pub struct ParentHeaders { pub headers: Vec, } -#[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")]