AI agents

The TaleArt skill teaches an AI agent, such as Claude Code, to manage your zones, tale groups, tales and targeting with the taleart command-line tool, and tells it how to handle your API token.

What the skill does

  • Gives the agent the TaleArt words (project, zone, display, tale group, tale, site key, tags, user lists, Visible on site) and the rule that decides when a zone shows a group.
  • Gives it recipes with exact commands: a zone with a bar and its install snippet, a tale group of image tales turned On, targeting by tags and by user lists, why something is not visible, copying a group to another zone, rotating a site key.
  • Makes it work the way scripts should: --json on every command, errors read by exit code, commands discovered with taleart help commands --json instead of guessed.
  • Has it report what changed: names, ids, On or Off, and whether each thing is visible on site.

Safety rules

The skill tells the agent to:

  • Never print, echo, log or store your token, never pass it on the command line, and never ask you to paste it into the chat. The CLI shows only its prefix (tat_ and 6 characters).
  • Never run commands that print the environment (env, printenv, set), never read the CLI's config file or a token file, and check access only with taleart whoami --json or taleart config show --json.
  • Ask before anything destructive: rotating or revoking the site key, deleting a tale or a user list, changing a zone's slug. It adds --yes only after you agree; without it the CLI refuses (exit 10) and changes nothing.
  • Use a read-only token, or only read commands, for tasks that only read.
  • Also ask before changes the CLI does not call destructive but you may not expect: clearing a group's tags or user lists, turning a group On where the zone is already installed, replacing a tale's media, changing the default project.
  • Run no more commands than the task needs, and stop and ask you after two failed tries at the same step.
  • Keep external user IDs out of URLs, commands and its replies: they go only into the CSV file you give it, which it never reads (it asks you whether the first row is a header), and TaleArt keeps only keyed hashes of them.
  • Hand you a new site key once, and never write it to a file unless you ask.

Install it

With the taleart command-line tool, in one line:

taleart skill install            # for you: ~/.claude/skills/taleart
taleart skill install --project  # for one repository: .claude/skills/taleart
taleart skill install --dir ~/.agents/skills   # another agent's skills folder

The CLI carries the skill, so this works without the TaleArt repository. If the folder already holds a different version, it stops (exit 6); add --force to replace the skill's files. Start a new agent session afterwards so the agent loads it.

Or copy the folder by hand: the skills/taleart folder of the TaleArt repository, or the files below, to ~/.claude/skills/taleart (for you) or .claude/skills/taleart (for one repository):

mkdir -p ~/.claude/skills
cp -R skills/taleart ~/.claude/skills/taleart

No taleart on your PATH yet? In the TaleArt repository, node_modules/.bin/nx run cli:build builds it to dist/apps/cli/taleart.js (Node 22 or later).

Give it a token

  1. Create an API token in the dashboard (Settings > API tokens): only the projects the agent needs, and read-only unless it must change things.
  2. In your own terminal, before you start the agent, put the API URL and the token in the environment. Type or paste the token there, never into the agent's chat:
export TALEART_API_URL=https://your-taleart-api
read -rs TALEART_TOKEN && export TALEART_TOKEN   # paste the token; it is not shown

Or store them in the CLI's config file (mode 0600) with taleart login --url https://your-taleart-api --token-stdin < token.txt, then delete token.txt. The agent checks access with taleart whoami --json, which shows the token's prefix, never the token.

An example session

