@commercelayer/agent-cli 0.8.0 → 1.0.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +95 -215
  2. package/dist/argv.js +214 -0
  3. package/dist/bin.js +15 -178
  4. package/dist/builtins/auth.js +169 -0
  5. package/dist/builtins/extensions.js +123 -0
  6. package/dist/builtins/index.js +8 -0
  7. package/dist/builtins/loops.js +215 -0
  8. package/dist/builtins/sandbox.js +186 -0
  9. package/dist/config.js +211 -0
  10. package/dist/confirm.js +56 -0
  11. package/dist/engine.js +173 -0
  12. package/dist/errors.js +146 -0
  13. package/dist/help.js +78 -0
  14. package/dist/http.js +117 -0
  15. package/dist/login.js +133 -0
  16. package/dist/resources.js +41 -0
  17. package/dist/schema.js +152 -0
  18. package/dist/schemas.js +72 -0
  19. package/dist/secrets.js +189 -0
  20. package/dist/version.js +4 -8
  21. package/package.json +4 -9
  22. package/dist/api/client.js +0 -190
  23. package/dist/api/server.js +0 -26
  24. package/dist/api/stub/db.js +0 -37
  25. package/dist/api/stub/http.js +0 -18
  26. package/dist/api/stub/index.js +0 -52
  27. package/dist/api/stub/locale.js +0 -52
  28. package/dist/api/stub/management.js +0 -409
  29. package/dist/api/stub/messages.js +0 -126
  30. package/dist/api/stub/orders.js +0 -625
  31. package/dist/api/stub/search.js +0 -78
  32. package/dist/api/stub/shopping.js +0 -673
  33. package/dist/api/stub/theme.js +0 -197
  34. package/dist/api/stub/ucp.js +0 -377
  35. package/dist/api/transport.js +0 -57
  36. package/dist/api/types.js +0 -4
  37. package/dist/assets.js +0 -100
  38. package/dist/catalog.js +0 -366
  39. package/dist/check.js +0 -187
  40. package/dist/commands.js +0 -1241
  41. package/dist/cursor.js +0 -31
  42. package/dist/files.js +0 -98
  43. package/dist/inbox/cursor.js +0 -11
  44. package/dist/inbox/fake.js +0 -95
  45. package/dist/inbox/imap.js +0 -139
  46. package/dist/inbox/mailbox.js +0 -25
  47. package/dist/inbox/message.js +0 -88
  48. package/dist/inspect.js +0 -230
  49. package/dist/languages.js +0 -49
  50. package/dist/links.js +0 -118
  51. package/dist/locales.js +0 -43
  52. package/dist/ops.js +0 -190
  53. package/dist/orderRequest.js +0 -141
  54. package/dist/output.js +0 -39
  55. package/dist/products.js +0 -151
  56. package/dist/promotions.js +0 -291
  57. package/dist/store.js +0 -379
  58. package/dist/storefront.js +0 -285
  59. package/dist/ucp/buy.js +0 -90
  60. package/dist/ucp/check.js +0 -360
  61. package/dist/ucp/client.js +0 -129
  62. package/dist/ucp/images.js +0 -62
  63. package/dist/ucp/platform.js +0 -90
  64. package/dist/ucp/shop.js +0 -357
  65. package/dist/ucp/signing.js +0 -67
  66. package/dist/web.js +0 -316
  67. package/dist/website.js +0 -390
