@commercelayer/agent-cli 1.0.0-dev.12 → 1.0.0-dev.14
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 +27 -2
- package/dist/argv.js +4 -3
- package/dist/builtins/extensions.js +33 -3
- package/dist/builtins/index.js +4 -2
- package/dist/builtins/loops.js +4 -2
- package/dist/builtins/shop.js +504 -0
- package/dist/builtins/skills.js +349 -0
- package/dist/config.js +1 -1
- package/dist/confirm.js +10 -3
- package/dist/engine.js +5 -2
- package/dist/errors.js +6 -0
- package/dist/help.js +1 -1
- package/dist/ucp.js +207 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ This is the 1.0.0 line, in development on the `clean-slate` branch. It replaces
|
|
|
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
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 a JSON array,
|
|
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`. `--data` takes the fields as one JSON object, or `@` and a file name.
|
|
19
19
|
- Amounts are in major units: `--amount 12.50`.
|
|
20
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.
|
|
21
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.
|
|
@@ -35,6 +35,9 @@ This is the 1.0.0 line, in development on the `clean-slate` branch. It replaces
|
|
|
35
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. |
|
|
36
36
|
| `c11r auth login [--client-id] [--secret-file] [--keep-file]` | The human's own Commerce Layer login, from their Provisioning API application. See below. |
|
|
37
37
|
| `c11r auth logout`, `auth switch <slug> [--global]` | Forgetting the login; 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. |
|
|
38
41
|
|
|
39
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.
|
|
40
43
|
|
|
@@ -83,7 +86,7 @@ Records that share a key (two shipping methods with the same name in one market,
|
|
|
83
86
|
|
|
84
87
|
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.
|
|
85
88
|
|
|
86
|
-
`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. It never replaces a different file without `--yes`. Without `--out`, the snippet stays in the JSON answer.
|
|
89
|
+
`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.
|
|
87
90
|
|
|
88
91
|
`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.
|
|
89
92
|
|
|
@@ -128,6 +131,27 @@ The CLI's own answers (help, loops.yml) have the same shape, with `state` null.
|
|
|
128
131
|
|
|
129
132
|
`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.
|
|
130
133
|
|
|
134
|
+
## Shop
|
|
135
|
+
|
|
136
|
+
`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.
|
|
137
|
+
|
|
138
|
+
- 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`.
|
|
139
|
+
- `profile`, `search`, `get` and `ask` only read. `get` and `ask` take the variant or the question as the bare value after the store.
|
|
140
|
+
- `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.
|
|
141
|
+
- `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.
|
|
142
|
+
- 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.
|
|
143
|
+
- With `--dry-run`, checkout answers what it would send and order the total it would pay: nothing is sent.
|
|
144
|
+
|
|
145
|
+
## Skills
|
|
146
|
+
|
|
147
|
+
`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.
|
|
148
|
+
|
|
149
|
+
- It reads the index (`start.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.
|
|
150
|
+
- 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.
|
|
151
|
+
- `--except <skill>` (repeated) leaves a skill out, and later syncs remember it; `--include <skill>` takes it back.
|
|
152
|
+
- 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`).
|
|
153
|
+
- The answer has `added`, `updated` (from and to), `removed`, `edited` and how many are `unchanged`. 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.
|
|
154
|
+
|
|
131
155
|
## Environment
|
|
132
156
|
|
|
133
157
|
| Variable | What it does |
|
|
@@ -139,5 +163,6 @@ The CLI's own answers (help, loops.yml) have the same shape, with `state` null.
|
|
|
139
163
|
| `C11R_AUTH_URL` | Commerce Layer's token endpoint, `https://auth.commercelayer.io/oauth/token` by default: for tests. |
|
|
140
164
|
| `C11R_TEST` | `1` marks a sandbox as a test organization, like a name starting `zztest`. |
|
|
141
165
|
| `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. |
|
|
166
|
+
| `C11R_SKILLS_URL` | The skills index, for a test site on localhost only: `https://commercelayer.ai/start.md` otherwise. |
|
|
142
167
|
| `C11R_RETRY_CAP_S` | The longest the CLI waits before one retry, 30 by default. |
|
|
143
168
|
| `C11R_TIMEOUT_MS` | How long a request may take, 30000 by default (sandbox create waits longer). |
|
package/dist/argv.js
CHANGED
|
@@ -181,9 +181,10 @@ export function coerce(key, spec, raw) {
|
|
|
181
181
|
// One object per flag (--items sku=A,quantity=2, repeated), or scalars separated by commas.
|
|
182
182
|
if (item === 'object')
|
|
183
183
|
return [coerce(key, { type: 'object', properties: spec.properties }, raw)];
|
|
184
|
-
//
|
|
185
|
-
// Several are the flag repeated, or a JSON array. Enums, numbers and
|
|
186
|
-
|
|
184
|
+
// A name or text is one value per flag, in a filter or not: a name may have a comma
|
|
185
|
+
// ("Invoice, 60 days"). Several are the flag repeated, or a JSON array. Enums, numbers and
|
|
186
|
+
// dates still split on commas.
|
|
187
|
+
if (item === 'string' || item === 'ref')
|
|
187
188
|
return [raw];
|
|
188
189
|
return raw
|
|
189
190
|
.split(',')
|
|
@@ -11,7 +11,7 @@ import crypto from 'node:crypto';
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import zlib from 'node:zlib';
|
|
14
|
-
import { credentialsFile, hintOf, readLink, storeBooking, storedBooking, storeOrg, writeLink } from '../config.js';
|
|
14
|
+
import { credentialsFile, hintOf, projectDir, readLink, storeBooking, storedBooking, storeOrg, writeLink } from '../config.js';
|
|
15
15
|
import { confirm, terminal } from '../confirm.js';
|
|
16
16
|
import { fail } from '../errors.js';
|
|
17
17
|
import { apiUrl } from '../http.js';
|
|
@@ -272,19 +272,49 @@ export const exports = {
|
|
|
272
272
|
return { ...r, data: { ...rest, file, bytes: bytes.length, unzipped: zipped && !file.endsWith('.gz') } };
|
|
273
273
|
},
|
|
274
274
|
};
|
|
275
|
+
/** Where a path really lands: symlinks followed, dangling ones too, and the missing rest as written. */
|
|
276
|
+
function realPath(p, hops = 0) {
|
|
277
|
+
let st = null;
|
|
278
|
+
try {
|
|
279
|
+
st = fs.lstatSync(p);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
st = null;
|
|
283
|
+
}
|
|
284
|
+
if (st?.isSymbolicLink() && hops < 40)
|
|
285
|
+
return realPath(path.resolve(realPath(path.dirname(p), hops + 1), fs.readlinkSync(p)), hops + 1);
|
|
286
|
+
if (st)
|
|
287
|
+
return fs.realpathSync(p);
|
|
288
|
+
const parent = path.dirname(p);
|
|
289
|
+
return parent === p ? p : path.join(realPath(parent, hops), path.basename(p));
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* --out resolved from here, refused (invalid_value on out) when it lands outside the project folder:
|
|
293
|
+
* by .., by an absolute path, or by a symlink on the way (the file itself or a folder above it).
|
|
294
|
+
*/
|
|
295
|
+
function insideProject(out) {
|
|
296
|
+
const file = path.resolve(out);
|
|
297
|
+
const root = fs.realpathSync(projectDir());
|
|
298
|
+
const target = realPath(file);
|
|
299
|
+
const rel = path.relative(root, target);
|
|
300
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
301
|
+
fail('invalid_value', `--out ${out} is outside the project folder ${root}: c11r writes only inside it.`, { path: 'out', details: { project: root, resolved: target } });
|
|
302
|
+
}
|
|
303
|
+
return file;
|
|
304
|
+
}
|
|
275
305
|
// scaffold (46): drop-in --out writes the snippet as one HTML file, its head part and its body part
|
|
276
306
|
// each under a comment saying where it goes. It never replaces a different file without --yes.
|
|
277
307
|
export const scaffold = {
|
|
278
308
|
flags: {
|
|
279
309
|
'drop-in': {
|
|
280
|
-
out: { type: 'string', to: 'local', description: 'Write the snippet to this HTML file: the part for the page\'s <head>, then the part for its <body>, each under a comment saying where it goes. A different file there is replaced only with --yes.' },
|
|
310
|
+
out: { type: 'string', to: 'local', description: 'Write the snippet to this HTML file: the part for the page\'s <head>, then the part for its <body>, each under a comment saying where it goes. It must be inside the project folder. A different file there is replaced only with --yes.' },
|
|
281
311
|
},
|
|
282
312
|
},
|
|
283
313
|
async run(run) {
|
|
284
314
|
const out = run.parsed.values.out;
|
|
285
315
|
if (run.verb.name !== 'drop-in' || !out)
|
|
286
316
|
return undefined;
|
|
287
|
-
const file =
|
|
317
|
+
const file = insideProject(out);
|
|
288
318
|
// Before anything is sent: a different file there stays without --yes.
|
|
289
319
|
const before = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
290
320
|
const r = await run.send(resourceRequest('scaffold', run.verb, undefined, run.parsed.values));
|
package/dist/builtins/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// The nouns with code of their own. Each brings a schema document in the API's format, so help,
|
|
2
2
|
// validation and confirmation are the engine's. report is a plain noun: the API's schema drives
|
|
3
|
-
// it.
|
|
3
|
+
// it.
|
|
4
4
|
import { auth } from './auth.js';
|
|
5
5
|
import { loops } from './loops.js';
|
|
6
6
|
import { sandbox, status } from './sandbox.js';
|
|
7
|
-
|
|
7
|
+
import { shop } from './shop.js';
|
|
8
|
+
import { skills } from './skills.js';
|
|
9
|
+
export const builtins = { sandbox, status, loops, auth, shop, skills };
|
|
8
10
|
export { extensions } from './extensions.js';
|
package/dist/builtins/loops.js
CHANGED
|
@@ -21,7 +21,7 @@ const policyFlags = (required) => [
|
|
|
21
21
|
{ name: 'skill', type: 'string', required, description: 'The skill the loop runs, as in manage-orders or report-sales: one a noun\'s schema names in loop.skill or loop.skills.' },
|
|
22
22
|
{ name: 'every', type: 'string', required, description: 'How often a pass should run, as in 15m, 2h or 1d. The scheduler reads it; the CLI does not wait.' },
|
|
23
23
|
{ name: 'allow', type: 'array', description: 'A verb the policy allows, repeated: approve, payments.capture. Only verbs the noun\'s schema lists in loop.verbs. Without it the loop only reports what is waiting.' },
|
|
24
|
-
{ name: 'max_total', type: 'array', description: 'The largest order total acted on, in major units: 500 for every currency, or EUR=500, repeated
|
|
24
|
+
{ name: 'max_total', type: 'array', description: 'The largest order total acted on, in major units: 500 for every currency, or EUR=500 per currency, the flag repeated or separated by commas. Above it, the item waits for the human.' },
|
|
25
25
|
{ name: 'market', type: 'array', description: 'A market code the loop acts in, repeated. Items in other markets wait. Without it, every market.' },
|
|
26
26
|
];
|
|
27
27
|
export const loops = {
|
|
@@ -147,7 +147,9 @@ async function write(run, name, file) {
|
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
/** 500, or EUR=500 per currency. */
|
|
150
|
-
function maxTotal(
|
|
150
|
+
function maxTotal(given) {
|
|
151
|
+
// Amounts and currency codes hold no comma: EUR=500,USD=550 is two, as is the flag repeated.
|
|
152
|
+
const items = given.flatMap((i) => i.split(',')).map((i) => i.trim()).filter(Boolean);
|
|
151
153
|
const plain = items.filter((i) => !i.includes('='));
|
|
152
154
|
if (plain.length && plain.length !== items.length)
|
|
153
155
|
fail('invalid_value', '--max-total takes one amount, or one CURRENCY=amount per currency, not both.', { path: 'max_total' });
|