The skill's main recipe, run through the CLI against a test API (answers shortened; the site key is redacted, and the token never appears):

  1. You: Create a zone home-top with a bar and a group Summer with two tales from ./img, and turn it On.

  2. Agent: I will use the TaleArt skill. First, checking access and the commands I will need.

    $ taleart whoami --json
    { "tokenPrefix": "tat_7OCJsd", "tokenSource": "env", "readOnly": false, "allProjects": true,
      "user": { "name": "Dana", "role": "admin" },
      "projects": [{ "id": "453572e6-…", "name": "Shop" }], "defaultProject": null }
    
    $ taleart help commands --json
    { "commands": [ … { "command": "zones create", "mutates": true, "destructive": false, … } … ] }
  3. Agent: The token reaches one project, Shop, so I pass --project "Shop" on every command. Creating the zone, its bar, the group and one tale per image.

    $ taleart zones create --name "Home top" --slug home-top --project "Shop" --json
    { "id": "b950ac31-…", "slug": "home-top", "frame": null,
      "visibility": { "visible": false, "reasons": [{ "code": "zone-no-display" }, { "code": "zone-not-installed" }, { "code": "no-tale-groups" }] } }
    
    $ taleart zones display home-top --type bar --project "Shop" --json
    { "type": "bar", "settings": { "gap": 12, "size": "medium", "shape": "circle", "alignment": "start", "showTitles": true } }
    
    $ ls ./img
    ocean.png  sunrise.png
    
    $ taleart groups create --name Summer --zone home-top --project "Shop" --json
    { "id": "985385d5-…", "name": "Summer", "status": "draft", "position": 0, "zoneIds": ["b950ac31-…"] }
    
    $ taleart tales create --group Summer ./img/sunrise.png ./img/ocean.png --project "Shop" --json
    [ { "id": "f536ba70-…", "title": "sunrise", "storedStatus": "live", "media": { "contentType": "image/png" } },
      { "id": "222121a4-…", "title": "ocean", "storedStatus": "live", "media": { "contentType": "image/png" } } ]
    
    $ taleart groups on Summer --project "Shop" --json
    { "name": "Summer", "status": "live",
      "visibility": { "visible": false, "reasons": [{ "code": "zone-not-installed", "subject": "zone" }] } }
    
    $ taleart zone-keys info home-top --project "Shop" --json
    null
  4. Agent: Summer is On in home-top, but it is not visible yet: the zone has no zone key (zone-not-installed). A zone key is issued once per zone and shown only once. Shall I issue it?

  5. You: Yes, issue it.

  6. Agent: Issuing it, then checking again and getting the snippet.

    $ taleart zone-keys issue home-top --project "Shop" --json
    { "key": "tak_oRYmcH…[REDACTED]", "prefix": "tak_oRYmcH", "createdAt": "2026-10-24T05:46:41.164Z" }
    
    $ taleart groups get Summer --project "Shop" --json
    { "name": "Summer", "status": "live", "tales": [ …2 tales… ], "visibility": { "visible": true, "reasons": [] } }
    
    $ taleart zones snippet home-top --zone-key "tak_oRYmcH…[REDACTED]" --project "Shop" --json
    { "snippet": "<script type=\"module\" src=\"https://api.example.com/sdk/v1/taleart.js\"></script>\n<!-- Optional, once per page: <script>Taleart.init({ locale: \"en\" })</script> -->\n<taleart-view zone-key=\"tak_oRYmcH…[REDACTED]\"></taleart-view>",
      "testPageUrl": "https://api.example.com/sdk/v1/test-page#zoneKey=tak_oRYmcH…[REDACTED]" }
  7. Agent: Done. Created the zone Home top (home-top) with a bar display, and the tale group Summer in it with two Shown tales, sunrise and ocean; Summer is On and visible on site. I issued the zone's key (prefix tak_oRYmcH): it is shown above once, keep it; I did not save it anywhere. Put the snippet on your page, or open the test page link to see the zone now.

The skill

SKILL.md holds what an agent reads first; it points to the reference files for the rest. Download each file, or read it here.

SKILL.md
---
name: taleart
description: Manages TaleArt projects through the taleart command-line tool - zones and their bar or banner displays, tale groups and their tales, targeting by tags and user lists, site keys and install snippets. Use when the user asks to create, change, check, copy or troubleshoot TaleArt zones, tale groups, tales, targeting or site keys, or to find out why TaleArt content does not show on a site.
---

# TaleArt through the taleart CLI

TaleArt shows tales (image or video pages) on a website. The `taleart` CLI drives its Management API
with an API token. Every task here runs through that CLI; never call the API by hand when a command
exists.

## Before anything: check access

```bash
taleart whoami --json
```

- Exit 0: note `user.role`, `readOnly`, `projects` and `defaultProject`.
- Exit 3: there is no URL or token. Ask the user to set them; see [reference/setup.md](reference/setup.md).
- `taleart` not found: see "Get the CLI" in [reference/setup.md](reference/setup.md).

The token lives in the environment (`TALEART_TOKEN`) or in the config file written by
`taleart login --token-stdin`. **Never print, echo, log or write the token anywhere, never pass it with
`--token`, and never ask the user to paste it into the chat.** The CLI only ever shows its prefix
(`tat_` and 6 characters).

You run with the user's environment, so the token is within reach: keep it that way, unread.

- Never run `env`, `printenv`, `set`, `export -p`, `declare -x` or `set -x`, and never
  `echo $TALEART_TOKEN` (or any command that expands it).
- Never read, `cat`, open or search the CLI's config file (`taleart config path` names it) or any
  file that holds a token.
- To know what is in use, run `taleart whoami --json` or `taleart config show --json`: they show
  where the URL and token come from and the token's prefix, never the token.

## Operating rules

1. **Always pass `--json`** and parse stdout: it holds exactly one JSON document, the API's answer
   (a `Zone`, a `TaleGroup`, a list...). Do not scrape the human-readable tables.
