@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 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 there in person.
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
@@ -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: `This CLI pays only with the sandbox test handler; ${store.host} ${handlers.length ? `takes ${handlers.join(', ')}` : 'takes no payment over UCP'}. The human buys there in person.` }],
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
- return answer(checkoutView(ck, store), c, { next: nextOf(ck, store) });
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
- /** Before anything of the human's is sent: a store this CLI cannot pay at is refused. */
64
- export function mustPay(store, extra = {}) {
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
- fail('not_supported', `${store.host} offers no UCP checkout: an agent can browse it, not buy there.`, { next_step: { action: 'human_step', url: store.origin } });
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
- const { handlers } = handlersOf(store);
69
- if (!handlers.includes(SANDBOX_HANDLER)) {
70
- fail('payment_handler_unsupported', `${store.host} takes ${handlers.length ? handlers.join(', ') : 'no payment handler'}; this CLI pays only with the ${SANDBOX_HANDLER} test handler, where no money moves. The human buys there in person.`, { details: { handlers }, next_step: { action: 'human_step', url: extra.continue_url ?? store.origin } }, 'rejected');
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.22",
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": [