@agent-cards/commerce-sdk 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,14 @@ const MAX_PRODUCTS = 20;
4
4
  const MARKDOWN_LINK = /\[([^\]]*)\]\([^)]*\)/g;
5
5
  const SCHEME_URL = /\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`)\]]*/gi;
6
6
  const WWW_URL = /\bwww\.[^\s<>"'`)\]]*/gi;
7
- /** A host followed by a path, a query or a fragment: `vault.agentcard.sh/a/1`, `vault.agentcard.sh?token=abc`. */
8
- const HOST_PATH = /\b(?:[a-z0-9-]+\.)+[a-z]{2,}[/?#][^\s<>"'`)\]]*/gi;
7
+ /**
8
+ * A host followed by a path, a query or a fragment: `vault.agentcard.sh/a/1`, `vault.agentcard.sh?token=abc`. A `?` that
9
+ * only punctuation follows before a space, a closing bracket or quote, or the end starts no query: in "at glossier.com?
10
+ * Say yes" it is the question's mark, and the host reads as its site's name (BARE_HOST). A `?` that more of an address
11
+ * follows starts one, punctuation or not (`glossier.com?.ref=x`), and a `#` always starts a fragment
12
+ * (`glossier.com#:~:text=Balm`).
13
+ */
14
+ const HOST_PATH = /\b(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/#]|\?(?![.,;:!?]*(?:[\s<>"'`)\]]|$)))[^\s<>"'`)\]]*/gi;
9
15
  /**
10
16
  * A bare host on a common top-level domain, such as `agentcard.sh` or `amazon.com`, which a chat app
11
17
  * turns into a link. The list keeps words with a dot, such as `Node.js`, and an email address's host.
@@ -109,13 +115,35 @@ export function pickId(product) {
109
115
  return product.ref;
110
116
  return typeof product.id === 'string' && product.id !== '' && stripLinks(product.id) === product.id ? product.id : null;
111
117
  }
118
+ /**
119
+ * What tells one product card from another: the product's link-free id, else its page. A restaurant gives
120
+ * every menu item the store's one page, so a card per page sent one card for a reply that offered three
121
+ * dishes (2026-10-20, Suavecito Birria & Tacos on DoorDash). Each product the model reads gets its own card.
122
+ */
123
+ export function cardKey(product) {
124
+ return pickId(product) ?? (typeof product.url === 'string' && product.url !== '' ? product.url : null);
125
+ }
126
+ /**
127
+ * The key of the card a product gets, or null when it gets none: a product with a page at its store,
128
+ * or a flight option, which has no page, with an id a pick takes. A flight's card is how the shopper
129
+ * taps the flight they want (2026-10-20: three flights listed in text, and no card to tap).
130
+ */
131
+ export function productCardKey(product) {
132
+ if (typeof product.url === 'string' && product.url !== '')
133
+ return cardKey(product);
134
+ return product.flight ? pickId(product) : null;
135
+ }
112
136
  function cartView(cart) {
113
137
  return {
114
138
  store: stripLinks(cart.store?.name ?? cart.merchant_name) || stripLinks(cart.merchant),
115
139
  items: cart.items.map((line) => ({ name: stripLinks(line.name), qty: line.qty, price: money(line.priceCents, cart.currency) })),
116
140
  fees: cart.serviceFeesCents > 0 ? money(cart.serviceFeesCents, cart.currency) : null,
141
+ fee_lines: (cart.merchantFeeLines ?? []).map((line) => ({ label: stripLinks(line.label), amount: money(line.amountCents, cart.currency) })),
117
142
  tip: cart.tipCents > 0 ? money(cart.tipCents, cart.currency) : null,
118
143
  total: money(cart.totalCents, cart.currency) ?? money(0, cart.currency),
144
+ total_is_estimate: cart.totalIsEstimate === true,
145
+ // Agentcard names a ceiling equal to the total only for a hold of exactly the total, as a flight's.
146
+ max_charge: typeof cart.approvedCeilingCents === 'number' && cart.approvedCeilingCents > 0 && cart.approvedCeilingCents >= cart.totalCents ? money(cart.approvedCeilingCents, cart.currency) : null,
119
147
  };
120
148
  }
121
149
  /**
@@ -127,7 +155,7 @@ function cartView(cart) {
127
155
  export function forModel(result) {
128
156
  const status = typeof result.status === 'string' && MODEL_STATUSES.has(result.status) ? result.status : 'needs_input';
129
157
  const products = shownProducts(result.products);
130
- const held = (product) => product.linkSent === true && typeof product.url === 'string' && product.url !== '';
158
+ const held = (product) => product.linkSent === true && productCardKey(product) !== null;
131
159
  const stores = shownStores(result.stores);
132
160
  const heldStore = (store) => store.linkSent === true && typeof store.url === 'string' && store.url !== '';
133
161
  return {
@@ -141,8 +169,11 @@ export function forModel(result) {
141
169
  shopper_need_not_reply: result.shopper_need_not_reply === true,
142
170
  shopper_already_told_looking: result.shopper_already_told_looking === true,
143
171
  link_sent: result.link_sent === true,
144
- // Two products on one page share one link, so the count is of pages.
145
- product_links_sent: new Set(products.filter(held).map((product) => product.url)).size,
172
+ links_sent: result.link_sent === true
173
+ ? [...new Set((result.links ?? []).map((link) => link.kind).filter((kind) => kind === 'vault' || kind === 'approval' || kind === 'store_login'))]
174
+ : [],
175
+ // A card per product, so two dishes on a restaurant's one page are two cards.
176
+ product_links_sent: new Set(products.filter(held).map(productCardKey)).size,
146
177
  // The id the model sent, never the catalog id, which is a store page for a retail product.
147
178
  pick: result.pick ? { product_id: pickId({ id: result.pick.product_id, ref: result.pick.ref }), in_cart: result.pick.in_cart === true } : null,
148
179
  stores: stores.map((store) => ({ store_id: store.id, name: stripLinks(store.name), eta_minutes: store.etaMinutes, link_sent: heldStore(store) })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-cards/commerce-sdk",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Give your agent one tool that buys for your shoppers at real stores, and post each purchase update Agentcard sends into the shopper's chat, over a connection your server opens or a webhook endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",