package/README.md CHANGED
@@ -1,246 +1,126 @@
1
- # @commercelayer/agent-cli
2
- `c11r`, the Commerce Layer CLI for coding agents. An agent reads [commercelayer.ai/start.md](https://commercelayer.ai/start.md), runs this CLI, and the human ends up with a working store in a sandbox: products, prices, a market, a storefront at `<org>.sandbox.commercelayer.store`, and a test order to approve, capture and ship. No account needed until they decide to keep it.
3
- Every store speaks the [Universal Commerce Protocol](https://ucp.dev) on its own hostname, so shopping agents can discover it, browse it and buy from it. The CLI itself buys through UCP (`orders test`) and checks the store against the spec (`ucp check`), so what it verifies is what any agent would get.
4
- It is a separate tool from [`@commercelayer/cli`](https://www.npmjs.com/package/@commercelayer/cli), the CLI for humans. The package is `@commercelayer/agent-cli`; the command is `c11r` (c, eleven letters, r), short and unmistakable in an agent's transcript.
5
- ```
6
- npx -y @commercelayer/agent-cli@latest init --name "The Blue Brand" # runs c11r init
7
- ```
8
- ## How it works
9
- - **The repo is the store.** `.commercelayer/store.yml` holds the desired state (name, markets, theme, policies, and the products of a small store) and is committed; a large catalog goes in `.commercelayer/products/` as `.yml` or `.csv` files, images in `.commercelayer/img/`. `plan` shows what `apply` would change; `apply` makes the store match the files.
10
- - **The website comes with the store.** Every store has a hosted theme at its preview URL, set by a few `theme:` settings and the product data: `apply` is all it takes. A custom storefront (your own web app) is opt-in: `storefront init`, `publish` and `check`.
11
- - **JSON only.** Every command prints one JSON object. A failure exits 1 with `{ "error": { "code", "message", "hint" } }`; the hint is the fix.
12
- - **Safe to re-run.** Every command is idempotent; a failed step can be fixed and run again.
13
- - **Always current.** Run it as `npx -y @commercelayer/agent-cli@latest`, even where a `c11r` binary is installed; the note `init` writes in `AGENTS.md` and `CLAUDE.md` says so. The CLI sends `User-Agent: c11r/<version> (node/<major>)`, and an outdated one gets a `cli_outdated` entry in `warnings`, with `current`, `latest` and a `hint`.
14
- - **No secrets in git.** `init` writes the sandbox token to `.commercelayer/credentials` (`C11R_ORG`, `C11R_TOKEN`) and gitignores it. The keys of a catalog source go in the same file.
15
- ## Commands
16
- | Command | Does |
17
- | --- | --- |
18
- | `init [--name <business name>]` | Create a sandbox store, with no login. The name (the folder name when absent) names the organization; the org slug and store hostname derive from it, numbered when taken. Writes `store.yml` (a commented template) and `credentials`, plus a short `AGENTS.md` and `CLAUDE.md` section (appended to an existing file, never twice) that sends later agents in the repo to [commercelayer.ai/skills.md](https://commercelayer.ai/skills.md). Reuses the store if they exist. |
19
- | `plan` | Validate `store.yml` and the product files, and list what `apply` would create, update or delete: markets, products, images, and the store's name, policies and theme. `missing_images` lists `img/<file>` references with no file in `.commercelayer/img/` (`apply` refuses them); `unused_images` lists files there that nothing references. |
20
- | `apply [--allow-delete] [--full]` | Make the store match the files, the website included: the theme settings, and the images in `.commercelayer/img/` that the theme and products reference, uploaded when new or changed (unused ones are dropped from the store host). Refuses deletes without `--allow-delete`. Product changes go through a Commerce Layer import: `apply` waits for it, with progress as JSON lines on stderr, and prints one result. `apply_failed` lists the products Commerce Layer refused. While Commerce Layer is busy or unavailable, `apply` keeps reading (backing off on busy) and writes a `retrying` line. `apply_pending` means the import keeps going after 10 minutes, and `c11r status` follows it. An apply still running is finished before a new one starts. `--full` re-imports every product and price, after edits in the Commerce Layer dashboard. |
21
- | `catalog pull` | Read the products from the catalog source in `store.yml` (a Supabase table, a Google Sheet, a Google Merchant Center feed, or a brand's website for a demo store) into `.commercelayer/products/catalog.csv`. Then `plan` and `apply` as usual. `--currency <code>` gives a website's currency when the site does not say it. |
22
- | `storefront inspect <url>` | Read a brand's website, read-only: title, languages, theme colour, icons, `og:image`, logo candidates, the colours its CSS uses most, the currencies and countries it sells in, the platform (Shopify, WooCommerce or unknown) and whether `robots.txt` lets `catalog pull` read its products. For a demo store that looks like the site: the agent picks `theme.color` and `theme.logo` (a URL, hot-linked) from it. Writes no file. |
23
- | `storefront init [--force]` | Custom storefronts only. Write a static storefront on [drop-in.js](https://commercelayer.github.io/drop-in.js/), no build step, wired to the sandbox's sales channel. `index.html`: the config block, the products grouped by top-level category with a navigation, a minicart that slides in on add to cart, the hosted cart and checkout, and a product page at `#/p/<sku>` built from `<template id="product-template">` and the `#products` data. `styles.css`: theme tokens first (`--paper`, `--ink`, `--muted`, `--line`, `--accent`, `--on-accent`, `--radius`, `--font`), which also restyle drop-in.js's prices, buttons and links, so a dark theme is a token change; then the drop-in.js overrides, then the page. `app.js`: the market switch, the category links and the product page. Product images are the store's, at `/img/<file>` (from `.commercelayer/img/`, uploaded by `apply`); products with no image show the tile the store host draws for them, at `/tiles/<sku>.svg`. The site's own files go in `storefront/assets/`. Refuses to overwrite without the flag; with it, rewrites the three files. |
24
- | `storefront publish` | Custom storefronts only. Upload the folder at `storefront.path` (default `storefront`) to the store host, where it replaces the hosted theme, leaving out dotfiles. `/img/` on the store host is always the store's images, so files under the folder's `img/` are not served: publish lists them as `not_served`. |
25
- | `storefront check` | Custom storefronts only. Read the published storefront at the preview URL and check it: drop-in.js and its config block wired to this store, the drop-in elements, an add to cart for every product and none for unknown skus, every image loading, alt text, stylesheets after drop-in.css, the theme tokens and drop-in overrides (no dead `::part` rules), title, `lang`, viewport, description, and whether the folder changed since publish. Prints `{ status, summary, checks: [{ id, kind, status: pass\|warn\|fail, message, fix, items }], render }` and exits 1 when a check fails; warnings do not fail. `kind` is `static` today; a later rendering phase adds `render` checks and fills `render`. |
26
- | `promotions list` · `show <id>` | The store's promotions, live in Commerce Layer: id, name, status, rules, actions, uses, dates; `managed: false` for ones made in the dashboard. |
27
- | `promotions create` · `update <id>` | Create a promotion, or replace one, from a spec in YAML or JSON on stdin or `-f <file>` (see [Promotions](#promotions)). Prints the promotion and the `orders test` call that checks it. |
28
- | `promotions enable <id>` · `disable <id>` · `delete <id>` | Turn a promotion on or off, or delete it (ask the human first). |
29
- | `links list` · `show <id>` | The store's links, live in Commerce Layer: url, id, name, status, what it sells, markets, dates; `managed: false` for ones made in the dashboard. |
30
- | `links create` | Create a shareable link to a microstore (sku, skus or category) or an order's checkout, from a spec in YAML or JSON on stdin or `-f <file>` (see [Links](#links)). Prints the link, `url` first. |
31
- | `links enable <id>` · `disable <id>` · `delete <id>` | Turn a link on or off, or delete it (ask the human first). |
32
- | `orders test [--sku <id>] [--quantity <n>] [--country <CC>] [--code <discount>] [--via-cart] [--decline]` | Buy from the store over UCP as a shopping agent would: discover, search, (cart,) checkout, address, pay with the sandbox handler, read the order back. Prints the order, the `discounts` applied (promotions and codes, in minor units like the totals) and every step. |
33
- | `orders list [--status pending\|placed\|approved\|cancelled\|editing\|all] [--channel message\|link] [--payment <status>] [--fulfillment <status>] [--archived [all]] [--limit <n>]` | The store's orders, live in Commerce Layer, newest first; by default placed and approved, not archived, 25 at most (see [Orders and fulfillment](#orders-and-fulfillment)). Pending orders (from emails and messages) only with `--status pending`. |
34
- | `orders create -f <file> \| <flags> --source email:<id> [--place\|--hold] [--force]` | An order from an email or a message: the agent writes the request (customer, address, lines by sku or by the words used); the API resolves the products at catalog prices and makes it pending, or answers `needs_review` with nothing created (see [Orders from emails and messages](#orders-from-emails-and-messages)). |
35
- | `orders place <n>` | Place a pending order once the human said yes. |
36
- | `orders show <number or id>` | One order as the merchant sees it: line items, totals, addresses, payment and transactions, shipments, and `next`, what it waits for. |
37
- | `orders approve <n> [--capture]` · `capture` · `cancel` · `refund <n> [--amount <major>]` · `archive` · `unarchive` | Move an order; each prints the order after it. |
38
- | `fulfillment list [--status <status>\|all] [--limit <n>]` · `show <shipment>` | The shipments, by default the ones not shipped yet. A shipment is its number (`1001/S/001`), its id, or its order's number. |
39
- | `fulfillment hold` · `pick` · `pack` · `ready` · `ship <shipment> [--carrier] [--tracking] [--tracking-url]` · `deliver` | Move a shipment; each prints the shipment after it. |
40
- | `ops check [--since 24h\|7d\|<ISO>]` | What is new since the last check and what the policy in `.commercelayer/ops.yml` would do, without changing anything: new orders, what it would approve and capture, what it holds for the human and why, orders with no payment yet, stuck shipments (see [Running the store on a schedule](#running-the-store-on-a-schedule)). |
41
- | `ops run [--dry-run]` | Apply the policy: approve, and capture if it says so, every placed order it does not hold. Never ships, cancels or refunds. |
42
- | `ops digest [--since 24h\|7d\|<ISO>]` | A summary for the human: orders, revenue per currency, what was approved, what is held or stuck, top products, as `summary` text. |
43
- | `insights [--since 24h\|7d\|30d\|<ISO>] [--until <ISO>] [--by product\|day\|market\|country\|channel\|promotion]` | Sales over a span (default 30 days): orders, units, revenue and average order per currency, refunds and cancellations apart, top products, channels, and with `--by` one row per product, day, market, country, channel or promotion; a `summary` to read. |
44
- | `inbox check` · `list [--unseen] [--since 24h] [--limit 20]` · `pull <id>\|--unseen` · `tag <id> [--label <name>]… [--unlabel <name>]… [--move <folder>] [--seen]` | The inbox orders arrive in, over IMAP: list it, save messages and their attachments under `.commercelayer/inbox/` for the agent to read, label or move them when done. No sending: replies stay with the human. |
45
- | `ucp profile [--agent]` | The store's UCP business profile (`/.well-known/ucp`); with `--agent`, this CLI's own platform profile. |
46
- | `ucp search [<words>] [--category <name>] [--min-price <n>] [--max-price <n>] [--country <CC>] [--images]` · `ucp product <id> [--images]` | Buy for the human, step by step (see [Buying for the human](#buying-for-the-human)): the store's products with their categories, prices in major units in `price.display`, sale prices, shipping and the market the prices are for. |
47
- | `ucp checkout create --item <variant id>[:<qty>] … [--code <discount>] [--country <CC>]` | Open a checkout; `messages` say what is still missing (email, shipping address). |
48
- | `ucp checkout update <id> [--email …] [--street …] [--item <variant id>:<qty>] [--code …] [--shipping <option>]` | Give the buyer's details (flags, or YAML or JSON on stdin or `-f`), change items or codes; `ready_for_complete` once nothing is missing, with shipping in the total. |
49
- | `ucp checkout show <id>` · `cancel <id>` · `complete <id> [--decline]` | Read, cancel, or pay a checkout with the sandbox handler (after the human said yes): `order` has `label` and `permalink_url`. |
50
- | `ucp order <id>` · `ucp ask "<question>"` | Read an order the CLI placed; ask the store a question. Every `ucp` buying command takes `--store <host>` to buy from any store that speaks UCP. |
51
- | `ucp check` | Walk every UCP capability the store advertises and report what passes. Places and pays one real test order on the way (the output says which). Exits 1 on a failed check. |
52
- | `status` | The sandbox, the sales channel client id, the published storefront, what is applied, the last import job (`apply`), the pending changes (exactly what `plan` lists; a store that does not load is pending too, with the error), orders, the store's semantic search index (`search`: model, status, vectors, embedded, updated_at, error; null before its first run), the UCP profile URL. |
53
- | `claim` | Start the device flow that keeps the sandbox: a URL and a code for the human. |
54
- | `help [<command>]` | Describe the commands. `--version` prints the version and `latest_hint`, how to always run the newest. |
55
- ## store.yml
56
- `init` writes a commented template. A filled-in store looks like this:
57
- ```yaml
58
- name: Beans
59
- markets:
60
- # One market per region you sell in. countries are ISO 3166 codes.
61
- # shipping is optional: without it orders ship free as "Standard".
62
- - code: eu
63
- name: Europe
64
- countries: [IT, FR, DE, ES]
65
- currency: EUR
66
- shipping: { method: Standard, price: 4.90, days: 3-5 }
67
- - code: us
68
- name: United States
69
- countries: [US]
70
- currency: USD
71
- products:
72
- # sku is unique and stays the same when you rename a product.
73
- - sku: COFFEE-ETH-250
74
- name: Ethiopia Yirgacheffe, 250 g
75
- description: Washed, floral, bright.
76
- image_url: img/eth.jpg # .commercelayer/img/eth.jpg, or an https:// URL
77
- prices: { EUR: 14.50, USD: 16.00 }
78
- theme:
79
- # The hosted website. Every setting is optional.
80
- color: "#2f5d50" # the accent: buttons, links, the hero band; text on it is black or white by contrast
81
- logo: img/logo.svg # or an https:// URL; default: the name as a wordmark
82
- hero: img/hero.jpg # the home page's wide image; default: a band in the accent
83
- tagline: Small-batch coffee, roasted in Milan # at most 120 characters; default: "Ships to <countries>"
84
- badge: Demo # a short label next to the logo, at most 20 characters
85
- storefront:
86
- mode: hosted # default; agent-only: only UCP and the pages it needs, no catalog pages
87
- policies:
88
- # Pages shopping agents show buyers before they pay. Without them the store
89
- # serves default sandbox pages at <preview_url>/policies/<name>.
90
- privacy_policy: https://example.com/privacy
91
- terms_of_service: https://example.com/terms
92
- returns: https://example.com/returns
1
+ # c11r
2
+
3
+ The Commerce Layer CLI for coding agents. One grammar from a sandbox to a running organization:
4
+
93
5
  ```
94
- ### Promotions
95
- Promotions are live settings in Commerce Layer, not store files: the `promotions` commands create and change them directly, and nothing about them is committed. A spec, in YAML or JSON on stdin or with `-f <file>`:
96
-
97
- ```yaml
98
- id: welcome10 # the handle: lowercase letters, digits, dashes; unique
99
- name: Welcome, 10% off
100
- rules: [{ coupon: WELCOME10 }]
101
- actions: [{ percentage: 10 }]
6
+ c11r <noun> <verb> [id] [--field value ...]
102
7
  ```
103
8
 
104
- Optional: `market` (a market code; without it, every market), `starts` and `ends` (a day, or a UTC time; a bare `ends` day runs to 23:59:59Z; without them the promotion is always on), `limit` (total uses), `priority` (lowest first), `exclusive`, `enabled`.
9
+ This is the 1.0.0 line, in development on the `clean-slate` branch. It replaces every earlier command; nothing of 0.x is kept.
105
10
 
106
- - Rules, one of each kind at most, all matching: `coupon: CODE` or a list (8 to 40 letters, digits, `-`, `_`), `order_amount: { min }` (the items total), `skus: [...]` or `category: A > B`. A category becomes the skus of its products in store.yml when the command runs: after adding products to it, run `update` again.
107
- - Actions: `percentage: 10` (with `skus` or `category` to limit it), `fixed_amount: 5`, `free_shipping`, `fixed_price: { price, skus|category }`, `free_gift: { skus|category, quantity }`, `buy_x_pay_y: { x, y, cheapest_free?, skus|category }`. Actions without their own skus take the rule's. Amounts are in major units, like prices; in a store with several currencies, a promotion with an amount names its `market`. Commerce Layer gets one promotion per action.
108
- - `flex: { rules: [...] }` holds a Commerce Layer Rules Engine promotion as written (amounts in cents), in place of `rules` and `actions`. Sandboxes have no Rules Engine: `create` answers `flex_unavailable` and creates nothing.
11
+ ## The grammar
109
12
 
110
- `list` and `show` print each promotion in the normalized form plus `status` (`active`, `scheduled`, `expired`, `disabled`, `used_up`) and `uses`; that output can be edited and sent back to `update`. Promotions made outside c11r (the dashboard, the sandbox's `SANDBOX10`) are listed with `managed: false` and Commerce Layer's id: `enable`, `disable` and `delete` work on them, `update` answers `promotion_not_managed`. `percentage` is a whole number from 1 to 100; a promotion takes at most 20 codes, a free gift at most 25 SKUs. More than 10 active promotion actions without a coupon gives a `warnings` entry (Commerce Layer applies 10). `orders test --code <CODE>` proves one: its `discounts` lists what was taken off, automatic promotions with `automatic: true`, and a code that applied nothing is a `discount_code_invalid` warning in `messages`.
13
+ - Standard verbs: `list`, `get`, `create`, `update`, `delete`. Nouns with a state machine add their own verbs: `c11r markets disable eu`, `c11r orders approve 1001`, `c11r payments capture 1001`.
14
+ - A plain noun has no code in the CLI. Its fields, filters and verbs come from the API's schema, and `c11r schema <noun>` returns that schema.
15
+ - Flags are the schema's fields, in kebab or snake case: `--price-list` and `--price_list` are the same.
16
+ - On `list`, `--<field> <value>` filters by equality and `--<field>.<op> <value>` by another operator: `--placed-at.gte 2026-11-01`, `--status.in placed,approved`. `--waiting` lists what needs a decision now.
17
+ - A dotted flag sets part of an object: `--customer.email ada@example.com`. An array takes a JSON array, values separated by commas, or the flag repeated; an array of objects takes `key=value` pairs, one object per flag: `--items sku=COFFEE,quantity=2`. `--data` takes the fields as one JSON object, or `@` and a file name.
18
+ - Amounts are in major units: `--amount 12.50`.
111
19
 
112
- ### Links
113
- Links are live settings in Commerce Layer too, never store files: shareable URLs that open a hosted microstore for one sku, a set of skus or a category, or the hosted checkout for an order. A spec, in YAML or JSON on stdin or with `-f <file>`:
20
+ ## Built-in nouns
114
21
 
115
- ```yaml
116
- id: single-origins # the handle: lowercase letters, digits, dashes; unique
117
- name: Single origins
118
- for: { category: Coffee > Single origin } # or { sku: ETH }, { skus: [ETH, COL] }, { order: 1001 | cart_... }
119
- ```
22
+ | Command | What it does |
23
+ | --- | --- |
24
+ | `c11r status` | Where this machine and folder stand: the organization picked and from where, its state, plan, mode, limits, usage and what is waiting; the stored login, if any. |
25
+ | `c11r schema <noun>` | The noun's schema document: fields, filters, verbs, which verbs are destructive and which a loop may automate. |
26
+ | `c11r sandbox create [--name] [--seed]` | An anonymous sandbox. Its token is stored on this machine and never printed; the folder is linked to it. |
27
+ | `c11r sandbox get`, `reset`, `claim`, `destroy` | The sandbox's details; a fresh organization behind the same token; a claim URL for the human; the end of the sandbox. |
28
+ | `c11r loops create <name> --skill --every --allow [--max-total] [--market]` | A loop in `loops.yml`: a skill, a policy and a cadence. Also `list`, `get`, `update`, `delete`. |
29
+ | `c11r loops run <name>` | One pass of a loop. The CLI is not a daemon: a scheduler repeats the pass. |
30
+ | `c11r report sales [--from] [--to] [--by] [--interval] [--market] [--top]` | Sales in a window, from the Metrics API. A plain noun: its flags come from the API's schema. |
31
+ | `c11r auth login [--client-id] [--secret-file] [--keep-file]` | The human's own Commerce Layer login, from their Provisioning API application. See below. |
32
+ | `c11r auth logout`, `auth switch <slug> [--global]` | Forgetting the login; the organization commands act on. |
120
33
 
121
- Optional: `market` (a code or a list; without it, every market; an order link takes the order's market and none), `starts` (default now) and `ends` (default a month after `starts`; a day, or a UTC time; a bare `ends` day runs to 23:59:59Z), `params` (names to plain values, added to the URL). A category becomes the skus of its products in store.yml when the command runs; a link sells at most 100 skus. An order link opens the checkout of a draft or pending order: its number, or the id of a UCP cart or checkout (`cart_...`, `chk_...`), read back as the order's number. A placed order opens its thank-you page, so `create` refuses it. Commerce Layer opens a link in one market, so a link has a URL per market: `urls`, by market code, and `url`, the first market's. `create`, `list` and `show` print `url` and `urls` first, then the spec, `status` (`pending`, `active`, `expired`, `disabled`: the first market's; enable and disable act on every market) and `created_at`. A disabled or pending link opens Commerce Layer's not-found page; checkout takes the sandbox's manual payment (wire transfer) unless a gateway is added. A link cannot change: delete it and create it again (what `show` prints can be sent back to `create`). Links made outside c11r are listed with `managed: false` and Commerce Layer's id, and may sell `for: { sku_list: <name> }`; `enable`, `disable` and `delete` work on them.
34
+ A loop policy allows verbs: a bare verb is the skill's own noun (`approve`), `<noun>.<verb>` another Manage noun's verb on the same item (`payments.capture`). Verbs that delete or return money are never open to a policy.
122
35
 
123
- ### The products folder
124
- Beyond a handful of products, put the catalog in `.commercelayer/products/` and leave `products` out of `store.yml`. Every file in the folder is merged into the catalog by sku, in file-name order:
125
- - `<name>.yml`: a list of products in the shape above (one file per collection, or one per product, as you like).
126
- - `<name>.csv`: one product per row. The first line names the columns: `sku`, `name`, `description`, `image_url`, `category`, and one column per currency, named by its ISO 4217 code, holding the price. Quoted cells and CRLF are fine; empty cells are left out.
127
- ```csv
128
- sku,name,description,category,EUR,USD
129
- COFFEE-ETH-250,"Ethiopia Yirgacheffe, 250 g","Washed, floral, bright.",Coffee > Single origin,14.50,16.00
130
- COFFEE-COL-250,"Colombia Huila, 250 g",,Coffee > Single origin,12.00,13.50
131
- ```
132
- Errors name the file (and line, for CSV) and the sku, so one product file can be fixed without reading the rest. Moving a product between `store.yml` and a file is not a change to the store: `plan` keys everything by sku. The catalog has no stock: inventory is tracked live in Commerce Layer, and the sandbox treats every product as in stock. Prices in these files are what the store charges; a published storefront reads prices and availability live from Commerce Layer.
133
- Rules `plan` and `apply` enforce: a name, at least one market and one product; every market has `code`, `name`, `countries` and an ISO 4217 `currency`; codes and skus are unique across all files; a product has only `sku`, `name`, `description`, `image_url`, `category` and `prices`; a `category` is a path of names from the top level, separated by ` > `; an `image_url` is an `https://` URL or a path in the storefront folder such as `img/eth.jpg` (served from the store host, given to shopping agents as a full URL); every product has a price in the currency of every market; `shipping`, if present, has `method` and a non-negative `price`; policy entries are URLs. Currency, languages and tax follow from the countries.
134
- ### Languages
135
- `language:` at the top of `store.yml` is the language the products are written in (ISO 639-1, default `en`). A product's `name`, `description` and `category` can also be a map with the base language and others, and a CSV takes `name_<ll>`, `description_<ll>` and `category_<ll>` columns next to the plain ones:
136
- ```yaml
137
- language: en
138
- products:
139
- - sku: ETH
140
- name: { en: Ethiopia, it: Etiopia }
141
- category: { en: Coffee > Single origin, it: Caffè > Monorigine } # the same levels, each translated
142
- prices: { EUR: 14.50 }
143
- ```
144
- A store has at most 6 languages, the base one included. Every country gets its own pages, `/<cc>-<ll>/` (`/it-it/`), in that country's language: the product texts in it when the product has them, the base language otherwise; `/` lets the buyer choose the country when there are several. Shopping agents get the same texts over UCP, in the language they ask for. `plan` warns about a language no country of the store speaks. A store without translations plans and applies as before.
145
-
146
- ### A catalog the human edits
147
- When the products live in a tool the human edits too, `store.yml` names it and `c11r catalog pull` copies it into `.commercelayer/products/catalog.csv`, sorted by sku: same source, same file. Pull applies the product rules above before it writes, with errors naming the row in the source, and reports the skus added, changed and removed. Removed products leave the store only through `apply --allow-delete`. Every source has the same columns: `sku`, `name`, `description`, `image_url`, `category`, and one price column per market currency, named by its ISO 4217 code in lowercase (`eur`, `usd`).
148
- ```yaml
149
- catalog:
150
- source: sheets # sheets | supabase | merchant | website
151
- url: https://docs.google.com/spreadsheets/d/e/2PACX-.../pub?output=csv
152
- # table: products # supabase only
153
- # limit: 100 # website only: how many products (at most 500)
154
- ```
155
- - `sheets`: a Google Sheet, by its CSV link (File > Share > Publish to web > Comma-separated values) or by its own link when anyone with the link can view it. No key. Translations are `name_<ll>`, `description_<ll>` and `category_<ll>` columns, in sheets and Supabase tables alike.
156
- - `supabase`: a table (default `products`), read over the REST API with the project's anon key: `C11R_SUPABASE_URL=https://<project>.supabase.co` and `C11R_SUPABASE_KEY=<anon key>` in `.commercelayer/credentials`, or the environment. The key needs a row level security policy that allows select. Never use the `service_role` key. Columns `id`, `created_at` and `updated_at` are skipped.
157
- - `merchant`: a Google Merchant Center product feed URL, RSS 2.0 (XML, `g:` namespace) or TSV. `id` is the sku, `title` the name, `description`, `image_link` the image, `price` (`14.50 EUR`) the price in its currency, `product_type` the category. An id listed once per currency is one product with several prices. Other attributes, `sale_price` included, are ignored. Other languages: `catalog.translations: { it: <the Italian feed URL> }`, one feed per language, merged by `id` (`title`, `description`, `product_type`).
158
- - `website`: a brand's own website, for a demo store (the human's site, or a brand they are pitching to). `url` is the site, `limit` how many products (default 100, at most 500). Pull tries, in order, and the first that yields products wins: Shopify's public `products.json` (also `/collections/all/products.json`), WooCommerce's Store API (`/wp-json/wc/store/v1/products`), then the product pages its sitemap lists (`/products/`, `/product/`, `/p/`, `/shop/`, `/item/`) and their schema.org Product JSON-LD. One row per product: the first available variant's sku and price (or the handle in upper snake case), the title, the description as plain text (paragraphs kept, at most 2,000 characters), the first image, the product type or breadcrumb as category. Products without a price are left out, and a sku seen twice keeps the first. Prices are the site's, in the site's currency, never converted: the store's markets must sell in it (`currency_mismatch` otherwise), and when the site does not say it, `currency_unknown` asks for `--currency <code>`. Translations are not pulled yet; `source_site.languages` lists the site's languages. **Images and the logo stay on the brand's servers: `image_url` links them, nothing is copied.** Pull reads politely: `robots.txt` is honoured for every path (`robots_skipped` counts what it disallows), one request at a time and at most 2 a second, 15 s per request and 60 s in all (`budget_exhausted` when it stopped early, with what it read), as `c11r/<version> (+https://commercelayer.ai)`. It prints `strategy`, `source_site: { url, title, platform, product_count_seen, currency, languages }`, `pulled`, `variants_skipped`, `robots_skipped`, `no_price_skipped`, `duplicates_skipped`, `other_currency_skipped`, `tried: [{ strategy, result }]` and `requests`, so the agent can tell the human "I found 340 products, pulled 100".
159
- Error codes: `no_catalog_source`, `invalid_catalog`, `missing_credentials`, `catalog_unreachable`, `catalog_unauthorized`, `catalog_not_found`, `catalog_not_csv`, `catalog_error`, `empty_catalog`, plus `invalid_product` and `missing_price`; for a website `site_unreachable`, `no_products_found` (ask the human for a feed or a sheet instead), `currency_unknown` and `currency_mismatch`.
36
+ ## Login, organization tokens and secrets
160
37
 
161
- `storefront inspect <url>` reads the same way, one page and up to 3 of its stylesheets, and writes nothing: `{ url, title, site_name, description, language, languages, theme_color, icons, og_image, logo_candidates: [{ url, source: json_ld|header_img|img|header_svg, alt, width, height }], colors: [{ hex, source: theme_color|css, count, neutral }], currencies_seen, countries_seen, platform, robots: { products_allowed, paths }, next }`. An inline SVG logo has `url: null`: there is nothing to link. Errors: `missing_argument`, `site_unreachable`, `site_error`, `robots_disallowed`.
38
+ Two kinds of bearer. An **organization token** (`sbx_...` from `sandbox create`, `org_...` from `organizations create` and `organizations connect`) acts on one organization's data. The **login** is the human's own Provisioning API application, for `organizations`, `members`, `roles` and the `credentials` of a claimed organization. Each verb's schema says which it takes (`verbs[].credential`), and the CLI sends that one.
162
39
 
163
- ### The website
164
- The theme renders the home page (logo, hero with name and tagline, products by top-level category), a page per category at `/categories/<slug>/...` and per product at `/products/<sku>`, with the market from `?market=<code>`, cart and checkout. Images written `img/<file>` (`theme.logo`, `theme.hero`, a product's `image_url`) are files in `.commercelayer/img/`, served at `<preview_url>/img/<file>`: paths of letters, digits, `.`, `_`, `-` and `/`, a lowercase `png`, `jpg`, `jpeg`, `webp`, `gif`, `avif` or `svg` extension, at most 5 MB each. `apply` uploads the new and changed ones (by SHA-256) before anything else, and the store host drops the ones nothing references any more.
40
+ `c11r auth login` reads the client id and the client secret:
165
41
 
166
- One rule for images: `img/<file>` anywhere in `store.yml` (product `image_url`, `theme.logo`, `theme.hero`) is `.commercelayer/img/<file>`, uploaded by `apply`, served at `/img/<file>`, whatever is in front.
42
+ - in a terminal, typed at a prompt; the secret is not echoed;
43
+ - with no terminal, from a file: `--secret-file <path>`, or by default `~/.config/c11r/provisioning.secret`. The file holds `client_id=...` and `client_secret=...` lines (or JSON with those keys, or the secret alone, with `--client-id`). c11r reads it only when no other user can (mode 600), and deletes it once read unless `--keep-file` is given;
44
+ - with no terminal and no file, it stops at once with `secret_required`, `next_step.action` `human_step` and the dashboard page in `next_step.url`, and makes the file, mode 600, as two lines for the human to complete: `client_id=` and `client_secret=`. A file with either value missing gets the same answer, and nothing is sent to Commerce Layer. Then `c11r auth login` reads it.
167
45
 
168
- `storefront.mode: agent-only` makes a store for shopping agents only: the store host serves UCP and the pages UCP needs (checkout, order, policy pages, `/buy` permalinks, images), a one-line home page pointing to `/.well-known/ucp`, and no catalog or product pages (`/products/…` and `/categories/…` redirect to `/`, so links shared earlier land on the agent page), nor a custom storefront. Removing it, or `mode: hosted`, brings the website back on the next `apply`.
46
+ Never from a flag or the environment. c11r exchanges the pair for a two hour token at Commerce Layer and sends only that token to the c11r API; the client secret never leaves this machine. Both are kept in `credentials.json`, mode 600, and never printed. `c11r status` shows the stored login as `cli.login`, `{ client_id, email, stored_at }`, or null: read on this machine, never the secret. `auth logout` forgets the login; its last token ends by itself within two hours. Organization tokens stay.
169
47
 
170
- A custom storefront replaces the theme: `storefront.path` in `store.yml` names its folder, `storefront init` writes a drop-in.js scaffold, `storefront publish` uploads it, `storefront check` checks it. Its pages link product images at `/img/`; its own files (fonts, icons, styles) go in `assets/`, not `img/`.
48
+ `organizations create` and `organizations connect <slug>` answer an organization token once: it is stored like a sandbox token. `connect --revoke-others` also ends every other token of that organization, and asks for confirmation. With a login and no organization token for the organization named, an organization noun stops with `no_credentials` and the next step `c11r organizations connect <slug>`. `members` and `roles` with an organization token and no login are sent with that token, and the API says what is needed: a claim for a sandbox, a login for a claimed organization. `organizations` with no login stops here with `login_required`.
171
49
 
172
- Error codes: `invalid_theme`, `invalid_storefront`, `invalid_promotion`, `promotion_exists`, `promotion_not_managed`, `flex_unavailable`, `promotions_unsupported`, `invalid_link`, `link_exists`, `link_not_managed` (reserved), `links_unsupported`, `missing_images`, `asset_too_large`, `no_store`, `invalid_yaml`, `invalid_store`, `invalid_market`, `invalid_product`, `missing_price`, `deletes_need_approval`, `apply_failed`, `apply_pending`, `apply_in_progress`, `storefront_exists`, `no_storefront`, `storefront_unreachable`, `not_applied`, `not_found`, `country_not_served`, `too_many_items`, `store_unavailable`, `orders_unsupported`, `missing_credentials`, `invalid_credentials`, `inbox_unreachable`, `labels_unsupported`, `invalid_policy`, `missing_argument`, `invalid_argument`, `unknown_command`, `unauthorized`, `expired`, `api_unreachable`, `api_error`, `no_product`, `checkout_not_ready`, `ucp_error`, `ucp_profile_unreachable`, `ucp_profile_malformed`, `ucp_version_unsupported`, `ucp_no_rest_service`, `internal`.
173
- ## Orders and fulfillment
50
+ `credentials create` and `credentials token` answer a secret once. c11r writes it to a file with mode 600 and prints the file, the format and a hint, never the value: by default `~/.config/c11r/secrets/<org>/<name>.env` (`<id>-token.env` for a token), or `--out <path>`, as `KEY=value` lines or with `--format json` one JSON object. The file is checked before the request is sent; one there already is replaced only with `--yes`. A file inside a git repository that git does not ignore gets the warning `secret_not_ignored`. With `--dry-run` nothing is written and `secret_file` says where it would go. `credentials delete` removes the files c11r wrote for that credential in `~/.config/c11r/secrets/<org>/` (its secret and its tokens) and lists them in `removed_files`; `sandbox destroy` removes the whole folder of that organization and names it in `forgotten.secrets`. A file written with `--out` is not tracked and stays: the warning `out_files_not_tracked` says so, when a secret of that organization was written with `--out` on this machine, or when a delete found nothing to remove. A token file carries `CL_BASE_ENDPOINT` beside the token. `sales-calls book` answers a cancel token once: it is kept in `credentials.json` and `sales-calls cancel <booking>` sends it.
174
51
 
175
- Orders land **placed** with the payment **authorized**, not taken: the store no longer approves or captures them. The merchant, through the agent, accepts an order with `orders approve <n> --capture` (or approves now and runs `orders capture <n>` later; Commerce Layer also captures a placed order), cancels one it doesn't want before it ships (`orders cancel`, which voids an authorization or refunds a captured payment; a shipped order is refunded, not cancelled), and gives money back with `orders refund <n> [--amount <major units>]` (the last of it cancels the order). `orders archive` hides an order from the list, whatever its status; `orders list --archived` shows them. Every command prints the order after it and a `next` that says what it waits for; a move the order doesn't allow answers `invalid_transition`, with the commands it allows now in the hint.
52
+ ## Global flags
176
53
 
177
- Each order has one shipment, `upcoming` when placed. `fulfillment hold` works on a placed order; `pick`, `pack`, `ready` and `ship` need the order approved, and `on_hold`, `picking`, `packing` and `ready_to_ship` move freely between each other and on to `shipped`. An `upcoming` shipment goes only to `hold` or `pick`, as Commerce Layer refuses to ship it straight away. `ship` takes the carrier's `--carrier`, `--tracking` and `--tracking-url` (https); `deliver` follows `ship`. An API older than these routes answers `orders_unsupported` or `fulfillment_unsupported`.
54
+ | Flag | What it does |
55
+ | --- | --- |
56
+ | `--json` | JSON output. The default and the only format. |
57
+ | `--dry-run` | On a write, the CLI sends it with `?dry_run=true`: the API checks it all (state, plan, limits, Commerce Layer's current status) and answers what it would do, `would: [...]`, and nothing is written. No confirmation is needed. Reads ignore it. |
58
+ | `--yes` | Confirms a destructive verb in advance. |
59
+ | `--org <slug>` | The organization to act on, by its stored token. |
60
+ | `--live` | Live data, on a claimed organization: `?mode=live` on every request. Without it, test mode. |
61
+ | `--help` | Describes the noun or verb it follows, from its schema. |
178
62
 
179
- ### Running the store on a schedule
63
+ Destructive verbs (deletes, `orders cancel`, `payments void` and `refund`, `sandbox reset` and `destroy`) ask for confirmation in a terminal, then send `confirm=true`. With no terminal, as when an agent or a schedule runs the CLI, they never wait: without `--yes` they stop at once with `confirmation_required` and exit code 3.
180
64
 
181
- An agent can run the store on a schedule (a cron job, a CI workflow, a loop) with `ops check` and `ops run`, under a policy the human agrees to. The policy is `.commercelayer/ops.yml`, committed with the store; the CLI reads it and never sends it to the API, and `plan` checks it:
65
+ ## Which organization
182
66
 
183
- ```yaml
184
- auto_approve: true # approve placed orders (default false: every order waits for the human)
185
- auto_capture: true # take the payment when approving, and of orders the human approved (default false)
186
- review_above: 500 # hold orders above this total, in major units of the order's currency
187
- hold_countries: [US] # hold orders shipping to these countries
188
- stuck_days: 2 # a shipment picking, packing or ready to ship longer is stuck (on hold never is)
189
- transit_days: 7 # a shipment shipped longer and not delivered is stuck in transit
190
- mail_orders: # orders made from emails and messages (orders create)
191
- auto_place: false # place them at once (default false: pending until the human says yes)
192
- labels: { pending: pending, ordered: ordered, declined: declined } # what the agent labels the emails (these by default)
193
- ```
67
+ `--org`, then `C11R_ORG`, then `.c11r.json` in the project folder (found by walking up, like `.git`), then the default set by `auth switch --global`, then the stored organization when exactly one is stored (never for a login noun's `?org=`). With several stored and nothing naming one, the command stops with `org_required` and lists them.
194
68
 
195
- `ops check` remembers what it reported in `.commercelayer/ops.json` (machine state, gitignored), so the next check does not repeat an order; `--since` looks back without moving it. `ops run` approves and captures what the policy allows, one order at a time, and reports what it did, what it held and why, and what is stuck; with `--dry-run` it only says what it would do. Shipping, cancelling and refunding stay with the human. `ops digest` and `insights` summarise for the human, in plain text they can read as is.
69
+ - Tokens are only in `~/.config/c11r/credentials.json` (or under `XDG_CONFIG_HOME`), mode 600. The CLI never prints one, only its hint.
70
+ - `.c11r.json` holds only `{ "org": "<slug>" }`: no secret, safe to commit. `sandbox create`, `organizations create` and `auth switch` write it, and `organizations connect` when the folder has no link; `reset` follows the new slug; `destroy` removes it when it names the destroyed organization.
71
+ - The project folder is the one with `.c11r.json`, else the nearest with `store.yml` or `loops.yml`, else the current one. `loops.yml` goes there.
196
72
 
197
- ### Orders from emails and messages
73
+ ## Output, errors and exit codes
198
74
 
199
- Merchants get orders by email: a sentence in the body, a purchase order as PDF, a list in a sheet. The agent reads the message (the CLI saves attachments as they came; the agent opens PDFs and sheets itself) and writes the request:
75
+ Every answer is one JSON object on stdout, the API's envelope:
200
76
 
201
- ```yaml
202
- source: { kind: email, id: "<po-114@bar.example>" } # required: the same source never makes a second order
203
- customer: { email: buyer@bar.example, company: Bar Centrale, phone: "+33 1 23 45 67 89" }
204
- ship_to: { line_1: 3 rue de la Paix, city: Paris, postal_code: "75002", country: FR }
205
- items:
206
- - { sku: ETH, quantity: 6 }
207
- - { query: white mug, quantity: 2 } # the words the customer used
208
- reference: PO-2026-114 # their purchase order number
77
+ ```json
78
+ { "data": {}, "state": { "lifecycle": "sandbox", "plan": null, "mode": "test", "org": "zztest-blue-beans", "expires_at": "2026-11-17T00:00:00Z" }, "warnings": [], "next": [] }
209
79
  ```
210
80
 
211
- `orders create -f request.yml` sends it; the API finds each line in the catalog (a sku, or the store's search for the words), at the catalog's prices, never the email's. A line it is not sure of comes back in `resolution` with up to three candidates and status `needs_review`, and nothing is created: the agent asks the human which product and runs it again with `--item <sku>:<qty>`, or with `--force` to leave those lines out. Nothing is invented: a missing phone, postal code or country is `invalid_argument` with the `path`. The order is made **pending**, shown to the human, and placed on their yes with `orders place <n>`; `mail_orders.auto_place: true` places it at once, `--hold` keeps it pending anyway. `orders cancel` drops a pending order (it stays archived).
212
-
213
- `inbox` reads the mailbox when the agent has no mail connector of its own. The connection goes in `.commercelayer/credentials` (gitignored), written by the human: `C11R_IMAP_HOST`, `C11R_IMAP_USER`, `C11R_IMAP_PASSWORD` (for Gmail an [app password](https://myaccount.google.com/apppasswords), never the account password), and optionally `C11R_IMAP_PORT` (993) and `C11R_IMAP_FOLDER` (INBOX); no output ever shows the password. `inbox pull` writes `.commercelayer/inbox/<id>/` (gitignored): `message.json`, `body.txt` and `attachments/`; `.commercelayer/inbox.json` remembers what was pulled. `inbox tag` labels the email (Gmail labels, else IMAP keywords, else `--move` to a folder) and `--seen` marks it read.
214
-
215
- ## The store speaks UCP
216
- Each store is a UCP business at `https://<org>.sandbox.commercelayer.store` (live stores: `<slug>.commercelayer.store`), implementing release **2026-08-25** over REST:
217
- | What | Where |
218
- | --- | --- |
219
- | Business profile | `GET /.well-known/ucp` (`Cache-Control: public, max-age=300`, ES256 public key in `keys[]`) |
220
- | Shopping service `dev.ucp.shopping` | `/ucp/v1`: `POST /catalog/search`, `POST /catalog/lookup`, `POST /catalog/product`, `POST /carts`, `GET/PUT /carts/{id}`, `POST /carts/{id}/cancel`, `POST /checkout-sessions`, `GET/PUT /checkout-sessions/{id}`, `POST /checkout-sessions/{id}/complete`, `POST /checkout-sessions/{id}/cancel`, `GET /orders/{id}` |
221
- | Common service `dev.ucp.common` | `/ucp/v1`: `POST /ask` |
222
- | Extensions | `dev.ucp.shopping.fulfillment` (one shipping method per market, from `shipping` in `store.yml`), `dev.ucp.shopping.discount` (the sandbox accepts code `SANDBOX10`, 10% off the items), `dev.ucp.shopping.buyer_consent`, `dev.ucp.shopping.policy.return` (30-day returns) |
223
- | Identity linking `dev.ucp.common.identity_linking` | `GET /.well-known/oauth-authorization-server`, `GET /oauth/authorize` (PKCE S256; the sandbox approves at once), `POST /oauth/token` (`authorization_code`, `client_credentials`), `POST /oauth/revoke`, `GET /oauth/jwks`. Scopes `dev.ucp.shopping.order:read`, `dev.ucp.shopping.order:manage`. |
224
- | Permalink `dev.ucp.shopping.permalink` | `GET /buy/{sku}:{qty}[,...]?context/postal_code=…&discounts/codes/0=…` → `303` to the cart |
225
- | Payment handler `ai.commercelayer.sandbox` | id `cl_sandbox`, `card` instruments, credential `{ "type": "sandbox_token", "token": "sbx_tok_success" \| "sbx_tok_declined", "expiry": … }`. No money moves. |
226
- | Pages | `/policies/{privacy,terms,returns}` (defaults when `store.yml` has no `policies`), `/carts/{id}`, `/checkout-sessions/{id}` (`continue_url`), `/orders/{id}` (`permalink_url`), and the published storefront at `/`. |
227
- Transport rules the store enforces: `UCP-Agent` (an HTTPS platform profile URL) and `Request-Id` on every request; `Idempotency-Key` on every state change, replayed when the body matches and `409` when it does not; `Content-Digest` verified when present. Every service response carries `Content-Digest`, `Signature-Input` and `Signature` (ES256 over `@status`, `content-digest`, `content-type`, key `kid` from the profile). Orders are private: readable by the platform that placed them or with a buyer token carrying an order scope; anything else gets `401` with `WWW-Authenticate: Bearer`. Catalog ids are the skus (`product.id` is `prod_<sku>`, `variant.id` is the sku); amounts are integers in minor units.
228
- The CLI is a UCP **platform**: its profile is served at `https://commercelayer.ai/ucp/agent-cli/profile.json` (`ucp profile --agent` prints it) and goes in `UCP-Agent` on every call. The store negotiates the capability intersection with it. `orders test` is the buyer; `ucp check` runs 30-odd checks across profile, transport rules, catalog, cart, discount, checkout, fulfillment, consent, payment, signatures, idempotency, order, identity linking, ask and permalink.
229
- ### Buying for the human
230
-
231
- An agent can buy from any UCP store on behalf of its human, one step per command, talking to the human in between: `ucp search` to show the products (words in any order; `--category`, which includes the subcategories, and `--min-price`/`--max-price` in major units narrow it; the `messages` the store sends, such as its categories when nothing matched, are passed on), `ucp checkout create --item <variant id>:<qty>` for what they picked (the `variants[].id` search lists; the sku on Commerce Layer stores), `ucp checkout update <id>` with the email and shipping address they give (never invented), then, once they said yes to the total, `ucp checkout complete <id>`, and the order's `label` and `permalink_url` back to them. Money comes as `{ amount, currency, major, display }`: `amount` in minor units as UCP has it, `display` for the human. The store is `--store <hostname or origin>`, or this folder's store when there is one; checkout and order ids are remembered with their store (in `~/.local/state/c11r/ucp.json`), so later steps need no `--store`. Paying needs the store's sandbox handler (`ai.commercelayer.sandbox`, test payments only): any store without it (every store taking real payments) answers `payment_handler_unsupported`, and the human pays at the checkout's `continue_url`. Responses are signature-checked like `orders test`: `signature` is `verified`, `invalid`, `unknown_key` or `unsigned`.
232
-
233
- `--images` is for agents that can read image files and want to look at or describe a product: it saves each product's `image_url` under `<tmpdir>/c11r/images/<store host>/` (the first 24, in parallel, 5 s each, a file already there reused) and adds `image_file`, the absolute path, or `image_error`. Showing a thumbnail in a chat needs only `image_url`, which checkout and order line items carry too.
81
+ The CLI's own answers (help, loops.yml) have the same shape, with `state` null. Every error, the API's or the CLI's, is:
82
+
83
+ ```json
84
+ {
85
+ "error": {
86
+ "code": "state_required",
87
+ "message": "Live mode needs a claimed organization; this is a sandbox.",
88
+ "required_state": "claimed",
89
+ "required_plan": null,
90
+ "missing": null,
91
+ "path": null,
92
+ "next_step": { "action": "claim", "command": "c11r sandbox claim", "url": null }
93
+ },
94
+ "state": { "lifecycle": "sandbox", "plan": null, "mode": "test", "org": "zztest-blue-beans", "expires_at": "2026-11-17T00:00:00Z" }
95
+ }
96
+ ```
234
97
 
235
- ## Requirements
98
+ `code` is stable. `missing` is `{ noun, why }`, `path` the flag or field at fault, `next_step.action` one of `create_sandbox`, `claim`, `upgrade`, `contact_sales`, `create_dependency`, `fix_request`, `retry_later`, `human_step`, `none`. `human_step` is something only the human can do, such as saving a secret to a file, with a page in `url` when there is one. The CLI may add `did_you_mean` and `details`.
236
99
 
237
- Node 20 or later. Runtime dependencies: `yaml`, and for `inbox` only `imapflow` and `postal-mime`, loaded when an inbox command runs (about 9 MB more on install).
100
+ | Exit | Meaning | Codes |
101
+ | --- | --- | --- |
102
+ | 0 | done | |
103
+ | 1 | unexpected failure: a bug in c11r, or a secret the disk refused after the API answered it | `secret_not_written` |
104
+ | 2 | the command is not valid | `unknown_noun`, `unknown_verb`, `unknown_field`, `invalid_value`, `missing_value`, `missing_argument`, `missing_id`, `unexpected_argument`, `no_fields`, `org_required`, `invalid_config`, `secret_required`, `insecure_file` |
105
+ | 3 | a destructive verb needs confirmation | `confirmation_required`, `confirmation_declined` |
106
+ | 4 | the organization needs another state or plan, has expired, or reached a limit | `state_required`, `plan_required`, `expired`, `limit_reached` |
107
+ | 5 | no such resource | `not_found`, `claim_not_found` |
108
+ | 6 | the API or Commerce Layer refused the request as it is | `invalid_argument`, `invalid_transition`, `conflict`, `dependency_missing`, `commercelayer_refused`, `policy_forbidden`, `not_supported`, `forbidden` |
109
+ | 7 | the API did not answer, is busy, or failed | `api_unreachable`, `rate_limited`, `commercelayer_busy`, `commercelayer_unavailable`, `api_error`, `schema_unavailable`, `invalid_response` |
110
+ | 8 | a wrong or revoked token, no token stored for the organization named, or no login | `unauthorized`, `no_credentials`, `login_required` |
238
111
 
239
- ## Links and license
112
+ `rate_limited` is retried twice with a short backoff, for reads and writes: nothing was processed. `commercelayer_busy` and `commercelayer_unavailable` are retried for reads only; a write that may have reached Commerce Layer fails with its next step. The one exception is `sandbox create`: it sends a fresh `Idempotency-Key`, and after a 5xx or a lost connection it repeats the create with the same key, so the API answers the sandbox it made and its token is stored. Each retry waits as long as the API says (`retry_after` seconds in the error, or the `Retry-After` header), at most 30 seconds; without it, 1 then 3 seconds. When the API asks for longer than 30 seconds, the CLI does not wait: the error goes to the agent, with `retry_after`, to plan around. Each retry writes a line to stderr.
240
113
 
241
- - [commercelayer.ai/start.md](https://commercelayer.ai/start.md): the entry point an agent reads.
242
- - [Universal Commerce Protocol](https://ucp.dev): the protocol every store speaks.
243
- - [`@commercelayer/cli`](https://www.npmjs.com/package/@commercelayer/cli): the Commerce Layer CLI for humans.
244
- - [Commerce Layer](https://commercelayer.io): the commerce API behind the stores.
114
+ ## Environment
245
115
 
246
- MIT. Copyright Commerce Layer.
116
+ | Variable | What it does |
117
+ | --- | --- |
118
+ | `C11R_API_URL` | The API's base URL, `https://api.commercelayer.ai/v1` by default; `http://localhost:8787/v1` for wrangler dev. A URL without `/v1` gets it. |
119
+ | `C11R_ORG` | The organization, when `--org` is not given. |
120
+ | `C11R_TOKEN` | An organization token to use as is, whatever is stored: for CI. |
121
+ | `C11R_PROVISIONING_TOKEN` | A ready login token, used as is: for CI. Never a client secret. |
122
+ | `C11R_AUTH_URL` | Commerce Layer's token endpoint, `https://auth.commercelayer.io/oauth/token` by default: for tests. |
123
+ | `C11R_TEST` | `1` marks a sandbox as a test organization, like a name starting `zztest`. |
124
+ | `C11R_SCHEMA_CACHE` | `0` turns off the 10 minute schema cache in `~/.cache/c11r`. |
125
+ | `C11R_RETRY_CAP_S` | The longest the CLI waits before one retry, 30 by default. |
126
+ | `C11R_TIMEOUT_MS` | How long a request may take, 30000 by default (sandbox create waits longer). |