@curless/sinocare-demo 0.55.0 → 0.57.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.
- package/README.md +3 -3
- package/dist/__tests__/checkout-card.test.js +13 -13
- package/dist/__tests__/checkout-card.test.js.map +1 -1
- package/dist/checkout-card.generated.js +1 -1
- package/dist/checkout-card.generated.js.map +1 -1
- package/dist/index.js +94 -80
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkout-card.generated.js","sourceRoot":"","sources":["../src/checkout-card.generated.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,iFAAiF;AACjF,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAW,MAAM,CAAC,IAAI,CACnD,
|
|
1
|
+
{"version":3,"file":"checkout-card.generated.js","sourceRoot":"","sources":["../src/checkout-card.generated.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,iFAAiF;AACjF,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,kBAAkB,GAAW,MAAM,CAAC,IAAI,CACnD,s60dAAs60d,EACt60d,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -229,11 +229,11 @@ const WALLET_FALLBACK_OK = process.env.SINOCARE_WALLET_FALLBACK === '1';
|
|
|
229
229
|
const mintSharedPaymentToken = async (amountMinor, currency) => {
|
|
230
230
|
if (!STRIPE_AGENT_KEY || !SELLER_PROFILE)
|
|
231
231
|
return null;
|
|
232
|
-
// A live key here means a booking charges a REAL card for the full
|
|
232
|
+
// A live key here means a booking charges a REAL card for the full product
|
|
233
233
|
// price (€1,280–2,900) the moment someone clicks pay in the card — with no
|
|
234
234
|
// confirmation step anywhere. Refuse unless that is explicitly what you want.
|
|
235
235
|
if (STRIPE_AGENT_SECRET_KEY_IS_LIVE && !WALLET_LIVE_OK) {
|
|
236
|
-
throw new Error('refusing to mint with a LIVE Stripe key — a booking would charge a real card for the full
|
|
236
|
+
throw new Error('refusing to mint with a LIVE Stripe key — a booking would charge a real card for the full product price. Set SINOCARE_ALLOW_LIVE_WALLET=1 if that is genuinely intended.');
|
|
237
237
|
}
|
|
238
238
|
const post = async (path, body, preview = false) => {
|
|
239
239
|
const res = await fetch(`https://api.stripe.com/v1/${path}`, {
|
|
@@ -500,7 +500,7 @@ let agentBudget = loadBudget();
|
|
|
500
500
|
let lastCheckout = null;
|
|
501
501
|
const completedBookings = new Map();
|
|
502
502
|
// Persist the card's checkout state (last pick + booking receipts) across restarts
|
|
503
|
-
// and conversations, so returning to a paid
|
|
503
|
+
// and conversations, so returning to a paid product's card shows its receipt
|
|
504
504
|
// instead of resetting to the picker — and so a fresh "book it again" starts
|
|
505
505
|
// clean. Keyed to the agent token so a different buyer starts empty. Override the
|
|
506
506
|
// path with SINOCARE_DEMO_STATE_FILE.
|
|
@@ -556,12 +556,12 @@ if (tokenFp) {
|
|
|
556
556
|
// It exists for one case: a card self-healing with no arguments, where
|
|
557
557
|
// the server replays the last pick so the widget fills instead of
|
|
558
558
|
// hanging. Restored from disk it does that to a card belonging to a
|
|
559
|
-
// DIFFERENT
|
|
559
|
+
// DIFFERENT product — restart the app, ask for a new hotel, and the card
|
|
560
560
|
// renders the previous booking's receipt, which reads as having booked
|
|
561
561
|
// something you did not.
|
|
562
562
|
//
|
|
563
563
|
// `bookings` is safe to restore because it is keyed by sku: it is only
|
|
564
|
-
// ever consulted for a
|
|
564
|
+
// ever consulted for a product someone actually asked about.
|
|
565
565
|
for (const [k, v] of saved.bookings ?? [])
|
|
566
566
|
completedBookings.set(k, v);
|
|
567
567
|
}
|
|
@@ -597,7 +597,7 @@ const finishBooking = (sku, value) => {
|
|
|
597
597
|
}).catch(() => { });
|
|
598
598
|
completedBookings.set(sku, {
|
|
599
599
|
sku,
|
|
600
|
-
name: String(value.
|
|
600
|
+
name: String(value.product ?? ''),
|
|
601
601
|
protocol: value.protocol,
|
|
602
602
|
total: (value.total ?? value.paidUsdc ?? value.paidRlusd ?? value.priceEur),
|
|
603
603
|
status: value.status,
|
|
@@ -616,7 +616,7 @@ const finishBooking = (sku, value) => {
|
|
|
616
616
|
return text(value);
|
|
617
617
|
};
|
|
618
618
|
// The 'booked' card view: a receipt shown when a rehydrated checkout resolves to
|
|
619
|
-
// an already-booked
|
|
619
|
+
// an already-booked product. Carries the pick fields too so the card's "再订一次"
|
|
620
620
|
// button can drop straight back into the picker without another round-trip.
|
|
621
621
|
const bookedView = (pick, r) => ({
|
|
622
622
|
view: 'booked',
|
|
@@ -645,24 +645,28 @@ const bookedView = (pick, r) => ({
|
|
|
645
645
|
// corrected data and the old JS draws the old picture. Three widget changes
|
|
646
646
|
// went out today without a bump, and the receipt/picker fix appeared to have
|
|
647
647
|
// no effect at all because of it.
|
|
648
|
-
const CHECKOUT_URI = 'ui://sinocare/card-
|
|
648
|
+
const CHECKOUT_URI = 'ui://sinocare/card-v25.html';
|
|
649
649
|
const CARD_MIME = 'text/html;profile=mcp-app';
|
|
650
650
|
const CHECKOUT_UI_META = { ui: { resourceUri: CHECKOUT_URI }, 'ui/resourceUri': CHECKOUT_URI };
|
|
651
651
|
const TOOLS = [
|
|
652
652
|
{
|
|
653
653
|
name: 'list_sinocare_products',
|
|
654
|
-
description: 'Browse Sinocare
|
|
654
|
+
description: 'Browse the Sinocare catalog AS A CARD — every row opens a checkout for that product. Prefer this over listing the catalog as text. Optional `category`: meter | cgm | strips | lancets | accessory.',
|
|
655
655
|
inputSchema: {
|
|
656
656
|
type: 'object',
|
|
657
657
|
properties: {
|
|
658
|
-
|
|
659
|
-
|
|
658
|
+
category: {
|
|
659
|
+
type: 'string',
|
|
660
|
+
enum: ['meter', 'cgm', 'strips', 'lancets', 'accessory'],
|
|
661
|
+
description: 'narrow to one aisle',
|
|
662
|
+
},
|
|
660
663
|
},
|
|
661
664
|
},
|
|
665
|
+
_meta: CHECKOUT_UI_META,
|
|
662
666
|
},
|
|
663
667
|
{
|
|
664
668
|
name: 'get_sinocare_product',
|
|
665
|
-
description: 'Get one Sinocare
|
|
669
|
+
description: 'Get one Sinocare product by its sku (from list_sinocare_products).',
|
|
666
670
|
inputSchema: {
|
|
667
671
|
type: 'object',
|
|
668
672
|
required: ['sku'],
|
|
@@ -671,17 +675,17 @@ const TOOLS = [
|
|
|
671
675
|
},
|
|
672
676
|
{
|
|
673
677
|
name: 'start_sinocare_checkout',
|
|
674
|
-
description: "THE DEFAULT WAY TO
|
|
678
|
+
description: "THE DEFAULT WAY TO BUY. Open a visual checkout card for a Sinocare product so the user PICKS a payment method by clicking (instead of typing) — Card (ACP) / UCP / USDC (x402) / RLUSD (XRP Ledger) in-conversation, or a Payment Link hosted page. When the user wants to buy, CALL THIS — do not list the payment methods as text and ask which one; the card is where the user chooses and pays. ALWAYS prefer this over buy_sinocare_product_*. YOU MUST identify the product: pass its `sku` OR a `query` name (e.g. 'Safe-Accu', 'Safe AQ Smart') — do NOT call this with no product (the card can't render without one). To show the whole catalog instead, call list_sinocare_products: it renders as a card too, and every row opens that product's checkout. If one product matches, open its card directly rather than asking to confirm.",
|
|
675
679
|
inputSchema: {
|
|
676
680
|
type: 'object',
|
|
677
681
|
properties: {
|
|
678
682
|
sku: {
|
|
679
683
|
type: 'string',
|
|
680
|
-
description: "the
|
|
684
|
+
description: "the product's exact sku (from list_sinocare_products)",
|
|
681
685
|
},
|
|
682
686
|
query: {
|
|
683
687
|
type: 'string',
|
|
684
|
-
description: "the
|
|
688
|
+
description: "the product NAME (e.g. 'Safe AQ Smart') — used if you don't have the exact sku; resolved server-side. Pass `sku` OR `query`.",
|
|
685
689
|
},
|
|
686
690
|
quantity: { type: 'integer', description: 'number of guests/weeks (default 1)' },
|
|
687
691
|
},
|
|
@@ -690,7 +694,7 @@ const TOOLS = [
|
|
|
690
694
|
},
|
|
691
695
|
{
|
|
692
696
|
name: 'create_sinocare_payment_link',
|
|
693
|
-
description: "Create a Payment Link for a
|
|
697
|
+
description: "Create a Payment Link for a product — a PUBLIC hosted checkout page (like a Stripe Payment Link) the user opens in a browser to pay (test-mode card). Returns the URL. Mostly invoked by the checkout card's 'Payment Link' button; the external-link payment path.",
|
|
694
698
|
inputSchema: {
|
|
695
699
|
type: 'object',
|
|
696
700
|
required: ['sku'],
|
|
@@ -712,7 +716,7 @@ const TOOLS = [
|
|
|
712
716
|
},
|
|
713
717
|
{
|
|
714
718
|
name: 'buy_sinocare_product_wallet',
|
|
715
|
-
description: "Book a Sinocare
|
|
719
|
+
description: "Book a Sinocare product over ACP, paying from the buyer's own Curless wallet: the wallet mints a credential from the card THEY bound, capped by the limits THEY set, scoped to this seller and this amount for 15 minutes. If nobody is signed in, this returns a sign-in link — show it to the person; the card polls on its own and pays once they finish in the browser. Do NOT ask them for a password in the chat. If the wallet refuses because the booking is over their limit, tell the person it is their own limit and offer to raise it — do not retry with another payment method.",
|
|
716
720
|
inputSchema: {
|
|
717
721
|
type: 'object',
|
|
718
722
|
required: ['sku'],
|
|
@@ -783,7 +787,7 @@ const TOOLS = [
|
|
|
783
787
|
},
|
|
784
788
|
{
|
|
785
789
|
name: 'buy_sinocare_product_acp',
|
|
786
|
-
description: 'Book a Sinocare
|
|
790
|
+
description: 'Book a Sinocare product over ACP (Agentic Commerce Protocol) — Sinocare prices the product and opens a merchant-quoted checkout session; this agent completes it with a delegated card credential, settling on the card rail (test mode). One of THREE ways to pay: ACP (card) / UCP (card) / x402 (USDC). The card rail is an implementation detail, not a separate payment choice. Returns the booked order.',
|
|
787
791
|
inputSchema: {
|
|
788
792
|
type: 'object',
|
|
789
793
|
required: ['sku'],
|
|
@@ -795,7 +799,7 @@ const TOOLS = [
|
|
|
795
799
|
},
|
|
796
800
|
{
|
|
797
801
|
name: 'buy_sinocare_product_ucp',
|
|
798
|
-
description: 'Book a Sinocare
|
|
802
|
+
description: 'Book a Sinocare product over UCP (Universal Commerce Protocol, ucp.dev) — Sinocare prices it and opens a UCP checkout session. UCP "supports x402", and x402 is chain-agnostic, so the SAME UCP session settles FOUR ways (pass `pay`): `card` (default) → the agent authorizes with an AP2 PaymentMandate bound to the cart, settled on the card rail (test mode, same as ACP); `usdc` → the gateway freezes an x402 challenge and the agent pays with a signed EIP-3009 X-PAYMENT (USDC on Base/EVM); `usdc-sol` → same, on Solana — the agent partially signs an SPL transfer the facilitator co-signs + submits (USDC on Solana, buyer needs no SOL); `rlusd` → an XRPL x402 challenge, a signed XRPL Payment (Ed25519, no gas), on the XRP Ledger. The three stablecoin ways all settle on-chain over the stablecoin rail — currency + chain just pick the rail. The standing authorization (authorize_agent_budget) is attached on the card path. Returns the booked order.',
|
|
799
803
|
inputSchema: {
|
|
800
804
|
type: 'object',
|
|
801
805
|
required: ['sku'],
|
|
@@ -840,7 +844,7 @@ const TOOLS = [
|
|
|
840
844
|
},
|
|
841
845
|
{
|
|
842
846
|
name: 'buy_sinocare_product_x402',
|
|
843
|
-
description: 'Book a Sinocare
|
|
847
|
+
description: 'Book a Sinocare product and pay in USDC over x402. `chain` picks the USDC chain: `base` (default) → EVM, a signed EIP-3009 authorization; `solana` → an SVM transfer the buyer partially signs and the facilitator co-signs (feePayer) + submits (buyer needs no SOL). The merchant receives USDC at its address on that chain. Returns the booking + the USDC amount paid (+ the Solana tx on the solana path).',
|
|
844
848
|
inputSchema: {
|
|
845
849
|
type: 'object',
|
|
846
850
|
required: ['sku'],
|
|
@@ -862,7 +866,7 @@ const TOOLS = [
|
|
|
862
866
|
},
|
|
863
867
|
{
|
|
864
868
|
name: 'buy_sinocare_product_rlusd',
|
|
865
|
-
description: "Book a Sinocare
|
|
869
|
+
description: "Book a Sinocare product and pay in RLUSD (Ripple USD) over x402 on the XRP Ledger. Sinocare opens a merchant-quoted RLUSD session; this agent signs a standard XRPL Payment (Ed25519, no gas — the buyer pays the tiny XRPL fee) and Curless's self-hosted facilitator submits it on-ledger. Same x402 protocol as the USDC tool, different chain. Returns the booking + the RLUSD amount paid + the XRPL tx.",
|
|
866
870
|
inputSchema: {
|
|
867
871
|
type: 'object',
|
|
868
872
|
required: ['sku'],
|
|
@@ -874,7 +878,7 @@ const TOOLS = [
|
|
|
874
878
|
},
|
|
875
879
|
{
|
|
876
880
|
name: 'buy_sinocare_product_skyfire',
|
|
877
|
-
description: "Book a Sinocare
|
|
881
|
+
description: "Book a Sinocare product over Skyfire KYAPay (agent-native JWT payments). The buyer mints a Skyfire-SIGNED PAY token (via Skyfire, authorizing a small USDC charge to Sinocare — unlike the crypto rails the buyer can't self-sign, Skyfire signs it) and presents it to the Sinocare merchant backend, which charges it via the merchant SDK; agentbank verifies it against Skyfire's live JWKS — proving the agent's identity (Know Your Agent) AND the payment authorization — and records the USDC payment intent. PRODUCTION Skyfire (real token, real verify). Needs SKYFIRE_BUYER_API_KEY + SKYFIRE_SELLER_SERVICE_ID (to mint). Returns the verification + the recorded payment intent (agentbank verifies + records the authorization; it does not auto-capture).",
|
|
878
882
|
inputSchema: {
|
|
879
883
|
type: 'object',
|
|
880
884
|
required: ['sku'],
|
|
@@ -886,7 +890,7 @@ const TOOLS = [
|
|
|
886
890
|
},
|
|
887
891
|
{
|
|
888
892
|
name: 'buy_sinocare_product_mpp',
|
|
889
|
-
description: "Book a Sinocare
|
|
893
|
+
description: "Book a Sinocare product over MPP (Machine Payments Protocol) settled on the Tempo payments chain. agentbank answers the unpaid request with an HTTP-402 Payment challenge whose SIGNED terms carry the on-chain payment (TIP-20 pathUSD token, merchant recipient, memo=challenge binding); the buyer's Tempo wallet pays it with transferWithMemo; the credential proves it by tx HASH and agentbank VERIFIES the on-chain transfer read-only, then captures the payment intent. Real Tempo testnet, on-chain stablecoin — no card, no gateway signing. Needs AGENTBANK_AGENT_TOKEN + TEMPO_BUYER_PRIVATE_KEY (a funded Tempo testnet key).",
|
|
890
894
|
inputSchema: {
|
|
891
895
|
type: 'object',
|
|
892
896
|
required: ['sku'],
|
|
@@ -903,7 +907,7 @@ const TOOLS = [
|
|
|
903
907
|
},
|
|
904
908
|
{
|
|
905
909
|
name: 'list_my_orders',
|
|
906
|
-
description: 'List my Sinocare bookings (orders) — newest first, with id,
|
|
910
|
+
description: 'List my Sinocare bookings (orders) — newest first, with id, product, amount, and status. Renders an interactive card: each settled booking has a 申请退款 (request refund) button. Use the order id with request_refund to ask for a refund.',
|
|
907
911
|
inputSchema: { type: 'object', properties: {} },
|
|
908
912
|
_meta: CHECKOUT_UI_META,
|
|
909
913
|
},
|
|
@@ -932,9 +936,9 @@ const text = (value) => ({
|
|
|
932
936
|
// non-UI fallback only.
|
|
933
937
|
const INSTRUCTIONS = [
|
|
934
938
|
'Sinocare storefront (buyer side).',
|
|
935
|
-
'When the user wants to
|
|
939
|
+
'When the user wants to buy something — "buy", "order", "我要买…", "再来一盒试纸" — call start_sinocare_checkout (pass the product `sku`, or a `query` name such as "Safe-Accu") to open the visual checkout card. To show the whole catalog, call list_sinocare_products — it renders as a card too, and every row opens that product\'s checkout. The user picks a payment method and pays by clicking IN THE CARD.',
|
|
936
940
|
'Do NOT enumerate the payment methods (ACP / UCP / x402 / RLUSD / Payment Link) as text and ask which one — that is what the card is for. Only fall back to describing options in prose if the checkout card genuinely cannot be shown (a non-UI host).',
|
|
937
|
-
'If the
|
|
941
|
+
'If the product is ambiguous or unknown, show the catalog card with list_sinocare_products and let the person pick a row. If exactly one product matches, open its card directly rather than asking to confirm.',
|
|
938
942
|
'Call the buy_sinocare_product_* tools directly only when the card is unavailable or the user explicitly asked for a specific rail in a plain-text flow.',
|
|
939
943
|
].join(' ');
|
|
940
944
|
const server = new Server({ name: 'sinocare', version: '0.5.0' }, { capabilities: { tools: {}, resources: {} }, instructions: INSTRUCTIONS });
|
|
@@ -951,31 +955,41 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
951
955
|
const args = (req.params.arguments ?? {});
|
|
952
956
|
try {
|
|
953
957
|
if (req.params.name === 'list_sinocare_products') {
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
958
|
+
// `products`, and a `view` the card dispatches on. This returned
|
|
959
|
+
// `{currency, count, products}` — the product storefront's shape, left
|
|
960
|
+
// behind by a rename that changed identifiers and not payloads. The card
|
|
961
|
+
// reads `products`, got undefined, and drew nothing.
|
|
962
|
+
const category = typeof args.category === 'string' ? args.category.trim().toLowerCase() : '';
|
|
963
|
+
const known = ['meter', 'cgm', 'strips', 'lancets', 'accessory'];
|
|
964
|
+
const q = known.includes(category) ? `?category=${encodeURIComponent(category)}` : '';
|
|
965
|
+
const { items } = await backend(`/catalog${q}`);
|
|
966
|
+
return {
|
|
967
|
+
content: [
|
|
968
|
+
{ type: 'text', text: JSON.stringify({ count: items.length, products: items }) },
|
|
969
|
+
],
|
|
970
|
+
structuredContent: { view: 'products', count: items.length, products: items },
|
|
971
|
+
_meta: CHECKOUT_UI_META,
|
|
972
|
+
};
|
|
959
973
|
}
|
|
960
974
|
if (req.params.name === 'get_sinocare_product') {
|
|
961
975
|
const sku = String(args.sku ?? '');
|
|
962
|
-
const
|
|
963
|
-
return text(
|
|
976
|
+
const product = await backend(`/catalog/${encodeURIComponent(sku)}`);
|
|
977
|
+
return text(product);
|
|
964
978
|
}
|
|
965
|
-
// Visual checkout: return the MCP App card (payment picker) + the
|
|
979
|
+
// Visual checkout: return the MCP App card (payment picker) + the product as
|
|
966
980
|
// structuredContent. The card's buttons call the book_* /
|
|
967
981
|
// create_sinocare_payment_link tools; the card renders their results.
|
|
968
982
|
if (req.params.name === 'start_sinocare_checkout') {
|
|
969
|
-
// Accept a `sku` (exact) OR a `query`/`
|
|
983
|
+
// Accept a `sku` (exact) OR a `query`/`product`/`name` (e.g. "Safe AQ Smart")
|
|
970
984
|
// so the agent doesn't need the exact sku to open the card.
|
|
971
|
-
const idOrName = String(args.sku ?? args.query ?? args.
|
|
985
|
+
const idOrName = String(args.sku ?? args.query ?? args.product ?? args.name ?? '').trim();
|
|
972
986
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
973
987
|
// The card sets __rehydrate when it re-invokes to fill itself (reload /
|
|
974
988
|
// self-heal), never on a fresh agent-initiated checkout. Only on rehydrate
|
|
975
989
|
// do we show a completed booking's receipt instead of the picker — so an
|
|
976
990
|
// explicit "book X again" from the agent still opens the picker.
|
|
977
991
|
const rehydrate = args.__rehydrate === true || args.__rehydrate === 1 || args.__rehydrate === '1';
|
|
978
|
-
// No
|
|
992
|
+
// No product given = the card self-healing (empty-arg re-invoke) — replay the
|
|
979
993
|
// last pick so the widget fills in instead of hanging on "Loading…". If that
|
|
980
994
|
// last checkout has since been booked, show its receipt on rehydrate.
|
|
981
995
|
if (!idOrName) {
|
|
@@ -995,7 +1009,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
995
1009
|
type: 'text',
|
|
996
1010
|
text: lastCheckout
|
|
997
1011
|
? `Checkout for ${lastCheckout.name}.`
|
|
998
|
-
: 'Pick a
|
|
1012
|
+
: 'Pick a product first (list_sinocare_products), then open its checkout.',
|
|
999
1013
|
},
|
|
1000
1014
|
],
|
|
1001
1015
|
structuredContent: lastCheckout ?? {
|
|
@@ -1009,20 +1023,20 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1009
1023
|
};
|
|
1010
1024
|
}
|
|
1011
1025
|
// Resolve: try exact sku first, then a name search over the catalog.
|
|
1012
|
-
let
|
|
1026
|
+
let product = null;
|
|
1013
1027
|
try {
|
|
1014
|
-
|
|
1028
|
+
product = await backend(`/catalog/${encodeURIComponent(idOrName)}`);
|
|
1015
1029
|
}
|
|
1016
1030
|
catch {
|
|
1017
1031
|
const { items } = await backend('/catalog');
|
|
1018
1032
|
const q = idOrName.toLowerCase();
|
|
1019
|
-
|
|
1033
|
+
product =
|
|
1020
1034
|
items.find((r) => r.sku.toLowerCase() === q) ??
|
|
1021
1035
|
items.find((r) => r.name.toLowerCase() === q) ??
|
|
1022
1036
|
items.find((r) => r.name.toLowerCase().includes(q)) ??
|
|
1023
1037
|
null;
|
|
1024
1038
|
}
|
|
1025
|
-
if (!
|
|
1039
|
+
if (!product) {
|
|
1026
1040
|
return {
|
|
1027
1041
|
content: [
|
|
1028
1042
|
{
|
|
@@ -1033,8 +1047,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1033
1047
|
isError: true,
|
|
1034
1048
|
};
|
|
1035
1049
|
}
|
|
1036
|
-
const priceEur =
|
|
1037
|
-
// Rehydrating an already-BOOKED
|
|
1050
|
+
const priceEur = product.priceDisplay ?? `€${((product.price ?? 0) / 100).toLocaleString('en-IE')}`;
|
|
1051
|
+
// Rehydrating an already-BOOKED product's card → show its receipt, not the
|
|
1038
1052
|
// picker (fixes: pay, leave the conversation, come back, and the card had
|
|
1039
1053
|
// reset to the payment selection). The card self-heals by re-invoking with
|
|
1040
1054
|
// its ORIGINAL args (the sku) + __rehydrate. EXCEPTION: a deliberate "book
|
|
@@ -1044,30 +1058,30 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1044
1058
|
// receipts are kept intact (never deleted), so #6's cross-conversation
|
|
1045
1059
|
// records — and OTHER booked cards in the same conversation — still
|
|
1046
1060
|
// rehydrate to their receipts.
|
|
1047
|
-
// Is THIS checkout paid? Not "has this
|
|
1061
|
+
// Is THIS checkout paid? Not "has this product ever been bought".
|
|
1048
1062
|
//
|
|
1049
|
-
// The receipt used to be looked up by sku, which treats a
|
|
1063
|
+
// The receipt used to be looked up by sku, which treats a product as a
|
|
1050
1064
|
// purchase. It is not: opening a checkout for somewhere you booked last
|
|
1051
|
-
// week is a NEW order. With 42 of 44
|
|
1065
|
+
// week is a NEW order. With 42 of 44 products recorded, that lookup made
|
|
1052
1066
|
// almost every card render as an already-paid receipt the moment it
|
|
1053
1067
|
// self-healed — no payment involved — and hid the payment picker behind
|
|
1054
1068
|
// a booking that had happened days ago.
|
|
1055
1069
|
//
|
|
1056
1070
|
// So the question is answered by the checkout in hand: the pick we just
|
|
1057
|
-
// had open, for this same
|
|
1071
|
+
// had open, for this same product, that was actually paid. A fresh open
|
|
1058
1072
|
// replaces it with an unpaid one (below), which is what makes "book it
|
|
1059
1073
|
// again" resolve to the picker.
|
|
1060
|
-
const rehydratedReceipt = rehydrate && lastCheckout?.sku ===
|
|
1061
|
-
? completedBookings.get(
|
|
1074
|
+
const rehydratedReceipt = rehydrate && lastCheckout?.sku === product.sku && lastCheckout.booked
|
|
1075
|
+
? completedBookings.get(product.sku)
|
|
1062
1076
|
: undefined;
|
|
1063
1077
|
lastCheckout = {
|
|
1064
1078
|
view: 'pick',
|
|
1065
|
-
sku:
|
|
1066
|
-
name:
|
|
1079
|
+
sku: product.sku,
|
|
1080
|
+
name: product.name,
|
|
1067
1081
|
priceEur,
|
|
1068
1082
|
quantity,
|
|
1069
|
-
imageUrl:
|
|
1070
|
-
externalShopify:
|
|
1083
|
+
imageUrl: product.imageUrl,
|
|
1084
|
+
externalShopify: product.source === 'shopify',
|
|
1071
1085
|
// Preserve booked only when rehydrating a paid pick; a fresh open is a
|
|
1072
1086
|
// new, unpaid order (booked stays falsy → the card shows the picker).
|
|
1073
1087
|
booked: rehydratedReceipt ? true : undefined,
|
|
@@ -1076,7 +1090,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1076
1090
|
const receipt = rehydratedReceipt;
|
|
1077
1091
|
if (receipt) {
|
|
1078
1092
|
return {
|
|
1079
|
-
content: [{ type: 'text', text: `Booked ${
|
|
1093
|
+
content: [{ type: 'text', text: `Booked ${product.name} (${priceEur}).` }],
|
|
1080
1094
|
structuredContent: bookedView(lastCheckout, receipt),
|
|
1081
1095
|
_meta: CHECKOUT_UI_META,
|
|
1082
1096
|
};
|
|
@@ -1085,7 +1099,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1085
1099
|
content: [
|
|
1086
1100
|
{
|
|
1087
1101
|
type: 'text',
|
|
1088
|
-
text: `Checkout for ${
|
|
1102
|
+
text: `Checkout for ${product.name} (${priceEur}). Pick a payment method in the card — Card (ACP) / UCP / USDC (x402) / RLUSD (XRP Ledger), or a Payment Link (hosted page).`,
|
|
1089
1103
|
},
|
|
1090
1104
|
],
|
|
1091
1105
|
structuredContent: lastCheckout,
|
|
@@ -1206,7 +1220,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1206
1220
|
}
|
|
1207
1221
|
const sku = String(args.sku ?? '');
|
|
1208
1222
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
1209
|
-
// 1. Sinocare prices the
|
|
1223
|
+
// 1. Sinocare prices the product + opens a merchant-quoted ACP session.
|
|
1210
1224
|
const session = await backend('/checkout-acp', {
|
|
1211
1225
|
method: 'POST',
|
|
1212
1226
|
body: JSON.stringify({ sku, quantity, ...(BUYER ? { buyer: BUYER } : {}) }),
|
|
@@ -1276,7 +1290,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1276
1290
|
const order = (await completeCheckout(acp(session.merchantId), session.sessionId, token));
|
|
1277
1291
|
return finishBooking(sku, {
|
|
1278
1292
|
booked: order.status === 'completed',
|
|
1279
|
-
|
|
1293
|
+
product: session.name,
|
|
1280
1294
|
quantity,
|
|
1281
1295
|
total: session.amountDisplay,
|
|
1282
1296
|
protocol: 'acp',
|
|
@@ -1313,7 +1327,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1313
1327
|
merchantId: session.merchantId,
|
|
1314
1328
|
amount,
|
|
1315
1329
|
currency,
|
|
1316
|
-
|
|
1330
|
+
product: session.name,
|
|
1317
1331
|
total: session.amountDisplay,
|
|
1318
1332
|
// Spells out the sign-in step. Without it the model calls
|
|
1319
1333
|
// wallet_pay_credential, gets "nobody is signed in", and has to invent
|
|
@@ -1343,7 +1357,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1343
1357
|
// idea it had been bought, so it rehydrated to the picker.
|
|
1344
1358
|
return finishBooking(bought?.sku ?? '', {
|
|
1345
1359
|
booked,
|
|
1346
|
-
|
|
1360
|
+
product: bought?.name,
|
|
1347
1361
|
quantity: bought?.quantity,
|
|
1348
1362
|
protocol: 'acp',
|
|
1349
1363
|
payer: 'curless-wallet (separate connector)',
|
|
@@ -1369,10 +1383,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1369
1383
|
message: '在浏览器里打开这个链接,看清金额再批准。这里还没有扣任何款。',
|
|
1370
1384
|
payUrl: `${API_BASE}/buyer/pay?session=${encodeURIComponent(session.sessionId)}`,
|
|
1371
1385
|
sessionId: session.sessionId,
|
|
1372
|
-
|
|
1386
|
+
product: session.name,
|
|
1373
1387
|
total: session.amountDisplay,
|
|
1374
1388
|
// Say plainly that nothing has been paid yet. A tool that returns a
|
|
1375
|
-
// link and a
|
|
1389
|
+
// link and a product name reads like a receipt if it does not.
|
|
1376
1390
|
paid: false,
|
|
1377
1391
|
merchantId: session.merchantId,
|
|
1378
1392
|
next: 'show the person this link. Nothing has been charged yet — they approve it in their browser. Then call get_sinocare_order_status with this sessionId.',
|
|
@@ -1394,7 +1408,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1394
1408
|
paid,
|
|
1395
1409
|
sessionId,
|
|
1396
1410
|
status: session.status,
|
|
1397
|
-
|
|
1411
|
+
product: bought?.name,
|
|
1398
1412
|
quantity: bought?.quantity,
|
|
1399
1413
|
protocol: 'acp',
|
|
1400
1414
|
payer: 'curless-wallet (hosted approval)',
|
|
@@ -1474,7 +1488,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1474
1488
|
const paidUsdc = fmtUsdc(BigInt(session.amount));
|
|
1475
1489
|
return finishBooking(sku, {
|
|
1476
1490
|
booked: paid.status === 'completed',
|
|
1477
|
-
|
|
1491
|
+
product: session.name,
|
|
1478
1492
|
quantity,
|
|
1479
1493
|
protocol: 'ucp',
|
|
1480
1494
|
payer: 'agent-key (UCP)',
|
|
@@ -1557,7 +1571,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1557
1571
|
const paidUsdc = `${(need / 1e6).toFixed(6)} USDC`;
|
|
1558
1572
|
return finishBooking(sku, {
|
|
1559
1573
|
booked: paid.status === 'completed',
|
|
1560
|
-
|
|
1574
|
+
product: session.name,
|
|
1561
1575
|
quantity,
|
|
1562
1576
|
protocol: 'ucp',
|
|
1563
1577
|
payer: 'agent-key (UCP)',
|
|
@@ -1638,7 +1652,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1638
1652
|
}
|
|
1639
1653
|
return finishBooking(sku, {
|
|
1640
1654
|
booked: paid.status === 'completed',
|
|
1641
|
-
|
|
1655
|
+
product: session.name,
|
|
1642
1656
|
quantity,
|
|
1643
1657
|
protocol: 'ucp',
|
|
1644
1658
|
payer: 'agent-key (UCP)',
|
|
@@ -1654,7 +1668,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1654
1668
|
});
|
|
1655
1669
|
}
|
|
1656
1670
|
// CARD path (default).
|
|
1657
|
-
// 1. Sinocare prices the
|
|
1671
|
+
// 1. Sinocare prices the product + opens a merchant-quoted UCP session,
|
|
1658
1672
|
// returning a merchant-signed AP2 CartMandate for it.
|
|
1659
1673
|
const session = await backend('/checkout-ucp', {
|
|
1660
1674
|
method: 'POST',
|
|
@@ -1699,10 +1713,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1699
1713
|
return text({
|
|
1700
1714
|
booked: false,
|
|
1701
1715
|
blockedBy: 'standing-authorization',
|
|
1702
|
-
|
|
1716
|
+
product: session.name,
|
|
1703
1717
|
total: session.amountDisplay,
|
|
1704
1718
|
authorizedCap: `€${agentBudget?.maxAmountEur.toLocaleString('en-IE')}`,
|
|
1705
|
-
message: `Blocked: ${session.name} (${session.amountDisplay}) is over your standing authorization (cap €${agentBudget?.maxAmountEur}, or it expired). Raise it with authorize_agent_budget, or book a cheaper
|
|
1719
|
+
message: `Blocked: ${session.name} (${session.amountDisplay}) is over your standing authorization (cap €${agentBudget?.maxAmountEur}, or it expired). Raise it with authorize_agent_budget, or book a cheaper product.`,
|
|
1706
1720
|
});
|
|
1707
1721
|
}
|
|
1708
1722
|
throw new Error(`ucp complete -> ${res.status}: ${errText}`);
|
|
@@ -1710,7 +1724,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1710
1724
|
const out = (await res.json());
|
|
1711
1725
|
return finishBooking(sku, {
|
|
1712
1726
|
booked: out.status === 'completed',
|
|
1713
|
-
|
|
1727
|
+
product: session.name,
|
|
1714
1728
|
quantity,
|
|
1715
1729
|
total: session.amountDisplay,
|
|
1716
1730
|
protocol: 'ucp',
|
|
@@ -1803,7 +1817,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1803
1817
|
// rail — the card rehydrates to a receipt on reload instead of the picker.
|
|
1804
1818
|
return finishBooking(sku, {
|
|
1805
1819
|
booked: paid.status === 'captured',
|
|
1806
|
-
|
|
1820
|
+
product: session.name,
|
|
1807
1821
|
quantity,
|
|
1808
1822
|
protocol: 'x402',
|
|
1809
1823
|
payer: 'buyer chain wallet (x402)',
|
|
@@ -1876,7 +1890,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1876
1890
|
const paidUsdc = fmtUsdc(BigInt(session.amount));
|
|
1877
1891
|
return finishBooking(sku, {
|
|
1878
1892
|
booked: paid.status === 'captured',
|
|
1879
|
-
|
|
1893
|
+
product: session.name,
|
|
1880
1894
|
quantity,
|
|
1881
1895
|
protocol: 'x402',
|
|
1882
1896
|
payer: 'buyer chain wallet (x402)',
|
|
@@ -1983,7 +1997,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
1983
1997
|
}
|
|
1984
1998
|
return finishBooking(sku, {
|
|
1985
1999
|
booked: paid.status === 'captured',
|
|
1986
|
-
|
|
2000
|
+
product: session.name,
|
|
1987
2001
|
quantity,
|
|
1988
2002
|
protocol: 'x402',
|
|
1989
2003
|
payer: 'buyer chain wallet (x402)',
|
|
@@ -2013,8 +2027,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2013
2027
|
}
|
|
2014
2028
|
const sku = String(args.sku ?? '');
|
|
2015
2029
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
2016
|
-
const
|
|
2017
|
-
const resortName =
|
|
2030
|
+
const product = await backend(`/catalog/${encodeURIComponent(sku)}`).catch(() => null);
|
|
2031
|
+
const resortName = product?.name ?? sku;
|
|
2018
2032
|
// The BUYER mints a Skyfire-signed PAY token (Skyfire's key signs it — the
|
|
2019
2033
|
// buyer can't self-sign), then presents it to the MERCHANT backend, which
|
|
2020
2034
|
// charges it via the merchant SDK (checkout.chargeSkyfire → the gateway
|
|
@@ -2035,7 +2049,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2035
2049
|
: `Booked ${resortName} × ${quantity} via Skyfire KYAPay — ${amountStr} verified against Skyfire's live JWKS + recorded (PI ${status}). agentbank verified the agent + payment authorization and recorded it; no seller key is configured, so it did not auto-capture and no money moved.`;
|
|
2036
2050
|
return finishBooking(sku, {
|
|
2037
2051
|
booked: out.agent?.verified === true,
|
|
2038
|
-
|
|
2052
|
+
product: resortName,
|
|
2039
2053
|
quantity,
|
|
2040
2054
|
protocol: 'skyfire',
|
|
2041
2055
|
payer: 'skyfire buyer token',
|
|
@@ -2063,8 +2077,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2063
2077
|
}
|
|
2064
2078
|
const sku = String(args.sku ?? '');
|
|
2065
2079
|
const quantity = Math.max(Number(args.quantity) || 1, 1);
|
|
2066
|
-
const
|
|
2067
|
-
const resortName =
|
|
2080
|
+
const product = await backend(`/catalog/${encodeURIComponent(sku)}`).catch(() => null);
|
|
2081
|
+
const resortName = product?.name ?? sku;
|
|
2068
2082
|
// Three-layer MPP · Tempo — the demo talks only to the MERCHANT backend,
|
|
2069
2083
|
// which mediates the HTTP-402 dance via the merchant SDK (checkout.openMPP):
|
|
2070
2084
|
// 1. ask the backend to open the charge → it returns the 402 challenge;
|
|
@@ -2089,7 +2103,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2089
2103
|
const settled = out.status === 'settled';
|
|
2090
2104
|
return finishBooking(sku, {
|
|
2091
2105
|
booked: settled,
|
|
2092
|
-
|
|
2106
|
+
product: resortName,
|
|
2093
2107
|
quantity,
|
|
2094
2108
|
protocol: 'mpp',
|
|
2095
2109
|
payer: 'buyer chain wallet (MPP·Tempo)',
|
|
@@ -2146,7 +2160,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
2146
2160
|
// The card hides its button on this — an explicit flag, not a substring
|
|
2147
2161
|
// of the status, so the two can never drift apart.
|
|
2148
2162
|
refundPending: Boolean(o.refundRequest),
|
|
2149
|
-
|
|
2163
|
+
product: o.lineItems?.map((li) => `${li.name} ×${li.quantity}`).join(', ') ?? '',
|
|
2150
2164
|
total: fmt(o.amount, o.currency),
|
|
2151
2165
|
protocol: o.protocol,
|
|
2152
2166
|
date: o.createdAt?.slice(0, 10) ?? '',
|