> For the complete user guide index, see [llms.txt](https://portraitresearch.com/documentation/llms.txt).

# CLI command and output reference

[Canonical page](https://portraitresearch.com/documentation/integrations/portrait-cli-reference/)

## Applicability

- **Task:** Implement a reliable Portrait CLI client
- **Access:** Portrait account and OAuth for remote commands; full Easel access and a provisioned Easel CLI bridge for ask and sessions
- **Audience:** Developers and AI agents using the Portrait CLI
- **Surfaces:** cli
- **Availability:** Production; command and account-specific access
- **Content type:** reference
- **Last reviewed:** 2026-09-08

This reference describes the command contract and `schemaVersion: 1` output of Portrait CLI 0.1.9. Check `portrait --version` and `portrait --help` against your installed package before building an integration. Follow [Install and use the CLI](https://portraitresearch.com/documentation/integrations/portrait-cli/) for setup and account requirements.

## Commands and effects

| Command | Effect |
|---|---|
| `auth login`, `auth status`, `auth logout` | Connect, check sign-in state, or remove this installation's connection |
| `sessions list`, `sessions history` | Read accessible conversations or a retained transcript |
| `sessions create` | Create an empty session; `--resume` resumes the specified conversation |
| `ask` | Send a new research turn; it can propose or execute supported work subject to its confirmation and usage rules |
| `files upload` | Save one explicitly selected regular file to My Library |
| `files status`, `files read` | Check processing or read extracted LLM text |
| `files delete` | Permanently remove the specified known document, without an interactive confirmation |
| `monitors list`, `monitors get` | Read one page; `get --mark-read` also changes the read state of that returned content |
| `install-skill` | Write the bundled agent skill locally; `--force` replaces its existing `portrait` directory recursively |

Authentication alone does not grant Easel access. `ask` and `sessions` require both full Easel access and the provisioned CLI bridge. File and direct Monitor operations have their own access checks. A read-like command name or a successful login does not authorize a later write.

`ask` accepts exactly one non-empty prompt argument. Place `--session`, `--document`, and `--stream` before it. Use shell-safe quoting; currency written as `USD 10 billion` avoids shell expansion of a dollar-prefixed number.

```sh
portrait ask --stream 'Compare the latest earnings commentary for AAPL and MSFT.'
```

## JSON envelopes

With non-interactive stdout, a non-streaming operational command writes one JSON envelope. A successful command exits with status `0`; a reported CLI failure exits with status `1`. Help and version remain plain text. Interactive authentication and non-streaming interactive errors use concise text; login instructions and diagnostics go to stderr. Keep stderr separate from a JSON parser.

Illustrative `ask` success; the UUID and answer below are placeholders:

```json
{"schemaVersion":1,"ok":true,"command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001","finalText":"Illustrative answer."}}
```

Illustrative failure before a selected document is ready:

```json
{"schemaVersion":1,"ok":false,"command":"ask","error":{"code":"DOCUMENT_NOT_READY","message":"Document is not ready.","httpStatus":null,"retryable":false}}
```

`command` identifies the operation, for example `ask`, `files.status`, or `sessions.list`. Require a supported `schemaVersion`, inspect `ok`, and read the command-specific `data`. Do not assume all data uses the same identifier casing. Error fields are `code`, `message`, nullable `httpStatus`, and boolean `retryable`. This schema does not include a serialized retry-delay field.

## Authentication errors

Authentication uses the operating system keyring. Keep stdout and stderr separate: the browser verification instructions are not JSON response data.

| Error | Meaning and recovery |
|---|---|
| `DEVICE_CODE_EXPIRED` | Restart `auth login` and use the newly printed code |
| `AUTHORIZATION_DENIED` | Authorization was declined; do not retry automatically |
| `AUTH_NETWORK_ERROR` | The authorization service could not be reached or its response could not be read; check connectivity before retrying login |
| `AUTH_LOGIN_IN_PROGRESS`, `AUTH_MUTATION_IN_PROGRESS` | Another authentication change holds the local lock; finish that operation first |
| `KEYRING_TIMEOUT`, `KEYRING_BACKEND_ERROR`, `KEYRING_HELPER_ERROR` | Credential-store access failed; repair the OS keyring or execution environment |
| `KEYRING_WRITE_UNCONFIRMED`, `KEYRING_STATE_UNKNOWN` | Stored sign-in state is uncertain; restore keyring access and sign in again |
| `KEYRING_DELETE_UNCONFIRMED` | Local credential removal was not confirmed; do not report successful local logout |
| `LOGOUT_INCOMPLETE` | Read the message's separate remote-revocation and local-removal results; neither implies the other succeeded |

Successful logout data includes `environment`, `revocationAttempted`, `revocationSucceeded`, and `localRemovalSucceeded`. With no stored credential, no revocation may be needed. On failure, these outcomes are described in the error message rather than a success `data` object.

If local removal succeeded but remote revocation failed, running logout again cannot revoke a token that is no longer stored. Contact support if remote revocation must be confirmed. Do not collect or transmit the token to diagnose this condition.

## Streaming completion

`ask --stream` writes NDJSON on stdout. Each line is one independent JSON object; there is no enclosing array and no `ok` field on these stream records.

```jsonl
{"schemaVersion":1,"type":"answer.delta","command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001","delta":"Illustrative "}}
{"schemaVersion":1,"type":"answer.final","command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001","finalText":"Illustrative answer."}}
{"schemaVersion":1,"type":"operation.complete","command":"ask","data":{"session_id":"00000000-0000-4000-8000-000000000001"}}
```

Use deltas for progress display only. The authoritative complete answer is `answer.final.data.finalText`; do not reconstruct it by joining deltas. Retain the returned `session_id` and require `operation.complete` plus a successful process exit before reporting transport completion. A completed transport still does not establish that the research answered the question correctly or that every proposed action ran.

A streaming failure is an `operation.error` record with the error at the top level:

```jsonl
{"schemaVersion":1,"type":"operation.error","command":"ask","error":{"code":"DOCUMENT_NOT_READY","message":"Document is not ready.","httpStatus":null,"retryable":false}}
```

EOF, a process interruption, or some received deltas without a successful terminal sequence leave the outcome uncertain. Preserve partial text as partial; do not label it a completed answer. Handle unknown fields without discarding known data, and stop for review if the schema version is unsupported.

## Identifiers, readiness, and pagination

All paths in this table are within the JSON envelope. Each read makes one page or chunk request; the CLI does not automatically exhaust the result.

| Operation | Identifier or state to retain | Continue / finish |
|---|---|---|
| `sessions list --limit N` | IDs in `data.sessions`; `data.next_page_cursor` | `N` is 1–50. Pass a non-empty returned cursor unchanged to `--cursor`; null or absent means no next page |
| `sessions create`, `ask` | `data.session_id` | Reuse the returned ID. Resume preserves the durable identity; an already-active resume can return `data.already_active=true` |
| `files upload` | `data.document_id` | Upload accepts one regular file, at most 26,214,400 bytes (25 MiB); it does not wait for extracted text to finish |
| `files status` | `data.documentId`, `data.representations`, `data.llmReady` | Only `llmReady=true` establishes that CLI text reading and `ask --document` can use the LLM representation |
| `files read` | `data.content`, `data.offset`, `data.limit`, `data.total_size` | Pass returned `data.nextOffset` as the next `--offset`; its absence means the end was reached. Default offset is 0 and limit is 65,536; maximum limit is 4,194,304 |
| `monitors list` | `data.total`, `data.limit`, `data.offset`, and each `datapoint_monitor_id` | Default limit 50, maximum 100. Advance `--offset` with the page's limit until the returned total is covered; preserve the filters |
| `monitors get` | `data.next_cursor` | Pass a non-empty cursor unchanged to `--cursor`; null or absent means no next page. Maximum content limit is 100 |

`files read` returns extracted LLM text, not original binary bytes. An uploaded Word document or workbook cannot be faithfully reconstructed from that text alone. A document still being prepared causes `ask --document` to refuse with `DOCUMENT_NOT_READY`; use a later status check instead of repeatedly submitting research turns.

For Monitor company filters, use full composite tickers returned by Portrait. Responses retain company UUID arrays and positionally matching `tickers` arrays; a ticker can be `null`. Do not shift the arrays or guess a ticker for an unresolved company. `monitors get --project` can pin a known Monitor to the Project ID returned by discovery; omit it when not needed.

`--mark-read` marks only the content retrieved in that page, after the read. Omit it for inspection that must preserve unread state. Paging through a changing unread feed while marking pages changes that feed; do not equate one marked page with marking the entire Monitor.

## Recover from an uncertain request

1. Keep the session ID, command, exit status, and safely redacted error or partial output.
2. If the request may have reached Portrait, inspect `sessions history` for that session before repeating an instruction. A saved transcript can trail work in flight, so absence of a final answer is not proof that nothing ran.
3. Reconnect authentication when required. Use a returned session ID to resume; do not substitute a new conversation merely to avoid an access or capacity refusal.
4. Treat `retryable=true` as a transient-error classification, not permission to repeat effects. Reconfirm any changed scope or proposed costly operation with the user.

If a failed `ask` returned no session ID, use `sessions list` to find candidate conversations from that attempt and inspect their histories before choosing one. Match the request and time; do not guess from a similar title. If the prior outcome cannot be identified, report it as uncertain instead of starting replacement work.

### Idempotency and message identity

Within one `ask` invocation, the CLI reuses its `clientMessageId` while performing supported transport recovery. A second invocation generates a new message identity, even with the same `--session`. The session ID selects conversation context; it is not an idempotency key. Without `--session`, the invocation also creates a new conversation. There is no CLI option for making separate invocations the same research turn.

### HTTP 429 and retry delays

For Easel operations, HTTP `429` can produce `RATE_LIMITED` (`retryable=true`) or `USAGE_REFUSED` (`retryable=false`). Inspect the error code as well as the status: a usage refusal needs its usage or access condition resolved before another attempt.

For `RATE_LIMITED`, the CLI can retry once within the same request when the server provides a numeric `Retry-After` delay that fits the CLI's bounded recovery budget. A missing, unusable, or excessive delay does not trigger that automatic retry. The JSON error envelope does not expose the header or a machine-readable retry delay. If the CLI returns the error, retain the session and review its history before deciding whether another `ask` is safe; do not infer a delay from the HTTP status or repeat the turn solely because `retryable` is true.

Stopping the local process or losing its network connection does not confirm remote cancellation. This CLI version has no public cancel or end command. Use the supported conversation controls in Portrait and verify the state; do not relaunch a screen or other workflow solely because its answer stream ended unexpectedly.

## Validate a consuming agent

Before using a parser for real research, check it with local fixtures for a complete stream, a stream ending before completion, an `operation.error`, an unready document, two-page results, and a null company ticker. Verify that it preserves exact IDs, detects incomplete output, and never changes read state or repeats a write merely to recover from a read failure.