@commercelayer/agent-cli 1.0.0-dev.22 → 1.0.0-dev.23
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 +5 -2
- package/dist/builtins/shop.js +48 -3
- package/dist/errors.js +4 -0
- package/dist/ucp.js +27 -6
- 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 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
|
+
- 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"`). On `markets update` and `stores update` the payment settings are the whole list: `markets update eu --payment-settings "Wire transfer" --payment-settings Stripe` replaces the market's settings with those two, so adding one means naming the ones it has too; 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.
|
|
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.
|
|
@@ -174,7 +174,10 @@ The CLI's own answers (help, loops.yml) have the same shape, with `state` null.
|
|
|
174
174
|
- `profile`, `search`, `get` and `ask` only read. `get` and `ask` take the variant or the question as the bare value after the store.
|
|
175
175
|
- `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.
|
|
176
176
|
- `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.
|
|
177
|
-
- 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
|
|
177
|
+
- 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 on the shop's site. That includes a live shop taking Stripe's `com.stripe.payments`: Stripe wallet tokens come from agents Stripe has enrolled, and this CLI is not one yet. A shop that offers no UCP checkout (a live shop without Stripe ready keeps its catalog) is `not_supported` for checkout; `shop profile` says either with `can_pay: false` and the warning `cannot_pay_here`, not an error.
|
|
178
|
+
- A checkout the store marks `payment_unavailable` (its market takes no payment from agents) answers with the warning `cannot_pay_here` and no next step, and `shop order` refuses it with `payment_handler_unsupported` before anything is completed.
|
|
179
|
+
- Each checkout's own `ucp.payment_handlers`, the handlers its market takes, decides over the profile's union: without the sandbox handler there, `shop order` refuses before anything is completed. A checkout paid whose order the store could not place (`order_not_placed`) is refused with a human step every time it is run, and nothing offers to pay it again until the human has reviewed it: the store keeps the checkout `incomplete` with that note until an update, the buyer's review, clears it.
|
|
180
|
+
- A payment the store has taken and not yet confirmed (`complete_in_progress`) answers `payment_in_progress`, exit 7, with `retry_later` and the same `shop order` command. Run again, it reads the checkout first and never completes it a second time: it answers the order once the store has settled it. While a payment is under way, `shop checkout` takes no changes to it (`payment_in_progress`, nothing sent). Two runs at once are safe too: the store answers a second complete with the first payment's state, and the CLI says `payment_in_progress` again.
|
|
178
181
|
- With `--dry-run`, checkout answers what it would send and order the total it would pay: nothing is sent.
|
|
179
182
|
|
|
180
183
|
## Skills
|
package/dist/builtins/shop.js
CHANGED
|
@@ -13,7 +13,7 @@ import { confirm } from '../confirm.js';
|
|
|
13
13
|
import { fail } from '../errors.js';
|
|
14
14
|
import { envelope } from '../http.js';
|
|
15
15
|
import { parseSchema } from '../schema.js';
|
|
16
|
-
import { call, discover, handlersOf, minorOf, money, mustPay, originOf, sandboxPayment } from '../ucp.js';
|
|
16
|
+
import { call, cannotPay, discover, SANDBOX_HANDLER, handlersOf, minorOf, money, mustPay, originOf, sandboxPayment } from '../ucp.js';
|
|
17
17
|
const ADDRESS = [
|
|
18
18
|
{ name: 'street', type: 'string', description: 'Street and number: Via Roma 1.' },
|
|
19
19
|
{ name: 'line_2', type: 'string', description: 'Apartment, floor, care of.' },
|
|
@@ -151,7 +151,7 @@ function profile(store) {
|
|
|
151
151
|
signed: store.keys.length > 0,
|
|
152
152
|
}, {
|
|
153
153
|
next: [{ command: cmd(store, 'search', '--query WORDS'), why: 'what the store sells' }],
|
|
154
|
-
warnings: can_pay ? [] : [{ code: 'cannot_pay_here', message:
|
|
154
|
+
warnings: can_pay ? [] : [{ code: 'cannot_pay_here', message: cannotPay(store).message }],
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
// ---------- reading
|
|
@@ -352,7 +352,31 @@ function checkoutView(ck, store) {
|
|
|
352
352
|
expires_at: ck.expires_at,
|
|
353
353
|
});
|
|
354
354
|
}
|
|
355
|
+
const hasCode = (ck, code) => (ck.messages ?? []).some((x) => x.code === code);
|
|
356
|
+
/**
|
|
357
|
+
* Why this checkout takes no payment from this CLI: the store's payment_unavailable (its market
|
|
358
|
+
* takes no payment from agents), or its own handler list, the authority over the profile's union,
|
|
359
|
+
* without the sandbox handler.
|
|
360
|
+
*/
|
|
361
|
+
function unpayable(ck, store) {
|
|
362
|
+
const m = (ck.messages ?? []).find((x) => x.code === 'payment_unavailable');
|
|
363
|
+
if (m)
|
|
364
|
+
return `${store.host} can't take a payment from agents for checkout ${ck.id}${m.content ? `: ${m.content}` : '.'} The human buys on the shop's site.`;
|
|
365
|
+
const own = ck.ucp?.payment_handlers;
|
|
366
|
+
if (own && typeof own === 'object' && !Object.keys(own).includes(SANDBOX_HANDLER)) {
|
|
367
|
+
return cannotPay(store, { id: ck.id, handlers: Object.keys(own) })?.message ?? null;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
/** The store took the payment and Commerce Layer would not place the order: nothing is paid again until the human has reviewed the checkout. */
|
|
372
|
+
function notPlaced(store, ck) {
|
|
373
|
+
const m = (ck.messages ?? []).find((x) => x.code === 'order_not_placed');
|
|
374
|
+
fail('order_not_placed', `${store.host} took the payment for checkout ${ck.id}, but could not place the order${m?.content ? `: ${m.content}` : '.'} Don't pay again until the human has reviewed the checkout, at its page.`, { details: { status: ck.status, messages: ck.messages ?? [] }, next_step: { action: 'human_step', url: ck.continue_url ?? store.origin } }, 'rejected');
|
|
375
|
+
}
|
|
355
376
|
function nextOf(ck, store) {
|
|
377
|
+
// Nothing to pay here, or paid already with the order not placed: no command offers to pay.
|
|
378
|
+
if (unpayable(ck, store) || hasCode(ck, 'order_not_placed'))
|
|
379
|
+
return [];
|
|
356
380
|
if (ck.status === 'ready_for_complete')
|
|
357
381
|
return [{ command: cmd(store, 'order', ck.id), why: `pay ${ck.totals ? money(ck.totals.find((t) => t.type === 'total')?.amount ?? 0, ck.currency).display : 'the total'} with the sandbox test handler, after the human's yes on it` }];
|
|
358
382
|
if (ck.status === 'completed' && ck.order?.id)
|
|
@@ -368,6 +392,9 @@ async function checkout(run, store) {
|
|
|
368
392
|
const lines = items(v.item);
|
|
369
393
|
const { buyer, address } = given(v);
|
|
370
394
|
const current = id ? (await call(store, 'GET', `${store.endpoint}/checkout-sessions/${encodeURIComponent(id)}`, undefined, `checkout ${id}`)).body : null;
|
|
395
|
+
// A payment under way takes no changes (the store answers not_allowed): read it again with order.
|
|
396
|
+
if (current?.status === 'complete_in_progress' && (lines.length || Object.keys(buyer).length || Object.keys(address).length || v.shipping))
|
|
397
|
+
inProgress(store, id);
|
|
371
398
|
if (current && (current.status === 'completed' || current.status === 'canceled')) {
|
|
372
399
|
if (lines.length || Object.keys(buyer).length || Object.keys(address).length || v.shipping) {
|
|
373
400
|
fail('invalid_transition', `Checkout ${id} is ${current.status}: it takes no changes.`, { details: { status: current.status }, next_step: current.status === 'completed' ? { action: 'fix_request', command: cmd(store, 'order', id) } : { action: 'fix_request', command: cmd(store, 'checkout', `--item VARIANT=QTY ${HUMAN_FLAGS}`) } }, 'rejected');
|
|
@@ -450,7 +477,8 @@ async function checkout(run, store) {
|
|
|
450
477
|
c = await call(store, 'PUT', `${store.endpoint}/checkout-sessions/${encodeURIComponent(c.body.id)}`, again, `checkout ${c.body.id}`);
|
|
451
478
|
}
|
|
452
479
|
const ck = c.body;
|
|
453
|
-
|
|
480
|
+
const why = unpayable(ck, store);
|
|
481
|
+
return answer(checkoutView(ck, store), c, { next: nextOf(ck, store), warnings: why ? [{ code: 'cannot_pay_here', message: why }] : [] });
|
|
454
482
|
}
|
|
455
483
|
// ---------- order
|
|
456
484
|
function orderView(o, store, checkoutId) {
|
|
@@ -482,7 +510,16 @@ async function order(run, store) {
|
|
|
482
510
|
return placed(store, ck, false);
|
|
483
511
|
if (ck.status === 'canceled')
|
|
484
512
|
fail('invalid_transition', `Checkout ${id} was canceled: it cannot be paid.`, { details: { status: ck.status }, next_step: { action: 'fix_request', command: cmd(store, 'checkout', `--item VARIANT=QTY ${HUMAN_FLAGS}`) } }, 'rejected');
|
|
513
|
+
// A payment under way is read again, never completed a second time.
|
|
514
|
+
if (ck.status === 'complete_in_progress')
|
|
515
|
+
inProgress(store, id);
|
|
516
|
+
// Paid, and the order not placed: not paid again until the human's review (an update) clears the store's note.
|
|
517
|
+
if (hasCode(ck, 'order_not_placed'))
|
|
518
|
+
notPlaced(store, ck);
|
|
485
519
|
mustPay(store, { continue_url: ck.continue_url ?? null });
|
|
520
|
+
const why = unpayable(ck, store);
|
|
521
|
+
if (why)
|
|
522
|
+
fail('payment_handler_unsupported', why, { details: { status: ck.status, messages: ck.messages ?? [] }, next_step: { action: 'human_step', url: ck.continue_url ?? store.origin } }, 'rejected');
|
|
486
523
|
if (ck.status !== 'ready_for_complete') {
|
|
487
524
|
const said = (ck.messages ?? []).map((m) => m.content).filter(Boolean).join(' ');
|
|
488
525
|
fail('invalid_transition', `Checkout ${id} is ${ck.status}, not ready to pay${said ? `: ${said}` : '.'}`, { details: { status: ck.status, messages: ck.messages ?? [] }, next_step: stillNeeds(ck, store) ? { action: 'fix_request', command: stillNeeds(ck, store) } : { action: 'human_step', url: ck.continue_url ?? null } }, 'rejected');
|
|
@@ -495,10 +532,18 @@ async function order(run, store) {
|
|
|
495
532
|
await confirm(`Pay checkout ${id}`, run.line, { yes: run.globals.yes, prompter: run.prompter, sentence, details: { store: store.host, checkout: id, total } });
|
|
496
533
|
const c = await call(store, 'POST', `${store.endpoint}/checkout-sessions/${encodeURIComponent(id)}/complete`, sandboxBody(), `checkout ${id}`);
|
|
497
534
|
const done = c.body;
|
|
535
|
+
if (done.status === 'complete_in_progress')
|
|
536
|
+
inProgress(store, id);
|
|
537
|
+
if (hasCode(done, 'order_not_placed'))
|
|
538
|
+
notPlaced(store, done);
|
|
498
539
|
if (done.status !== 'completed' || !done.order?.id) {
|
|
499
540
|
const said = (done.messages ?? []).map((m) => m.content).filter(Boolean).join(' ');
|
|
500
541
|
fail('store_refused', `${store.host} did not take the payment for checkout ${id}${said ? `: ${said}` : '.'}`, { details: { status: done.status, messages: done.messages ?? [] }, next_step: { action: 'human_step', url: done.continue_url ?? null } }, 'rejected');
|
|
501
542
|
}
|
|
502
543
|
return placed(store, done, true);
|
|
503
544
|
}
|
|
545
|
+
/** The store took the payment and has not finished it: the agent reads it again, and pays nothing more. */
|
|
546
|
+
function inProgress(store, id) {
|
|
547
|
+
fail('payment_in_progress', `${store.host} is still taking the payment for checkout ${id}: it is not confirmed yet. Run the same command again in a moment: it reads the checkout first and never pays twice. Until it answers the order, don't tell the human it is paid.`, { details: { status: 'complete_in_progress' }, next_step: { action: 'retry_later', command: cmd(store, 'order', id) } });
|
|
548
|
+
}
|
|
504
549
|
const sandboxBody = () => ({ ...sandboxPayment(), signals: { 'dev.ucp.user_agent': 'c11r' } });
|
package/dist/errors.js
CHANGED
|
@@ -66,6 +66,10 @@ const KIND = {
|
|
|
66
66
|
store_unavailable: 'unavailable',
|
|
67
67
|
store_refused: 'rejected',
|
|
68
68
|
payment_handler_unsupported: 'rejected',
|
|
69
|
+
// The store took the payment and has not confirmed it: read again, never paid twice.
|
|
70
|
+
payment_in_progress: 'unavailable',
|
|
71
|
+
// Paid at the store, and the order not placed: the human reviews it; never paid again.
|
|
72
|
+
order_not_placed: 'rejected',
|
|
69
73
|
// The site that publishes the skills (c11r skills sync).
|
|
70
74
|
site_unavailable: 'unavailable',
|
|
71
75
|
};
|
package/dist/ucp.js
CHANGED
|
@@ -55,20 +55,41 @@ export async function discover(origin) {
|
|
|
55
55
|
capabilities.add(name);
|
|
56
56
|
return { origin, host, profile, endpoint, commonEndpoint: rest('dev.ucp.common'), capabilities, keys: Array.isArray(profile.keys) ? profile.keys : [] };
|
|
57
57
|
}
|
|
58
|
+
/** Stripe's UCP handler: agents pay with a Stripe wallet token (spt_), which this CLI does not hold. */
|
|
59
|
+
export const STRIPE_HANDLER = 'com.stripe.payments';
|
|
58
60
|
/** The payment handlers the store offers, by name, and whether this CLI can pay there. */
|
|
59
61
|
export function handlersOf(store) {
|
|
60
62
|
const handlers = Object.keys(store.profile.ucp?.payment_handlers ?? {});
|
|
61
63
|
return { handlers, can_pay: handlers.includes(SANDBOX_HANDLER) && store.capabilities.has('dev.ucp.shopping.checkout') };
|
|
62
64
|
}
|
|
63
|
-
/**
|
|
64
|
-
export function
|
|
65
|
+
/** Why this CLI cannot pay at the store, in words for the human; null when it can. */
|
|
66
|
+
export function cannotPay(store, checkout) {
|
|
67
|
+
const handlers = checkout?.handlers ?? handlersOf(store).handlers;
|
|
68
|
+
if (handlers.includes(SANDBOX_HANDLER) && store.capabilities.has('dev.ucp.shopping.checkout'))
|
|
69
|
+
return null;
|
|
70
|
+
// A checkout's own list, the authority over the profile's union: named, so the human knows which.
|
|
71
|
+
const who = checkout ? `${store.host}, for checkout ${checkout.id},` : store.host;
|
|
72
|
+
const elsewhere = `The human buys on the shop's site.`;
|
|
65
73
|
if (!store.capabilities.has('dev.ucp.shopping.checkout')) {
|
|
66
|
-
|
|
74
|
+
return { code: 'not_supported', message: `${who} doesn't take orders from agents yet: an agent can show its products, not buy them. ${elsewhere}` };
|
|
67
75
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
if (!handlers.length)
|
|
77
|
+
return { code: 'payment_handler_unsupported', message: `${who} takes no payment from agents yet. ${elsewhere}` };
|
|
78
|
+
const only = 'This CLI pays only in test shops, with the sandbox test handler, where no money moves.';
|
|
79
|
+
if (handlers.includes(STRIPE_HANDLER)) {
|
|
80
|
+
return { code: 'payment_handler_unsupported', message: `${who} takes agent payments through Stripe, from agents Stripe has enrolled, and this CLI is not one of them yet. ${only} ${elsewhere}` };
|
|
71
81
|
}
|
|
82
|
+
return { code: 'payment_handler_unsupported', message: `${who} takes ${handlers.join(', ')}, which this CLI can't pay with. ${only} ${elsewhere}` };
|
|
83
|
+
}
|
|
84
|
+
/** Before anything of the human's is sent: a store this CLI cannot pay at is refused. */
|
|
85
|
+
export function mustPay(store, extra = {}) {
|
|
86
|
+
const why = cannotPay(store);
|
|
87
|
+
if (!why)
|
|
88
|
+
return;
|
|
89
|
+
const { handlers } = handlersOf(store);
|
|
90
|
+
if (why.code === 'not_supported')
|
|
91
|
+
fail('not_supported', why.message, { details: { handlers }, next_step: { action: 'human_step', url: store.origin } });
|
|
92
|
+
fail(why.code, why.message, { details: { handlers }, next_step: { action: 'human_step', url: extra.continue_url ?? store.origin } }, 'rejected');
|
|
72
93
|
}
|
|
73
94
|
export const sandboxPayment = () => ({ payment: { instruments: [{ id: 'pi_1', handler_id: SANDBOX_HANDLER_ID, type: 'card', selected: true, credential: { type: SANDBOX_CREDENTIAL_TYPE, token: SANDBOX_TOKEN_SUCCESS } }] } });
|
|
74
95
|
/** A UCP call. The answer's digest and signature are checked; an error is a CliError. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commercelayer/agent-cli",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.23",
|
|
4
4
|
"description": "c11r: the Commerce Layer CLI for coding agents. One grammar, c11r <noun> <verb>, from a sandbox to a running organization.",
|
|
5
5
|
"homepage": "https://commercelayer.ai",
|
|
6
6
|
"keywords": [
|