@commercelayer/agent-cli 1.0.0-dev.2 → 1.0.0-dev.21
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.
- package/README.md +83 -11
- package/dist/argv.js +23 -5
- package/dist/builtins/auth.js +21 -3
- package/dist/builtins/extensions.js +329 -5
- package/dist/builtins/index.js +4 -2
- package/dist/builtins/loops.js +68 -31
- package/dist/builtins/sandbox.js +45 -15
- package/dist/builtins/shop.js +504 -0
- package/dist/builtins/skills.js +355 -0
- package/dist/config.js +25 -8
- package/dist/confirm.js +17 -6
- package/dist/engine.js +192 -28
- package/dist/errors.js +6 -0
- package/dist/help.js +36 -6
- package/dist/http.js +10 -0
- package/dist/inputs.js +21 -1
- package/dist/project.js +185 -0
- package/dist/schema.js +15 -4
- package/dist/schemas.js +82 -8
- package/dist/secrets.js +25 -7
- package/dist/storefront.js +280 -0
- package/dist/ucp.js +207 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,12 @@ This is the 1.0.0 line, in development on the `clean-slate` branch. It replaces
|
|
|
14
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
15
|
- Flags are the schema's fields, in kebab or snake case: `--price-list` and `--price_list` are the same.
|
|
16
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
|
|
17
|
+
- A ref flag, or one the schema marks `nullable`, given `null` or an empty value sends `null` and takes it off: `c11r customers update ada@example.com --customer-group null` sends `customer_group: null`, `c11r orders update 1001 --coupon-code null` sends `coupon_code: null`. A ref the schema marks `nullable: false`, and any other field, sends the text.
|
|
18
|
+
- A dotted flag sets part of an object: `--customer.email ada@example.com`. An `in` filter takes several values as the flag repeated or a JSON array; on enums, numbers and dates also separated by commas, while a name or text is one value even with a comma in it (`--tax-calculator.in "UK VAT, manual"`), and a list that has one is sent as a JSON array. Any other array takes the flag repeated or a JSON array, and the same rule holds: enums, numbers and dates also separated by commas, a name or text one value per flag (`--payment-settings "Invoice, 60 days"`); an array of objects takes `key=value` pairs, one object per flag: `--items sku=COFFEE,quantity=2 --items sku=TEA,quantity=1`. Each array flag's help has a `repeat` line with its own form. `--data` takes the fields as one JSON object, or `@` and a file name.
|
|
18
19
|
- Amounts are in major units: `--amount 12.50`.
|
|
19
20
|
- A secret field the schema marks `secret_input` (provider credentials) has no flag of its own: `--credentials-file <file>` reads it, as JSON or `KEY=value` lines, from a plain file only its owner can read (mode 600). Without the file, a terminal asks for it without echo; with neither, the command stops with `secret_required`. It is refused as a flag value or in `--data`, and never printed.
|
|
20
|
-
- A
|
|
21
|
+
- A field with documented values the API does not enforce (`known`: webhook topics) takes any value. One outside them is sent anyway, with the warning `unknown_value` and `did_you_mean`. Help lists up to 20 of them, and past that says how many and that `c11r schema <noun>` has them all.
|
|
22
|
+
- A document field the schema marks `file_input` (price rules, rule filters) also reads from a JSON file: `--file <file>`, or `--<field>-file` when a verb has more than one. Where the verb takes `--format csv` (`imports create`), the file goes as CSV text with `--format csv`, or when its name ends in `.csv` and no format is given; otherwise it is JSON.
|
|
21
23
|
- A verb whose schema lists `id_alternative` takes the id or those flags in its place: `c11r prices get pr_1`, or `c11r prices get --sku BEANS-1KG --price-list eur`. Both at once, or half the flags, stop before sending.
|
|
22
24
|
|
|
23
25
|
## Built-in nouns
|
|
@@ -26,15 +28,18 @@ This is the 1.0.0 line, in development on the `clean-slate` branch. It replaces
|
|
|
26
28
|
| --- | --- |
|
|
27
29
|
| `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. |
|
|
28
30
|
| `c11r schema <noun>` | The noun's schema document: fields, filters, verbs, which verbs are destructive and which a loop may automate. |
|
|
29
|
-
| `c11r sandbox create [--name] [--seed]` | An anonymous sandbox. Its token is stored on this machine and never printed; the folder is linked to it. |
|
|
31
|
+
| `c11r sandbox create [--name] [--seed [coffee\|tshirts\|games\|custom]] [--catalogue <file>]` | An anonymous sandbox. Its token is stored on this machine and never printed; the folder is linked to it. With `--seed`, a demo shop of that type with its storefront on, and the shop link in `data.shop_url` and `next`. See below. |
|
|
30
32
|
| `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. |
|
|
31
|
-
| `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`. |
|
|
33
|
+
| `c11r loops create <name> --skill --every [--allow] [--max-total] [--market]` | A loop in `loops.yml`: a skill, a policy and a cadence; with no `--allow` it only reports what is waiting. A policy that allows a verb with no `--max-total` or no `--market` is saved with a `no_limit` warning naming what is unbounded. Also `list`, `get`, `update`, `delete`. |
|
|
32
34
|
| `c11r loops run <name>` | One pass of a loop. The CLI is not a daemon: a scheduler repeats the pass. |
|
|
33
35
|
| `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. |
|
|
34
36
|
| `c11r auth login [--client-id] [--secret-file] [--keep-file]` | The human's own Commerce Layer login, from their Provisioning API application. See below. |
|
|
35
|
-
| `c11r auth logout`, `auth switch <slug> [--global]` | Forgetting the login; the organization commands act on. |
|
|
37
|
+
| `c11r auth logout [--live --org <slug>]`, `auth switch <slug> [--global]` | Forgetting the login, or with `--live` revoking the API's live credential for an organization; the organization commands act on. |
|
|
38
|
+
| `c11r shop profile\|search\|get\|ask <store>` | Reading a store that speaks UCP, as a shopping agent: what it offers, its catalog, a variant, a question. No organization. See below. |
|
|
39
|
+
| `c11r skills sync` | Local copies of the Commerce Layer skills in `.claude/skills`, from the index at commercelayer.ai, listed in `AGENTS.md`. No organization. See below. |
|
|
40
|
+
| `c11r shop checkout <store> [<checkout>]`, `shop order <store> <checkout>` | Buying there for the human: their details after their yes on what is shared, then the payment after their yes on the total, with the sandbox test handler only. |
|
|
36
41
|
|
|
37
|
-
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.
|
|
42
|
+
A loop runs a skill a Manage or Report noun's schema names in `loop.skill`, or in `loop.skills` for a noun with several (`manage-orders`, `manage-stock`, `report-sales`, `report-pending`, ...); a report loop allows no verb and only reports. create and update refuse any other skill, and any verb that loop does not list, with the allowed values. 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.
|
|
38
43
|
|
|
39
44
|
## Login, organization tokens and secrets
|
|
40
45
|
|
|
@@ -50,7 +55,7 @@ Never from a flag or the environment. c11r exchanges the pair for a two hour tok
|
|
|
50
55
|
|
|
51
56
|
`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`.
|
|
52
57
|
|
|
53
|
-
`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. Each file records the record it belongs to. A `delete` of a record whose noun answers secrets (`credentials`, `webhooks`) removes the files c11r wrote for it in `~/.config/c11r/secrets/<org>/` (a credential's secret and tokens, a webhook's shared secret), found by the id, name or code given, 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
|
|
58
|
+
`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; a record with no name, such as a gift card, `gift-card-<id>.env` after the id it is given), 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. Each file records the record it belongs to. A `delete` of a record whose noun answers secrets (`credentials`, `webhooks`) removes the files c11r wrote for it in `~/.config/c11r/secrets/<org>/` (a credential's secret and tokens, a webhook's shared secret), found by the id, name or code given, 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 on the delete of the record whose secret went to `--out` on this machine, and on `sandbox destroy` of an organization that had one. Any other delete has no such warning. 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.
|
|
54
59
|
|
|
55
60
|
## Global flags
|
|
56
61
|
|
|
@@ -60,10 +65,25 @@ Never from a flag or the environment. c11r exchanges the pair for a two hour tok
|
|
|
60
65
|
| `--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. |
|
|
61
66
|
| `--yes` | Confirms a destructive verb in advance. |
|
|
62
67
|
| `--org <slug>` | The organization to act on, by its stored token. |
|
|
63
|
-
| `--live` | Live data, on a claimed organization: `?mode=live
|
|
68
|
+
| `--live` | Live data, on a claimed organization named with `--org` (or `C11R_ORG`), with the human's login: `?mode=live&org=` on every request. Without it, test mode. See below. |
|
|
64
69
|
| `--help` | Describes the noun or verb it follows, from its schema. |
|
|
65
70
|
|
|
66
|
-
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.
|
|
71
|
+
Destructive verbs (deletes, `orders cancel`, `payments void` and `refund`, `sandbox reset` and `destroy`), and any verb the schema marks `confirm`, 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. Its `next_step` is `{ "action": "confirm", "command": "c11r markets delete eu --yes" }`: the same command with `--yes`, and the action is the guard. The agent asks the human first, and runs the command only on their yes, never on its own. The API's own `confirmation_required` works the same way; when it names no command, the CLI fills in this one with `--yes`.
|
|
72
|
+
|
|
73
|
+
## Live mode
|
|
74
|
+
|
|
75
|
+
`--live` acts on a claimed organization's live data. It sends the human's login and names the organization with `?org=`: from `--org`, or `C11R_ORG`, never from the folder's link or the default, which are a sandbox's. Without one it is `missing_value` before any call, with the command and `--org ORG_SLUG --live`; in a linked folder, the message and the command name the linked organization instead of `ORG_SLUG`.
|
|
76
|
+
|
|
77
|
+
- Without a login, nothing is checked on this machine: the request goes with the organization token this machine holds for the organization named, or with no token when it holds none (never `C11R_TOKEN`), and the API's answer (`login_required`, or `state_required` for an organization not claimed) comes through as it is. So do `live_unavailable` (a plan without live), `forbidden` (the login is not an active admin of it) and `not_supported` for a verb that comes with a later step of go-live.
|
|
78
|
+
- A verb that never runs live (sandbox, plans, sales calls, `auth login`, `auth switch`, `shop`) stops on this machine with `state_required`.
|
|
79
|
+
- `c11r events writes --live --org <slug>` (`--since 2026-11-12T00:00:00Z`, an ISO time, `--noun`, `--cursor`, `--limit`) is the audit of live writes, and runs only in live: without `--live` it is `missing_value` with the full command, before any call.
|
|
80
|
+
- `c11r status --live --org <slug>` is the organization's live status, with `live_credential` and the warning `live_credential_held` while the API holds one.
|
|
81
|
+
- `c11r auth logout --live --org <slug>` revokes that credential in Commerce Layer (`DELETE /v1/auth/live`): `{ org, revoked, credential }`. The login stays on this machine. `--dry-run` says what it would revoke.
|
|
82
|
+
- A verb that needs a yes is confirmed by the API in live, with a sentence naming the action, the amount and the mode (`Refunds EUR 1.00 on order N, in LIVE mode, real money.`): the CLI sends it without `confirm=true` and shows that `confirmation_required` as it is, with the API's next command, or asks it with those words in a terminal. `--yes` sends `confirm=true` on every live write, `payments capture` included.
|
|
83
|
+
- A verb that answers a secret (`credentials create`) writes it in live only to the file the human chose with `--out`: without it, `missing_value` with the command and `--out FILE`. Never to c11r's secrets folder, never printed. A live delete leaves the test mode's secret files of the same organization alone.
|
|
84
|
+
- `store.yml`, `.c11r/export.json` and the drift notes are of an organization in a mode: a live write marks a live `store.yml` behind, never a test one, and the other way round.
|
|
85
|
+
- `c11r organizations disconnect <slug>` (confirmed) has the API revoke its credentials and the organization tokens; this machine then forgets the token, its secret files and a `.c11r.json` naming it, as `sandbox destroy` does, and answers `forgotten`.
|
|
86
|
+
- A secret read from a file (`--credentials-file`) is never printed: an answer or an error that holds it, a dry run's included, shows `[secret]` in its place. No live key is stored on this machine.
|
|
67
87
|
|
|
68
88
|
## Which organization
|
|
69
89
|
|
|
@@ -73,6 +93,36 @@ Destructive verbs (deletes, `orders cancel`, `payments void` and `refund`, `sand
|
|
|
73
93
|
- `.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.
|
|
74
94
|
- 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.
|
|
75
95
|
|
|
96
|
+
## The store's setup in files
|
|
97
|
+
|
|
98
|
+
`c11r config export` writes the organization's setup into the project folder: `store.yml` (the Configure nouns, refs by key, no ids, no secrets; its first key, `meta`, names the organization, the mode (`test`, or `live` with `--live`) and the time) and `price_lists/<code>.csv` (in a row, an empty `compare_at_amount` means equal to `amount`, and one below `amount` is refused). It records what it wrote in `.c11r/export.json`, and never overwrites a file another organization or mode exported, nor a file changed since the last export (or one no export wrote) without `--yes`: the answer lists them. A price list file the organization no longer has, unchanged since, goes. `c11r config plan` sends the files and answers what `config apply` would change; `apply` makes the organization match them, and with `--prune` (confirmed) also deletes what the files lack. While imports run, `apply` answers `status: running` with each noun's state, `retry_after` and the run's token, `run`; `--wait` applies again with that token and the same files until done or failed, and the done answer counts everything the run wrote. The `next` an unfinished apply suggests carries `--run <token> --wait`. `plan` lists in `needs` the new records the files can't make alone (a webhook without its `callback_url`); `apply` skips them and warns `needs_input` with the command that creates each.
|
|
99
|
+
|
|
100
|
+
Records that share a key (two shipping methods with the same name in one market, say) can't be matched to the files. `export` and `plan` list them in `ambiguous` (an empty list when there are none), each with the command that makes its key differ, and `export` puts the first one at the top of `next`. `apply` refuses with `conflict` and the same list in `error.details.ambiguous`, before it writes anything.
|
|
101
|
+
|
|
102
|
+
After a Configure write that went through (create, update, delete or a domain verb), in a folder whose `store.yml` names the same organization, the answer warns `config_drift` and `.c11r/drift` notes the write; `config plan` repeats the warning, and `config export` clears it.
|
|
103
|
+
|
|
104
|
+
`c11r sandbox create --seed` loads a demo shop: `coffee` (also `--seed` alone, sent as `true`), `tshirts` or `games`, six products each, or `custom`, the person's own from `--catalogue shop.yml`. Each has the markets `eu` and `us`, shipping, a manual payment setting and the storefront on. `sandbox reset --seed <type>` takes the same values, and `--seed false` reseeds with nothing. The catalogue file is YAML:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
name: Fern & Clay
|
|
108
|
+
products:
|
|
109
|
+
- name: Speckled mug
|
|
110
|
+
description: Stoneware, 350 ml.
|
|
111
|
+
prices: { EUR: 18, USD: 20 }
|
|
112
|
+
code: FC-MUG
|
|
113
|
+
look:
|
|
114
|
+
color: "#7a4b2a"
|
|
115
|
+
font: rounded
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`name` has 1 to 40 characters; `products` 1 to 24, each with a unique `name` (1 to 80), an optional `description` (up to 500), `prices` in EUR and USD (above 0, at most 100000, two decimals at most; a quoted amount such as "9.50" is taken too) and an optional unique `code` (2 to 24 capitals, digits and dashes). `look` takes `color`, `background`, `font`, `corners`, `tagline` and `badge`, as `scaffold storefront` does. The CLI checks the file before anything is sent: a refusal is `invalid_value` with the field in `path`, as the API names it (`catalogue.products[1].prices.USD`, counted from 0), and the file and line in `details`. `--catalogue` goes only with `--seed custom`, and `--seed custom` needs it. When the answer has a store, `data.shop_url` is its link and the first `next` is `c11r scaffold storefront`.
|
|
119
|
+
|
|
120
|
+
`c11r scaffold storefront` sets the shop's look: `--name` (the shop's name in its title and header, 1 to 40 characters after trimming; the organization's name and slug stay), `--color` (`#rrggbb`), `--background` (`#rgb` or `#rrggbb`), `--font classic|modern|rounded|mono`, `--corners round|square`, `--tagline` (up to 120 characters), `--badge` (up to 20), `--logo`, `--hero`, and `--product-image SKU=FILE`, once per sku. An image is a local file or an https URL; a local file is read here and sent as `{ type, content_base64, sha256 }`. It must be svg, png, jpg or webp by its name and its first bytes, at most 512 KB, and one call carries at most 2 MB of files. An SVG the store would refuse is refused here, with the reason and the way out: a DOCTYPE or entity, script or embedded content (`<script>`, `<foreignObject>` and the like), an `on...` attribute, a script URL, a link or `url()` other than `#id` or a `data:image/...;base64` image, or an `@import`. `null` (or an empty value) takes a field off: `--logo null`, `--product-image SKU=null`. These checks run before anything is sent. The answer has the shop link in `data.shop_url` and in `next`. `config export` does not carry the look.
|
|
121
|
+
|
|
122
|
+
`c11r scaffold drop-in --out <file>` writes the drop-in.js snippet as one HTML file: the part for the page's `<head>`, then the part for its `<body>`, each under a comment saying where it goes. The file must be inside the project folder: a path that leads out of it, by `..`, an absolute path or a symlink, is refused with `invalid_value` on `out` before anything is sent. It never replaces a different file without `--yes`. Without `--out`, the snippet stays in the JSON answer.
|
|
123
|
+
|
|
124
|
+
`c11r exports get <id> --out <file>` downloads a completed export while its link lasts (5 minutes), unzipped unless the name ends in `.gz`; `--wait` reads the job again until it completes.
|
|
125
|
+
|
|
76
126
|
## Output, errors and exit codes
|
|
77
127
|
|
|
78
128
|
Every answer is one JSON object on stdout, the API's envelope:
|
|
@@ -98,7 +148,7 @@ The CLI's own answers (help, loops.yml) have the same shape, with `state` null.
|
|
|
98
148
|
}
|
|
99
149
|
```
|
|
100
150
|
|
|
101
|
-
`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`.
|
|
151
|
+
`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`, `confirm`, `none`. `confirm` comes with every `confirmation_required`: its command carries `--yes`, run only on the human's yes. `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`.
|
|
102
152
|
|
|
103
153
|
| Exit | Meaning | Codes |
|
|
104
154
|
| --- | --- | --- |
|
|
@@ -114,6 +164,27 @@ The CLI's own answers (help, loops.yml) have the same shape, with `state` null.
|
|
|
114
164
|
|
|
115
165
|
`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.
|
|
116
166
|
|
|
167
|
+
## Shop
|
|
168
|
+
|
|
169
|
+
`c11r shop` buys for the human from any store that speaks UCP. The CLI talks to the store directly, with no organization and no call to the API. `<store>` is the store's host (`shop.example.com`) or an https URL on it.
|
|
170
|
+
|
|
171
|
+
- Every call carries `UCP-Agent: profile="https://commercelayer.ai/ucp/agent-cli/profile.json"`, a `Request-Id`, a `Content-Digest` with a body, and an `Idempotency-Key` on a write. The CLI holds no keys and signs no request. The store's answers are checked against the keys its `/.well-known/ucp` publishes: a digest that does not match, or a signature that does not verify, is `invalid_response` and the answer is not used. An unsigned answer warns `answer_unsigned`. A store that wants signed requests is `not_supported`.
|
|
172
|
+
- `profile`, `search`, `get` and `ask` only read. `get` and `ask` take the variant or the question as the bare value after the store.
|
|
173
|
+
- `checkout` opens a checkout with `--item ID=QTY` (repeated) and the buyer's `--email`, `--first-name`, `--last-name`, `--phone` and `--address.street`, `--address.line-2`, `--address.locality`, `--address.postal-code`, `--address.region`, `--address.country`, all but the line 2 and the region required. Each is the human's, never invented: a missing one is `missing_value` with a human step. Before any of them is sent, `confirmation_required` shows every value and the host (`Sharing with shop.example.com: Ada Lovelace, ada@example.com, ...`); the agent asks the human and, on their yes, runs it again with `--yes`. `checkout <store> <checkout>` changes one (items, the buyer, the address, `--shipping <option>`), or with nothing to change reads it again. The answer has the totals with shipping and, when it is ready, `next` is the order.
|
|
174
|
+
- `order <store> <checkout>` shows the total and asks the same way, then pays with the sandbox test handler (`ai.commercelayer.sandbox`), where no money moves, and answers the order. On a checkout already paid it answers the order and pays nothing. A checkout that is not ready is `invalid_transition`: when the store's messages name the fields it still needs, `next_step` is the command with their flags (`--phone PHONE`), and when they do not, it is a human step at the store's checkout page; a declined payment is `store_refused` with the store's words.
|
|
175
|
+
- The CLI has no flag for card data. A store without the sandbox handler is `payment_handler_unsupported` before anything of the human's is sent, with the store's URL for the human to buy there in person.
|
|
176
|
+
- With `--dry-run`, checkout answers what it would send and order the total it would pay: nothing is sent.
|
|
177
|
+
|
|
178
|
+
## Skills
|
|
179
|
+
|
|
180
|
+
`c11r skills sync` keeps copies of the skills in the project folder, where coding agents load them: `.claude/skills/<name>/SKILL.md`, one per skill as https://commercelayer.ai serves it, and in `AGENTS.md` a block between `<!-- c11r skills: start -->` and `<!-- c11r skills: end -->` listing them, for agents that read that file. Nothing outside the markers changes; without an `AGENTS.md`, sync creates one holding only the block, and says so with `agents_md: "created"`. The project folder is the one already holding the copies, else the one with `.c11r.json`, `store.yml` or `loops.yml`, else the git repository's root, else the current folder. It needs no organization and calls only commercelayer.ai, not the API.
|
|
181
|
+
|
|
182
|
+
- It reads the list of skills (`https://commercelayer.ai/skills.md`) with the ETag of the last sync, and fetches only the skills whose version changed. A skill the index dropped is removed. `.claude/skills/.c11r-skills.json` records what sync wrote, with each file's sha256; a folder sync did not write is never touched.
|
|
183
|
+
- Every copy's sha256 is checked on each run, also when nothing new is in the index. A copy that is not what sync wrote is an edit: it is kept, neither replaced nor removed, and listed in `edited` with a `skill_edited` warning. `--yes`, on the human's yes, replaces or removes it.
|
|
184
|
+
- `--except <skill>` (repeated) leaves a skill out, and later syncs remember it; `--include <skill>` takes it back.
|
|
185
|
+
- A file whose name or version is not what the index says is not used (`skill_invalid`); a link off `commercelayer.ai/skills/` is skipped (`skill_skipped`).
|
|
186
|
+
- The answer has `added`, `updated` (from and to) and `removed`, each as `{ "count": 3, "skills": [...] }`; past 10 a list is only its `count` unless `--verbose`. `edited` lists every edited copy, always, and `unchanged` is how many are. With `--dry-run` it reads only the index and writes nothing. When the site cannot be reached it is `site_unavailable` (exit 7), and the copies stay as they are.
|
|
187
|
+
|
|
117
188
|
## Environment
|
|
118
189
|
|
|
119
190
|
| Variable | What it does |
|
|
@@ -124,6 +195,7 @@ The CLI's own answers (help, loops.yml) have the same shape, with `state` null.
|
|
|
124
195
|
| `C11R_PROVISIONING_TOKEN` | A ready login token, used as is: for CI. Never a client secret. |
|
|
125
196
|
| `C11R_AUTH_URL` | Commerce Layer's token endpoint, `https://auth.commercelayer.io/oauth/token` by default: for tests. |
|
|
126
197
|
| `C11R_TEST` | `1` marks a sandbox as a test organization, like a name starting `zztest`. |
|
|
127
|
-
| `C11R_SCHEMA_CACHE` | `0` turns off the 10 minute schema cache in `~/.cache/c11r`. |
|
|
198
|
+
| `C11R_SCHEMA_CACHE` | `0` turns off the 10 minute schema cache in `~/.cache/c11r`. The cache records the `C11R-Schema` hash it was read under; an answer with another hash drops it, and a command the API refused on the old schema runs once more on the fresh one (without the header, after `confirmation_required`, `unknown_field` or `unknown_verb`). The second run asks what the fresh schema asks: it never adds `confirm=true` by itself. |
|
|
199
|
+
| `C11R_SKILLS_URL` | The skills index, for a test site on localhost only: `https://commercelayer.ai/skills.md` otherwise. |
|
|
128
200
|
| `C11R_RETRY_CAP_S` | The longest the CLI waits before one retry, 30 by default. |
|
|
129
201
|
| `C11R_TIMEOUT_MS` | How long a request may take, 30000 by default (sandbox create waits longer). |
|
package/dist/argv.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// verb's flags, which say whether a flag takes a value, what type it is and where it goes.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { closest, fail, fixWith } from './errors.js';
|
|
6
|
+
import { clearable } from './schema.js';
|
|
6
7
|
const BOOLEAN_GLOBALS = {
|
|
7
8
|
'--json': 'json',
|
|
8
9
|
'--dry-run': 'dryRun',
|
|
@@ -60,7 +61,11 @@ export function parseFields(noun, verb, rest) {
|
|
|
60
61
|
id = a;
|
|
61
62
|
continue;
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
+
// A verb with no id (stock set): say which flags it does take, as --sku for the sku.
|
|
65
|
+
const sent = Object.entries(verb.flags).filter(([, f]) => f.to !== 'local');
|
|
66
|
+
const required = sent.filter(([, f]) => f.required);
|
|
67
|
+
const takes = (required.length ? required : sent).map(([k]) => toFlag(k));
|
|
68
|
+
fail('unexpected_argument', `${cmd} does not take ${a}${verb.id ? `: it already has the id ${id}` : takes.length ? `: it takes no id, only flags (${takes.join(', ')})` : ': it takes no id, only flags'}. A value goes after its flag.`, { next_step: fixWith(`${cmd} --help`) });
|
|
64
69
|
}
|
|
65
70
|
const [flag, inline] = splitInline(a);
|
|
66
71
|
const key = toKey(flag);
|
|
@@ -91,6 +96,10 @@ export function parseFields(noun, verb, rest) {
|
|
|
91
96
|
if (spec.type === 'boolean' && !sub.length) {
|
|
92
97
|
raw = next === 'true' || next === 'false' ? rest[++i] : 'true';
|
|
93
98
|
}
|
|
99
|
+
else if (spec.bare && !sub.length && (next === undefined || /^--[a-z]/i.test(next))) {
|
|
100
|
+
values[top] = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
94
103
|
else {
|
|
95
104
|
if (next === undefined || /^--[a-z]/i.test(next))
|
|
96
105
|
fail('missing_value', `${toFlag(key)} needs a value.`, { path: key });
|
|
@@ -107,6 +116,11 @@ export function parseFields(noun, verb, rest) {
|
|
|
107
116
|
target[sub[sub.length - 1]] = part in (spec.properties ?? {}) ? coerce(key, spec.properties[part], raw) : raw;
|
|
108
117
|
continue;
|
|
109
118
|
}
|
|
119
|
+
// A flag that may stand alone takes true and false as values too (--seed false).
|
|
120
|
+
if (spec.bare && !sub.length && (raw === 'true' || raw === 'false')) {
|
|
121
|
+
values[top] = raw === 'true';
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
110
124
|
const value = coerce(key, spec, raw);
|
|
111
125
|
values[top] = spec.type === 'array' ? [...(values[top] ?? []), ...value] : value;
|
|
112
126
|
}
|
|
@@ -126,6 +140,9 @@ export function coerce(key, spec, raw) {
|
|
|
126
140
|
if ((spec.type === 'enum' || (spec.type === 'array' && spec.items === 'enum')) && !spec.values?.length) {
|
|
127
141
|
fail('invalid_response', `The API's schema gives ${toFlag(key)} as an enum with no values, so c11r cannot check ${JSON.stringify(raw)}. It was not sent.`, { path: key, next_step: { action: 'none' } });
|
|
128
142
|
}
|
|
143
|
+
// null, or an empty value, clears what the schema lets null clear (--customer-group null, --coupon-code "").
|
|
144
|
+
if (clearable(spec) && (raw === 'null' || raw.trim() === ''))
|
|
145
|
+
return null;
|
|
129
146
|
switch (spec.type) {
|
|
130
147
|
case 'string':
|
|
131
148
|
case 'ref':
|
|
@@ -173,9 +190,10 @@ export function coerce(key, spec, raw) {
|
|
|
173
190
|
// One object per flag (--items sku=A,quantity=2, repeated), or scalars separated by commas.
|
|
174
191
|
if (item === 'object')
|
|
175
192
|
return [coerce(key, { type: 'object', properties: spec.properties }, raw)];
|
|
176
|
-
//
|
|
177
|
-
// Several are the flag repeated, or a JSON array. Enums, numbers and
|
|
178
|
-
|
|
193
|
+
// A name or text is one value per flag, in a filter or not: a name may have a comma
|
|
194
|
+
// ("Invoice, 60 days"). Several are the flag repeated, or a JSON array. Enums, numbers and
|
|
195
|
+
// dates still split on commas.
|
|
196
|
+
if (item === 'string' || item === 'ref')
|
|
179
197
|
return [raw];
|
|
180
198
|
return raw
|
|
181
199
|
.split(',')
|
|
@@ -206,7 +224,7 @@ function json(raw, invalid) {
|
|
|
206
224
|
}
|
|
207
225
|
/**
|
|
208
226
|
* sku=A,quantity=2 as { sku: 'A', quantity: 2 }, each part typed by the schema's properties.
|
|
209
|
-
* A part the schema does not type stays text: a
|
|
227
|
+
* A part the schema does not type stays text: a zip code or a phone is never made a number.
|
|
210
228
|
*/
|
|
211
229
|
function pairs(key, raw, props, invalid) {
|
|
212
230
|
const out = {};
|
package/dist/builtins/auth.js
CHANGED
|
@@ -14,7 +14,7 @@ import { credentialsFile, readLink, setDefaultOrg, storedLogin, storedOrgs, stor
|
|
|
14
14
|
import { terminal } from '../confirm.js';
|
|
15
15
|
import { closest, errorBody, CliError, fail } from '../errors.js';
|
|
16
16
|
import { envelope, request } from '../http.js';
|
|
17
|
-
import { loginToken, mint, PROVISIONING_URL, readSecretFile, secretFileDefault, writeSecretTemplate } from '../login.js';
|
|
17
|
+
import { hasLogin, loginToken, mint, PROVISIONING_URL, readSecretFile, secretFileDefault, writeSecretTemplate } from '../login.js';
|
|
18
18
|
import { parseSchema } from '../schema.js';
|
|
19
19
|
export const auth = {
|
|
20
20
|
schema: parseSchema({
|
|
@@ -30,6 +30,7 @@ export const auth = {
|
|
|
30
30
|
path: '/v1/auth/login',
|
|
31
31
|
state: 'none',
|
|
32
32
|
credential: 'none',
|
|
33
|
+
live: false,
|
|
33
34
|
body: [
|
|
34
35
|
{ name: 'client_id', type: 'string', description: 'The client id of the Provisioning API application. Not a secret. Also read from the secret file.' },
|
|
35
36
|
{ name: 'secret_file', type: 'string', description: `A file with the client secret, alone or as client_secret=..., readable only by its owner (mode 600). By default ${'~/.config/c11r/provisioning.secret'} when there is no terminal.` },
|
|
@@ -37,13 +38,21 @@ export const auth = {
|
|
|
37
38
|
],
|
|
38
39
|
effect: 'Logs in with the human\'s Provisioning API application: mints a token at Commerce Layer, checks it with the API, keeps the client id and secret in credentials.json (mode 600). The API receives only the token.',
|
|
39
40
|
},
|
|
40
|
-
{
|
|
41
|
+
{
|
|
42
|
+
verb: 'logout',
|
|
43
|
+
method: 'POST',
|
|
44
|
+
path: '/v1/auth/logout',
|
|
45
|
+
state: 'none',
|
|
46
|
+
credential: 'none',
|
|
47
|
+
effect: 'Forgets the stored login on this machine. Its last token ends by itself within two hours. Organization tokens stay. With --live --org, instead revokes the API\'s own live credential for that organization in Commerce Layer, with the login: the login stays on this machine.',
|
|
48
|
+
},
|
|
41
49
|
{
|
|
42
50
|
verb: 'switch',
|
|
43
51
|
method: 'POST',
|
|
44
52
|
path: '/v1/auth/switch/{slug}',
|
|
45
53
|
state: 'none',
|
|
46
54
|
credential: 'none',
|
|
55
|
+
live: false,
|
|
47
56
|
body: [{ name: 'global', type: 'boolean', description: 'Sets the default for this machine, used when no --org, C11R_ORG or .c11r.json names one; without it, links this project folder.' }],
|
|
48
57
|
effect: 'Picks the organization commands act on: links this project folder to it (.c11r.json), or with --global sets the default for this machine.',
|
|
49
58
|
},
|
|
@@ -54,7 +63,7 @@ export const auth = {
|
|
|
54
63
|
case 'login':
|
|
55
64
|
return login(run);
|
|
56
65
|
case 'logout':
|
|
57
|
-
return logout();
|
|
66
|
+
return run.globals.live ? logoutLive(run) : logout();
|
|
58
67
|
case 'switch':
|
|
59
68
|
return switchOrg(run);
|
|
60
69
|
}
|
|
@@ -132,6 +141,15 @@ async function login(run) {
|
|
|
132
141
|
next: who.next?.length ? who.next : [{ command: 'c11r organizations list', why: 'The organizations this login reaches.' }],
|
|
133
142
|
};
|
|
134
143
|
}
|
|
144
|
+
/** The API's live credential for the organization, revoked in Commerce Layer: DELETE /v1/auth/live?org=. */
|
|
145
|
+
async function logoutLive(run) {
|
|
146
|
+
// The path names live already: the request carries ?org= and not ?mode=live.
|
|
147
|
+
run.ctx.live = false;
|
|
148
|
+
// Revoking the live credential is the login's: never an organization token.
|
|
149
|
+
if (!hasLogin())
|
|
150
|
+
run.ctx.token = null;
|
|
151
|
+
return run.send({ method: 'DELETE', path: '/auth/live' });
|
|
152
|
+
}
|
|
135
153
|
function logout() {
|
|
136
154
|
const l = storedLogin();
|
|
137
155
|
if (!l)
|