2. **Read errors from stderr.** With `--json` a failure writes one document,
   `{"error":{"code","message","status","exitCode","hint"}}`. Decide by the exit code
   (table below), explain the `message` and follow the `hint`. A success with warnings writes
   `{"warnings":[...]}` to stderr: pass them on to the user.
3. **Discover, never guess.** Run `taleart help commands --json` once per session for every command's
   options, arguments, `mutates`, `destructive` and `result` schema, or `taleart help <group> <command>`
   for one. Use only flags it lists.
4. **Read-only first.** For a task that only reads (list, check, explain), prefer a read-only token
   (`readOnly: true` in `whoami`). If the token in use can write, still run only read commands.
5. **Confirm destructive commands.** Before any command whose `destructive` flag is `true`
   (`zone-keys rotate`, `zone-keys revoke`, `site-keys rotate`, `site-keys revoke`, `tales delete`, `user-lists delete`,
   `zones update --slug`), tell the user exactly what it will do and what breaks, and wait for a clear
   yes. Only then add `--yes`. Never add `--yes` on your own. Without it the CLI stops with exit 10 and
   changes nothing.
6. **Confirm these too**, although the CLI does not flag them destructive, unless the user asked for
   exactly that change: `groups audience --clear` and `groups tags --clear` (the group then shows to
   more viewers), `groups on` when the group would go live in a zone that is installed (the project
   has a zone key), `tales media` (replaces a tale's image or video), and `projects use` (changes the
   default project in the user's config file).
7. **Run no more than needed.** No exploratory writes, no test objects, no retries of a write that
   failed with exit 4, 6 or 7 without changing something first. A read is fine to repeat.
8. **Two strikes, then stop.** If the same step fails twice (two non-zero exits, whatever you changed
   in between), stop: report the command, the exit codes and the error messages, and ask the user how
   to go on. Do not try a third variation.
9. **Pick the project explicitly.** Pass `--project <id|name>` (or rely on `defaultProject` when the
   user means it). Names must be exact for destructive commands.
10. **Report what changed:** for each object created or changed, its kind, name or slug, id, status
    (On/Off, Shown/Hidden) and whether it is visible on the site (`visibility.visible`, or the reason
    codes). Say what you did not do and why.
11. **Privacy.** External user IDs go only into a CSV file given to `user-lists upload`; never put
    them in a URL, a command argument, a log or your report. Never open, print, count by value or
    summarise such a CSV: ask the user whether its first row is a header. TaleArt stores only keyed hashes and
    reports counts. A zone key (`tak_...`) or site key (`tas_...`) is shown once: give it to the user, never write it to a file
    unless they ask.

## Exit codes

| Code | Meaning | What to do |
| --- | --- | --- |
| 0 | Success | Parse stdout. |
| 1 | Unexpected error | Report it with the message; do not retry blindly. |
| 2 | Usage error (unknown command or flag, missing argument, no project) | Re-read `taleart help commands --json`; pass `--project`. |
| 3 | Not authenticated (no URL or token, or 401) | Ask the user to set `TALEART_API_URL` and `TALEART_TOKEN`. |
| 4 | Forbidden (403: role, the token's projects, or a read-only token) | Tell the user which token or role is needed. |
| 5 | Not found (404, or a name or slug matching nothing) | List first (`zones list`, `groups list`) and use the exact name. |
| 6 | Conflict (409, or a name matching several) | Use an id, or pick another slug or name. |
| 7 | Refused input (400, 413, 415, 422, or a bad local file) | Fix the value or the file the message names. |
| 8 | Rate limited or server error (429, 5xx) | Wait and retry once; then report. |
| 9 | API unreachable or timed out | Check `TALEART_API_URL`; report. |
| 10 | Confirmation needed | Ask the user; add `--yes` only after they agree. |

More in [reference/troubleshooting.md](reference/troubleshooting.md).

## The main recipe: a zone with a bar and a group of two tales, On

User: "create a zone home-top with a bar and a group Summer with two tales from ./img, and turn it On".

```bash
taleart whoami --json
taleart zones create --name "Home top" --slug home-top --project "Shop" --json
taleart zones display home-top --type bar --project "Shop" --json
taleart groups create --name Summer --zone home-top --project "Shop" --json
taleart tales create --group Summer ./img/sunrise.png ./img/ocean.png --project "Shop" --json
taleart groups on Summer --project "Shop" --json
taleart groups get Summer --project "Shop" --json
taleart zones snippet home-top --project "Shop" --json
```

- List `./img` first and pass the image or video files it holds, in the order the user wants.
- `groups get` answers `visibility`: when `visible` is false, `reasons[].code` says why (often
  `zone-not-installed`: the zone has no zone key yet; see the install recipe).
- Give the user the snippet and the `testPageUrl` from `zones snippet`.

Every other recipe (install snippet and zone key, image tales, tags, user lists, why something is not
visible, copying a group, rotating a zone key) is in [reference/recipes.md](reference/recipes.md).

## Concepts in one breath

A **project** holds zones, tale groups and one **site key** (for server-side callers only). A **zone** is a place on a page, named
by its **slug**, with one **display** (bar or banner) and one **zone key** (the page's key for that zone). A **tale group** is a set of **tales**; it is
placed in zones and is **On** or **Off** (new groups start Off). A tale is **Shown** or **Hidden**.
**Tags** (with match **Any** or **All**) and **user lists** (external user IDs) target a group to some
viewers. "**Visible on site**" (`visibility.visible`) says whether it shows now. Details:
[reference/concepts.md](reference/concepts.md).

## Reference

- [reference/setup.md](reference/setup.md): get the CLI, the token, projects, config.
- [reference/concepts.md](reference/concepts.md): every TaleArt term and the visibility rule.
- [reference/recipes.md](reference/recipes.md): exact commands for common tasks.
- [reference/troubleshooting.md](reference/troubleshooting.md): by exit code and by visibility reason.
reference/setup.md
# Setup: the CLI, the token, the project

## Get the CLI

Check first:

```bash
command -v taleart && taleart --version --json
```

If `taleart` is not on the PATH, it is built from the TaleArt repository (Node 22 or later, no other
runtime):

```bash
node_modules/.bin/nx run cli:build
node dist/apps/cli/taleart.js --version --json
```

The build is one file, `dist/apps/cli/taleart.js`. Either put it on the PATH once
(`ln -s "$PWD/dist/apps/cli/taleart.js" ~/.local/bin/taleart`, with the user's agreement) or write
`node <repo>/dist/apps/cli/taleart.js` wherever these files say `taleart`.

## Install this skill

```bash
taleart skill install            # for the user: ~/.claude/skills/taleart
taleart skill install --project  # for this repository: .claude/skills/taleart
taleart skill install --dir ~/.agents/skills   # another agent's skills folder
```

The CLI carries the skill's files. When the folder already holds different files, it stops (exit 6)
unless `--force` is given. Copying the `skills/taleart` folder of the repository to one of those places
does the same.

## The API URL and the token

The CLI reads, first match wins:

- API URL: `--api-url`, then `TALEART_API_URL`, then the config file.
- Token: `TALEART_TOKEN`, then the config file.
- Project: `--project`, then `TALEART_PROJECT`, then the config file (`taleart projects use`).

The user creates the token in the dashboard: **Settings > API tokens**. It starts `tat_`, is shown
once and can be limited to some projects and to read-only. Ask the user to put it in the environment
themselves (their shell profile or a secret manager), for example:

```bash
export TALEART_API_URL=https://taleart.example.com
export TALEART_TOKEN=...   # the user types or pastes this in their own terminal, never in the chat
```

Or, to store it in the CLI's config file (mode 0600), the user runs in their terminal:

```bash
taleart login --url https://taleart.example.com --token-stdin < token.txt
```

`--token-stdin` keeps the token out of the shell history and the process list. Never use the
`--token` flag of `taleart login`, never `echo` the token, never write it to a file you keep, and never ask for it
in the chat. If the user pastes a token into the chat anyway, do not repeat it; suggest they revoke it
and create another.

An agent runs with the user's environment, so `TALEART_TOKEN` is within its reach. Keep it unread:

- Never run `env`, `printenv`, `set`, `export -p`, `declare -x` or `set -x` (they print every
  variable, the token included), and never `echo $TALEART_TOKEN` or expand it in any command.
- Never read, `cat`, open, copy or search the CLI's config file (`taleart config path` says where it
  is) or any file that holds a token (such as `token.txt` above).
- To see what is set, use `taleart whoami --json` or `taleart config show --json`: they name the
  source of the URL and the token (`env` or `config`) and show the token's prefix only.

Check:

```bash
taleart whoami --json
taleart config show --json
```

`whoami` answers the user, role, token name, `readOnly`, `allProjects`, `expiresAt`, the projects the
token reaches and the default project, and only the token's prefix. `config show` says where each
setting comes from.

## Read-only or read-write

- A task that only reads (list zones, explain why a group does not show, report statuses) needs only a
  read-only token. If the user has one for such tasks, ask them to use it.
- A read-only token answers exit 4 (`forbidden`) to any change: tell the user a read-write token is
  needed rather than looking for a way around it.
- An admin's read-write token can issue, rotate and revoke zone keys (one zone each) and site keys (every zone of the project, server-side only).

## Choose the project

```bash
taleart projects list --json
```

Pass `--project <id|name>` on each command (safest when the token reaches several), or, with the
user's agreement, make one the default in the config file:

```bash
taleart projects use "Shop" --json
```

## Non-interactive by design

The CLI never asks a question when stdin or stderr is not a terminal, with `--json`, or when `CI=true`
or `TALEART_NO_INPUT=1` is set. A destructive command then stops with exit 10 and changes nothing.
Set `TALEART_NO_INPUT=1` in your environment to be sure no prompt ever waits for input.
reference/concepts.md
# TaleArt concepts

Use these words with the user; they are the dashboard's words. `taleart help concepts` prints the
same summary.

| Term | Meaning | In the CLI's JSON |
| --- | --- | --- |
| Project | Holds zones, tale groups, user lists, tags and one site key. A token may reach only some projects. | `Project` (`id`, `name`) |
| Zone | A place on the site where tales show, named by its **slug** (lowercase letters, digits and hyphens, such as `home-top`). A page places it with `<taleart-view zone="home-top">`. | `Zone` (`id`, `slug`, `name`, `frame`, `visibility`) |
| Display | How a zone shows its tale groups: a **bar** (a row of circles, one per group) or a **banner** (wide slides that rotate). A zone has at most one. | `Zone.frame` (`type`, `settings`); `taleart zones display-types` |
| Site key | `tas_...`, one per project, shown only once when issued. Server-side callers send it to the Delivery API with the zone slug. Web pages use each zone's zone key instead. Rotating it breaks every server that has the old one. | `IssuedSiteKey.key` (once), `SiteKeyInfo.prefix` |
| Install snippet | The script tag and `<taleart-view>` element for a page, and a test page link. | `taleart zones snippet` |
| Tale group | A set of tales, one circle in a bar or one slide in a banner. Belongs to the project; placed in one or more zones, in an order per zone. | `TaleGroup` (`id`, `name`, `status`, `storedStatus`, `zoneIds`, `tales`, `tags`, `tagMatch`, `userLists`, `visibility`) |
| On / Off | A group shows only when **On** (stored `live`). New groups start Off (`draft`); Off is `paused`. | `taleart groups on`, `taleart groups off` |
| Tale | One page of a group: an image or a video, uploaded from a file. | `Tale` (`id`, `title`, `media`, `status`, `visibility`) |
| Shown / Hidden | A tale shows only when **Shown** (`live`); **Hidden** is `paused`. New tales start Shown. | `taleart tales show`, `taleart tales hide` |
| Tags | Targeting by the viewer's tags, which the page sets with the SDK. A group with no tags shows to everyone. | `TaleGroup.tags`, `tagMatch` |
| Any / All | How a group's tags combine: **Any** shows the group to a viewer with at least one of them, **All** only to a viewer with every one. A viewer whose page sets no tags always passes. | `--match any`, `--match all` |
| User list | A list of external user IDs (the site's own customer IDs) for targeting. A group targeted to lists shows only to viewers whose ID, set by the page with the SDK, is in one of them. | `UserList` (`id`, `name`, `idCount`) |
| External user ID | An opaque ID such as `cust-1001`, never an email address or a phone number. TaleArt stores only a keyed hash of each one and reports counts, never values. | CSV rows for `taleart user-lists upload` |
| Visible on site | Whether a zone, group or tale shows on the site right now, computed by the same rule the Delivery API uses. When not, the reason codes say why. | `visibility.visible`, `visibility.reasons[].code` |

## When a zone shows a group

All of these must hold:

1. The zone has a display (bar or banner).
2. The zone has an active zone key, and the page sends it.
3. The group is placed in the zone and is On, inside its schedule window if it has one.
4. The group has at least one Shown tale whose media is ready.
5. The viewer passes the group's targeting: its tags (Any or All) and, if it has user lists, the
   viewer's external user ID is in one of them.

Targeting is decided per viewer, so `visibility` cannot know a viewer's tags: a group targeted by tags
reads visible when everything else holds. A group targeted only to user lists that hold no ID reads
`audience-empty`.

## Privacy

- External user IDs are personal data of the user's customers. Put them only in a CSV file the user
  gives you, uploaded with `taleart user-lists upload`. Never in a URL, a query string, a command-line
  argument, a log, a commit or your report.
- An upload report gives counts only (`added`, `alreadyPresent`, `refused`, `emailLike`, `phoneLike`,
  `idCount`) and refused rows by line number. If `emailLike` or `phoneLike` is above zero, tell the user:
  the list holds what looks like email addresses or phone numbers, which should not be used as IDs.

## References the CLI accepts

- Projects by id or exact name; zones by id or slug; tale groups and user lists by id or name within
  the project; tales by id only.
- A name that matches nothing is exit 5, several is exit 6: then use the id from a `list` command.
- Destructive commands take exact names only.
reference/recipes.md
# Recipes

Exact commands for common tasks. Every command takes `--json` and prints one JSON document on stdout.
Replace `"Shop"` with the user's project (name or id) and the names with theirs. Before a recipe, run
`taleart whoami --json` once; before a command you have not used in this session, its help:
`taleart help <group> <command> --json`.

In a recipe, stop at the first command that exits non-zero, read the error on stderr and decide by the
exit code ([troubleshooting.md](troubleshooting.md)). Do not run the remaining steps blindly.

## 1. A zone with a bar, and its install snippet

```bash
taleart zones create --name "Home top" --slug home-top --project "Shop" --json
taleart zones display home-top --type bar --project "Shop" --json
taleart zone-keys info home-top --project "Shop" --json
taleart zones snippet home-top --project "Shop" --json
```

- `zones create --template stories-bar` instead of `zones display` gives the zone a bar in one step;
  `taleart zones display-types --json` lists the display types and their settings. Settings:
  `taleart zones display home-top --set size=large --set gap=8 --project "Shop" --json`.
- A banner: `taleart zones display home-top --type banner --project "Shop" --json`.
- `zone-keys info` answers `null` when the zone has no key yet. Then, with the user's agreement
  (a zone needs one to show anything), issue it:

  ```bash
  taleart zone-keys issue home-top --project "Shop" --json
  ```

  The answer's `key` (`tak_...`) is shown **once**: give it to the user to keep, do not write it to a file.
  When the zone already has a key, `issue` answers exit 6 (`zone_key_exists`) and changes nothing: the
  user has the key; never rotate to get it back. `zone-keys rotate` and `zone-keys revoke` affect only this zone.
- `zones snippet` answers `snippet` (the HTML for the page) and `testPageUrl`. Without
  `--zone-key` the snippet says `YOUR_ZONE_KEY`: the user puts their key there. Right after issuing,
  `taleart zones snippet home-top --zone-key "<the key>" --project "Shop" --json` fills it in.

## 2. A tale group with image tales from local files, turned On

```bash
ls ./img
taleart groups create --name Summer --zone home-top --project "Shop" --json
taleart tales create --group Summer ./img/sunrise.png ./img/ocean.png --project "Shop" --json
taleart groups on Summer --project "Shop" --json
taleart groups get Summer --project "Shop" --json
```

- One tale per file, in the order given, each titled after its file (`--title` for one file only).
  Images (PNG, JPEG, WebP, GIF) and videos (MP4, WebM) are read by their first bytes; a bad file stops
  the command before anything is created (exit 7).
- If `tales create` stops part way, its error lists `created` (the tales made) and the `file` that
  failed. Report both; do not create the made ones again.
- Without `--zone`, the group belongs to the project and is placed nowhere: place it with
  `taleart groups place Summer --zone home-top --project "Shop" --json`.
- `groups on` warns (stderr `warnings`) when the group has no tales or no zone: pass that on.
- When the zone already has a zone key, `groups on` puts the group live on the site at once. The
  user asked for it here; when a task did not say "turn it On", ask first.
- Check `visibility` in the `groups get` answer; see recipe 5 when it is false.
- Hide one tale without deleting it: `taleart tales hide <tale id> --json` (`tales show` to undo).

## 3. Target a group with tags

```bash
taleart groups tags Summer --tag vip --tag beta --match all --project "Shop" --json
```

- The tags given become the group's whole set; missing tags are created in the project.
- `--match any` (the default) shows the group to a viewer with at least one of the tags; `--match all`
  only to a viewer with every one. A viewer whose page sets no tags always passes.
- Change only the match: `taleart groups tags Summer --match any --project "Shop" --json`.
- Remove every tag (the group then shows to everyone; confirm with the user first):
  `taleart groups tags Summer --clear --project "Shop" --json`.
- The page sets the viewer's tags with the Web SDK's `setTags`; the docs' Targeting page shows how.

## 4. A user list from a CSV, and a group targeted to it

The CSV holds one external user ID per row (a single column). **Never read its values**: do not
open, `cat`, `head`, print, count by value or summarise it. Ask the user whether its first row is a
header (a column name such as `user_id`) and pass `--header` only if they say yes; the upload report
then gives the counts.

```bash
taleart user-lists create --name VIPs --project "Shop" --json
taleart user-lists upload VIPs ./vips.csv --header --project "Shop" --json
taleart groups audience Summer --list VIPs --project "Shop" --json
taleart groups get Summer --project "Shop" --json
```

- `--header` skips the first row, as the user answered; leave it out when they say the first row is
  an ID. If the user does not know, upload without `--header`: a header row is then one extra ID (or
  a refused row), which they can judge from the counts.
- The upload is all or nothing: refused rows are reported by line number, never by value. Report
  `added`, `alreadyPresent`, `refused` and `idCount`; warn when `emailLike` or `phoneLike` is above 0.
- `groups audience` sets the group's whole set of user lists (`--list` again for more). The group then
  shows only to viewers whose ID is in one of them, and its tag rule still applies.
- Remove the audience (the group then shows to every viewer; confirm first):
  `taleart groups audience Summer --clear --project "Shop" --json`.
- The page sets the viewer's ID with the Web SDK's `setExternalUserId`.
- Deleting a list is destructive (its groups lose that audience and stay hidden until someone sets a
  new one): `taleart user-lists delete VIPs --project "Shop" --yes --json`, only after the user agrees.

## 5. Why is something not visible?

```bash
taleart zones get home-top --project "Shop" --json
taleart groups list --zone home-top --project "Shop" --json
taleart groups get Summer --project "Shop" --json
taleart tales list --group Summer --project "Shop" --json
taleart zone-keys info home-top --project "Shop" --json
```

- Read `visibility.reasons[].code` on the zone, the group and each tale, most useful first. Map each
  code to its fix with the table in [troubleshooting.md](troubleshooting.md#visibility-reasons), tell
  the user what is wrong and which command would fix it, and run the fix only if they ask.
- Everything visible but nothing on the page: check the page's snippet has this zone's current key
  (`zone-keys info` answers its `prefix`), and the viewer passes the group's tags and user lists.
- This recipe only reads: a read-only token is enough.

## 6. Copy a group to another zone

```bash
taleart zones list --project "Shop" --json
taleart groups copy Summer --zone footer --project "Shop" --json
```

- The copy is an independent group with the **same name**, the same tales and tags, placed after the
  zone's groups. From then on the name matches two groups (exit 6), so use the copy's `id` from the
  answer: `taleart groups get <copy id> --json`, and rename it if the user wants
  (`taleart groups update <copy id> --name "Summer footer" --json`).
- The copy starts Off (`draft`), whatever the original's status. Turn it On with
  `taleart groups on <copy id> --json` if the user wants it showing, then check its `visibility`.
- Another project's zone: pass the zone's id (from `taleart zones list --project "Other" --json`).
  The copy does not take the original's user lists there (`audience-removed` until someone sets one).
- To show the **same** group in a second zone instead of a copy:
  `taleart groups place Summer --zone footer --project "Shop" --json`.

## 7. Rotate a zone key safely

Rotating replaces one zone's key: its old key stops working **at once** on every page that shows the
zone, until the page has the new key. It is destructive.

```bash
taleart zone-keys info home-top --project "Shop" --json
```

1. Tell the user: the zone, the current key's prefix, the pages that will stop showing it until they
   are updated, and that the new key is shown once.
2. Wait for a clear yes. If they want to plan first (for example deploy the pages right after), stop
   here.
3. Only then:

   ```bash
   taleart zone-keys rotate home-top --project "Shop" --yes --json
   ```

4. Give the user the new `key` once, and the snippet with it:
   `taleart zones snippet home-top --zone-key "<the new key>" --project "Shop" --json`. Do not write the
   key to a file.

`taleart zone-keys revoke home-top --project "Shop" --yes --json` turns that zone off with no new key: the
same confirmation, stronger wording. A project's site key (`site-keys`) is for server-side callers only.

## Other changes

- Rename: `taleart zones update home-top --name "Home banner" --project "Shop" --json`,
  `taleart groups update Summer --name "Summer 2026" --project "Shop" --json`.
- Change a slug (destructive: pages with the old slug stop showing the zone):
  `taleart zones update home-top --slug home-banner --project "Shop" --yes --json`, only after the
  user agrees.
- Turn a group Off: `taleart groups off Summer --project "Shop" --json`.
- Take a group out of a zone (it stays in the project):
  `taleart groups unplace Summer --zone home-top --project "Shop" --json`.
- Delete a tale (destructive, for good): `taleart tales delete <tale id> --yes --json`, only after the
  user agrees; `tales hide` is the undoable alternative.
reference/troubleshooting.md
# Troubleshooting

With `--json`, a failure prints one document on stderr:

```json
{"error":{"code":"slug_taken","message":"A zone with this slug already exists","status":409,"exitCode":6,"hint":"..."}}
```

`code` is the API's error code or one of the CLI's (`taleart help output --json` lists them); `status`
is the HTTP status when the API answered. Decide by the exit code, tell the user the `message`, and
follow the `hint`. `taleart help exit-codes --json` lists the codes.

## By exit code

**2, usage.** An unknown command or flag, a missing argument, a bad value, or no project chosen.
Do not guess another spelling: read `taleart help commands --json` (or
`taleart help <group> <command> --json`) and use what it lists. `No project chosen`: pass
`--project <id|name>` (`taleart projects list --json` lists them).

**3, not authenticated.** `no_api_url` or `no_token`: the environment has no `TALEART_API_URL` or
`TALEART_TOKEN` and nothing is stored; `invalid_token`: the value does not start `tat_`;
`unauthenticated` (401): the token is unknown, revoked or expired. Ask the user to create a token in the
dashboard (Settings > API tokens) and set it themselves; never ask for it in the chat.

**4, forbidden.** The token is read-only, does not reach that project, or the user's role does not
allow the change (for example a viewer, or a site key change by a non-admin). Tell the user which; do
not look for a way around it.

**5, not found.** An id, slug or name that matches nothing in the project. Run the matching list
(`taleart zones list --json`, `taleart groups list --project "Shop" --json`,
`taleart user-lists list --project "Shop" --json`) and use an exact name or the id. Tales are by id
only (`taleart tales list --group Summer --project "Shop" --json`). A project the token does not reach
also answers 404 or 403.

**6, conflict.** `slug_taken`: pick another slug or use the existing zone (ask the user which).
`ambiguous`: a name matches several objects; use the id. `site_key_exists`: the project already has a
site key; the user has it (never rotate just to see a key). `invalid_status_transition`: an archived or
processing group cannot be turned On or Off.

**7, refused input.** A value the API refused (`validation_failed`, the message names the field), a
local file that is missing (`file_not_found`), empty, too large or not an image or video
(`unsupported_media_type`), a CSV with refused rows (reported by line number). Fix what the message
names, then run the command again once.

**8, rate limited or server error.** Wait a few seconds and retry once. A feature turned off on the
server also lands here. Then report the message.

**9, unreachable.** `api_unreachable`, `timeout` or `response_interrupted`: check `TALEART_API_URL`
(`taleart config show --json` says where it comes from) and that the API runs. A write that timed out
may have happened: read before retrying (for example `taleart groups get Summer --json`).

**10, confirmation needed.** A destructive command without `--yes`. Nothing was changed. Ask the user,
naming what will happen; add `--yes` only after a clear yes.

**1, unexpected.** Report the message and the command; do not retry in a loop.

## Visibility reasons

`visibility.reasons[].code` on a zone, group or tale, most useful first. The dashboard shows the same
as "Visible on site" or "Not visible: <reason>".

| Code | Means | Fix (only when the user asks) |
| --- | --- | --- |
| `archived` | The group or tale is archived. | Restore it in the dashboard. |
| `group-blocked` | A process holds the group (media processing, invalid media, quota). | Wait, or fix the tales' media. |
| `group-off` | The group is Off. | `taleart groups on Summer --project "Shop" --json` |
| `not-in-any-zone` | The group is placed in no zone. | `taleart groups place Summer --zone home-top --project "Shop" --json` |
| `tale-hidden` | The tale is Hidden. | `taleart tales show <tale id> --json` |
| `media-processing` | The tale's media is still processing. | Wait, then read again. |
| `media-problem` | The tale's media is invalid or over quota. | `taleart tales media <tale id> ./img/new.png --json` |
| `not-updated-yet` | Nothing was published for the tale yet. | Publish it in Tale Studio (dashboard). |
| `starts-at` / `ended-at` | Outside the group's schedule window (`at` says when). | Change the schedule in the dashboard. |
| `zone-no-display` | The zone has no bar or banner. | `taleart zones display home-top --type bar --project "Shop" --json` |
| `zone-not-installed` | The zone has no active zone key. | `taleart zone-keys issue home-top --project "Shop" --json`, then the snippet on the page. |
| `no-tale-groups` | The zone has no groups. | `taleart groups place Summer --zone home-top --project "Shop" --json` |
| `no-shown-tales` | The group has no Shown tale with media. | `taleart tales create --group Summer ./img/one.png --project "Shop" --json` |
| `audience-empty` | The group's user lists hold no ID. | `taleart user-lists upload VIPs ./vips.csv --project "Shop" --json` |
| `audience-removed` | The group's user lists were removed (deleted, or copied to another project). | `taleart groups audience Summer --list VIPs --project "Shop" --json`, or `--clear` for everyone. |
| `no-visible-groups` | The zone has groups but none is visible. | Read each group's reasons. |

Tags are decided per viewer, so no reason code covers them: a group can read visible and still not show
to a viewer whose tags do not match (`tagMatch` any or all).

## Common mistakes

- Running a command without `--json` and parsing its table: rerun with `--json`.
- Turning a group On before it has tales: `groups on` works and warns; add the tales.
- Two zones with the same slug in different projects: pass `--project`, or the zone's id.
- A video still processing after upload reads `media-processing` for a while: that is expected.