Skip to main content

CLI

@naturali/cli gives every API operation a command, generated from the OpenAPI specs. An endpoint that exists in the contract is reachable from the shell; one that doesn't, isn't.

pnpm add -g @naturali/cli
export NATURALI_TOKEN=nat_sk_...
naturali list-agents --project-id proj_V1StGXR8Z5jdHi6B

The CLI always talks to https://api.naturali.ai — the only naturali.ai API origin, and not configurable.

Configuration​

Credentials and the default project both come from the environment:

VariablePurpose
NATURALI_TOKENBearer credential — a nat_sk_… API key or a session JWT. Required.
NATURALI_PROJECTDefault --project-id for any command that takes one, so you don't have to repeat it. An explicit --project-id still wins.
export NATURALI_TOKEN=nat_sk_...
export NATURALI_PROJECT=proj_V1StGXR8Z5jdHi6B

naturali list-agents

Discovering commands​

A command is its operationId in kebab-case — createAgent → create-agent.

naturali list-commands # every command, with its description
naturali create-agent --help # a command's flags, grouped, plus its docs link

--help lists each flag with its type, whether it is required, and whether the value lands in the URL path, the query string, or the request body — read straight from the spec.

Flags​

Flag names are the spec's parameter names. Kebab-case is the convention used throughout these docs, and snake_case and camelCase are accepted too: --project-id, --project_id and --projectId are one flag.

naturali list-agents --project-id proj_V1StGXR8Z5jdHi6B --limit 10

naturali create-agent \
--project-id proj_V1StGXR8Z5jdHi6B \
--ai-provider-id aip_V1StGXR8Z5jdHi6B \
--name support-triage \
--tool-bindings '[{"tool_id":"tool_V1StGXR8Z5jdHi6B"}]'
Value kindHow to pass it
String--name support-triage
Number--temperature 0.7
Boolean--force — a bare flag means true
Object--tool-choice '{"type":"tool","tool_name":"get_weather"}'
Arrayrepeat the flag (--capabilities a --capabilities b) or pass JSON (--tool-bindings '[{"tool_id":"tool_a"}]')
From a file--content @playbook.md — @@ sends a literal leading @
Whole body--tags '{"team":"finance"}' on replace-*-tags and merge-*-tags, whose body is the tag map itself; no other body flag may accompany it

An array-typed field always reaches the API as a list, even with a single value.

Values from a file​

@path reads the value from disk, for the fields a shell makes awkward: a document's text, an agent's instructions, a whole formation template.

A .yaml, .yml or .json file is parsed, and a { "file": "..." } inside it is replaced by that file's contents, resolved relative to the document holding the reference. A formation template can therefore declare documents whose text is versioned beside it, instead of carrying a pasted copy of each:

squad.formation.yaml
resources:
Playbook:
type: document
properties:
path: /squad/playbook.md
content:
file: ../intelligence/playbook.md
naturali validate-formation \
--project-id proj_V1StGXR8Z5jdHi6B \
--template @squad.formation.yaml

Any other extension is read as text and never parsed, so a markdown file that opens with { stays a string. Only a lone { "file": ... } is a reference — an object with other keys beside it is left alone, so a property genuinely named file keeps its value.

A missing or unreadable file stops the command and names both the flag and the path; nothing is sent.

caution

A template can read any file the person running the CLI can read. Read a template you did not write before deploying it.

Every path parameter a command needs must be passed explicitly — a command that leaves one out stops and names it rather than guessing:

$ naturali get-agent --agent-id agent_V1StGXR8Z5jdHi6B
Missing required path parameter(s): project_id.
Provide with --project-id <value>.

Output​

The response body goes to stdout as formatted JSON, ready for jq:

naturali list-agents --project-id proj_V1StGXR8Z5jdHi6B | jq -r '.data[].id'

A non-2xx response prints the error envelope to stderr and exits 1, so a failing command fails a shell script:

{
"status": 403,
"error": { "code": "access_denied", "message": "Not your project" }
}

Scripting example​

Create an agent, open a session, and ask it something:

set -euo pipefail
export NATURALI_TOKEN=nat_sk_...

PROJECT_ID=proj_V1StGXR8Z5jdHi6B

AGENT_ID=$(naturali create-agent \
--project-id "$PROJECT_ID" \
--ai-provider-id aip_V1StGXR8Z5jdHi6B \
--name support-triage | jq -r '.id')

SESSION_ID=$(naturali create-session \
--project-id "$PROJECT_ID" \
--agent-id "$AGENT_ID" | jq -r '.id')

naturali add-session-message \
--project-id "$PROJECT_ID" \
--session-id "$SESSION_ID" \
--message 'What is our refund window?'

Versioning​

@naturali/cli shares its version with the API and with @naturali/sdk — one number, released together. The CLI is generated from the API's OpenAPI specs and bundles the SDK at build time, so @naturali/cli@0.28.0 speaks to API 0.28.0 and pulls in no separate client at runtime.

A consequence worth knowing: a bump can mean the API gained a command, not that anything in the CLI's own behaviour changed. naturali --help is generated from the specs the release was built from, so it is always the authority on what the version you installed can do.