@brinkcommerce/agentic-shopping-sdk 0.1.0-alpha.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/dist/index.cjs ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AgentError: () => AgentError,
24
+ PRODUCT_MARKER: () => PRODUCT_MARKER,
25
+ collectReply: () => collectReply,
26
+ createSessionId: () => createSessionId,
27
+ formatPrice: () => formatPrice,
28
+ interleaveProducts: () => interleaveProducts,
29
+ isWireEvent: () => isWireEvent,
30
+ parseWireEvents: () => parseWireEvents,
31
+ stripMarkers: () => stripMarkers
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/ndjson.ts
36
+ function parseLine(line) {
37
+ const trimmed = line.trim();
38
+ if (trimmed === "") return void 0;
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(trimmed);
42
+ } catch {
43
+ return void 0;
44
+ }
45
+ if (typeof parsed !== "object" || parsed === null) return void 0;
46
+ const event = parsed;
47
+ return typeof event.type === "string" ? event : void 0;
48
+ }
49
+ function isWireEvent(event, type) {
50
+ return event.type === type;
51
+ }
52
+ async function* parseWireEvents(body) {
53
+ const reader = body.getReader();
54
+ const decoder = new TextDecoder();
55
+ let buffer = "";
56
+ try {
57
+ for (; ; ) {
58
+ const { done, value } = await reader.read();
59
+ if (done) break;
60
+ buffer += decoder.decode(value, { stream: true });
61
+ let newline = buffer.indexOf("\n");
62
+ while (newline !== -1) {
63
+ const event = parseLine(buffer.slice(0, newline));
64
+ buffer = buffer.slice(newline + 1);
65
+ if (event !== void 0) yield event;
66
+ newline = buffer.indexOf("\n");
67
+ }
68
+ }
69
+ buffer += decoder.decode();
70
+ const last = parseLine(buffer);
71
+ if (last !== void 0) yield last;
72
+ } finally {
73
+ reader.releaseLock();
74
+ }
75
+ }
76
+ async function collectReply(events) {
77
+ let text = "";
78
+ let products = [];
79
+ let suggestions = [];
80
+ for await (const event of events) {
81
+ if (event.type === "text" && typeof event.content === "string") {
82
+ text += event.content;
83
+ continue;
84
+ }
85
+ if (event.type === "products" && Array.isArray(event.products)) {
86
+ products = event.products;
87
+ continue;
88
+ }
89
+ if (event.type === "suggestions" && Array.isArray(event.suggestions)) {
90
+ suggestions = event.suggestions;
91
+ }
92
+ }
93
+ return { text, products, suggestions };
94
+ }
95
+
96
+ // src/markers.ts
97
+ var PRODUCT_MARKER = /\[product:([\w-]+)\]/g;
98
+ var MARKER_OPEN = "[product:";
99
+ function hidePartialMarker(text) {
100
+ const open = text.lastIndexOf("[");
101
+ if (open === -1 || text.includes("]", open)) return text;
102
+ const tail = text.slice(open);
103
+ const partial = tail.startsWith(MARKER_OPEN) ? /^\[product:[\w-]*$/.test(tail) : MARKER_OPEN.startsWith(tail);
104
+ return partial ? text.slice(0, open) : text;
105
+ }
106
+ var PARAGRAPH_BREAK = /\n\s*\n/;
107
+ var MARKER_WITH_PADDING = /[^\S\r\n]*\[product:[\w-]+\][^\S\r\n]*/g;
108
+ function stripMarkersFrom(paragraph) {
109
+ return paragraph.replace(MARKER_WITH_PADDING, (gap) => /\s/.test(gap) ? " " : "").trim();
110
+ }
111
+ function interleaveProducts(text, products) {
112
+ const byId = new Map(products.map((product) => [product.productId, product]));
113
+ const segments = [];
114
+ const matched = [];
115
+ const placed = /* @__PURE__ */ new Set();
116
+ const visible = hidePartialMarker(text);
117
+ for (const paragraph of visible.split(PARAGRAPH_BREAK)) {
118
+ const pattern = new RegExp(PRODUCT_MARKER.source, PRODUCT_MARKER.flags);
119
+ const cards = [];
120
+ let match;
121
+ while ((match = pattern.exec(paragraph)) !== null) {
122
+ const product = byId.get(match[1]);
123
+ if (product === void 0 || placed.has(product.productId)) continue;
124
+ placed.add(product.productId);
125
+ matched.push(product);
126
+ cards.push(product);
127
+ }
128
+ const prose = stripMarkersFrom(paragraph);
129
+ if (prose !== "") segments.push({ type: "text", content: prose });
130
+ for (const product of cards) segments.push({ type: "product", product });
131
+ }
132
+ return { segments, matched, unmatched: products.filter((product) => !placed.has(product.productId)) };
133
+ }
134
+ function stripMarkers(text) {
135
+ return hidePartialMarker(text).replace(PRODUCT_MARKER, "");
136
+ }
137
+
138
+ // src/session.ts
139
+ function createSessionId() {
140
+ return `session-${crypto.randomUUID()}`;
141
+ }
142
+
143
+ // src/price.ts
144
+ function formatPrice(amount, currencyCode, locale) {
145
+ return new Intl.NumberFormat(locale, { style: "currency", currency: currencyCode }).format(amount / 100);
146
+ }
147
+
148
+ // src/errors.ts
149
+ var AgentError = class extends Error {
150
+ constructor(statusCode, body) {
151
+ super(`Agent error: ${statusCode}`);
152
+ this.statusCode = statusCode;
153
+ this.body = body;
154
+ this.name = "AgentError";
155
+ }
156
+ };
157
+ // Annotate the CommonJS export names for ESM import in node:
158
+ 0 && (module.exports = {
159
+ AgentError,
160
+ PRODUCT_MARKER,
161
+ collectReply,
162
+ createSessionId,
163
+ formatPrice,
164
+ interleaveProducts,
165
+ isWireEvent,
166
+ parseWireEvents,
167
+ stripMarkers
168
+ });
@@ -0,0 +1,102 @@
1
+ import { W as WireEvent, U as UnknownWireEvent, P as ProductCard } from './errors-CYbkUYHr.cjs';
2
+ export { A as AgentError, C as CartContext, a as CartLine, b as ProductCardVariant, S as StoreContext } from './errors-CYbkUYHr.cjs';
3
+
4
+ /**
5
+ * Narrows an event to one of the five this SDK knows.
6
+ *
7
+ * `switch (event.type)` cannot do it on its own: `UnknownWireEvent` carries an index signature, so it stays
8
+ * in the union on every branch and each field comes out as `unknown`. This is what makes a handler read the
9
+ * fields without casting.
10
+ *
11
+ * @example
12
+ * if (isWireEvent(event, 'text')) append(event.content)
13
+ */
14
+ declare function isWireEvent<T extends WireEvent['type']>(event: WireEvent | UnknownWireEvent, type: T): event is Extract<WireEvent, {
15
+ type: T;
16
+ }>;
17
+ /**
18
+ * The events in an NDJSON response body, one JSON object per line. Lines arrive split across chunk
19
+ * boundaries, so partial lines are buffered until their newline.
20
+ *
21
+ * An event type this SDK does not know is yielded through untouched — the backend adds types without a major
22
+ * bump, and dropping them here would hide the new one from a caller that does know it.
23
+ *
24
+ * The wire is bare, single-encoded NDJSON: no `data: ` prefix and no double encoding. Stripping a prefix or
25
+ * parsing twice "just in case" only ever masks a real framing change.
26
+ *
27
+ * @example
28
+ * for await (const event of parseWireEvents(response.body)) {
29
+ * if (event.type === 'text') append(event.content)
30
+ * }
31
+ */
32
+ declare function parseWireEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<WireEvent | UnknownWireEvent>;
33
+ /**
34
+ * A whole reply, for callers that render nothing until the agent is done. Streaming is what the protocol is
35
+ * for — reach for this only when there is no progressive UI to feed.
36
+ */
37
+ declare function collectReply(events: AsyncIterable<WireEvent | UnknownWireEvent>): Promise<{
38
+ text: string;
39
+ products: ProductCard[];
40
+ suggestions: string[];
41
+ }>;
42
+
43
+ /**
44
+ * The marker the agent writes after each product name: `[product:<productId>]`. The card's own id, never
45
+ * `productParentId` — several cards can share a parent, so a parent id would resolve to an arbitrary one.
46
+ *
47
+ * The hyphen in the character class is load-bearing. A product id is a merchant-chosen string over
48
+ * `[A-Za-z0-9_-]`, and hyphens are common in one — so a `\w+` pattern misses every id that carries one, and
49
+ * a reply whose ids all carry one loses every card.
50
+ */
51
+ declare const PRODUCT_MARKER: RegExp;
52
+ type ContentSegment = {
53
+ type: 'text';
54
+ content: string;
55
+ } | {
56
+ type: 'product';
57
+ product: ProductCard;
58
+ };
59
+ /**
60
+ * A reply split into the pieces to render in order: prose, with each product card below the paragraph whose
61
+ * marker named it. Markers are consumed, so no `[product:…]` reaches the shopper — including the half of one
62
+ * that the tail of a streaming reply holds.
63
+ *
64
+ * The cut is at the paragraph boundary rather than at the marker, because the agent writes the marker inside
65
+ * a sentence — straight after the product name. Cutting at the marker itself puts the card between "The
66
+ * Gore-Tex Offshore Jacket" and "is the shell", breaking the sentence across it. So each text segment is one
67
+ * paragraph, trimmed, and the cards it named follow it in the order they were written.
68
+ *
69
+ * A marker whose id has no card is stripped silently, which is what the backend expects — the prose already
70
+ * names the product. Cards no marker referenced come back as `unmatched`, to render as a shelf below the
71
+ * reply. A product referenced twice gets one card, under the paragraph that mentions it first.
72
+ *
73
+ * There is deliberately no fuzzy name matching: a fallback that guesses degrades silently instead of failing,
74
+ * and any word list it needs is tenant- and language-specific.
75
+ */
76
+ declare function interleaveProducts(text: string, products: ProductCard[]): {
77
+ segments: ContentSegment[];
78
+ matched: ProductCard[];
79
+ unmatched: ProductCard[];
80
+ };
81
+ /**
82
+ * The reply with every marker removed, for a caller that renders prose and cards separately. A partial
83
+ * marker at the end of a streaming reply goes too, for the same reason it does in `interleaveProducts`.
84
+ */
85
+ declare function stripMarkers(text: string): string;
86
+
87
+ /**
88
+ * A session id for one conversation. The agent requires a session id of at least 33 characters; a bare
89
+ * UUID is 36 and passes, but the prefix makes that constraint visible and the value greppable in logs.
90
+ *
91
+ * The first turn pins the session to its store group and market. A later turn naming a different store is
92
+ * refused, so switching store means a new id.
93
+ */
94
+ declare function createSessionId(): string;
95
+
96
+ /**
97
+ * A card's price, formatted for display. Every amount on the wire is in minor units, so `59900` in `SEK` is
98
+ * 599,00 kr. Pass the card's own `currencyCode` — it is per product, not per store.
99
+ */
100
+ declare function formatPrice(amount: number, currencyCode: string, locale?: string): string;
101
+
102
+ export { type ContentSegment, PRODUCT_MARKER, ProductCard, UnknownWireEvent, WireEvent, collectReply, createSessionId, formatPrice, interleaveProducts, isWireEvent, parseWireEvents, stripMarkers };
@@ -0,0 +1,102 @@
1
+ import { W as WireEvent, U as UnknownWireEvent, P as ProductCard } from './errors-CYbkUYHr.js';
2
+ export { A as AgentError, C as CartContext, a as CartLine, b as ProductCardVariant, S as StoreContext } from './errors-CYbkUYHr.js';
3
+
4
+ /**
5
+ * Narrows an event to one of the five this SDK knows.
6
+ *
7
+ * `switch (event.type)` cannot do it on its own: `UnknownWireEvent` carries an index signature, so it stays
8
+ * in the union on every branch and each field comes out as `unknown`. This is what makes a handler read the
9
+ * fields without casting.
10
+ *
11
+ * @example
12
+ * if (isWireEvent(event, 'text')) append(event.content)
13
+ */
14
+ declare function isWireEvent<T extends WireEvent['type']>(event: WireEvent | UnknownWireEvent, type: T): event is Extract<WireEvent, {
15
+ type: T;
16
+ }>;
17
+ /**
18
+ * The events in an NDJSON response body, one JSON object per line. Lines arrive split across chunk
19
+ * boundaries, so partial lines are buffered until their newline.
20
+ *
21
+ * An event type this SDK does not know is yielded through untouched — the backend adds types without a major
22
+ * bump, and dropping them here would hide the new one from a caller that does know it.
23
+ *
24
+ * The wire is bare, single-encoded NDJSON: no `data: ` prefix and no double encoding. Stripping a prefix or
25
+ * parsing twice "just in case" only ever masks a real framing change.
26
+ *
27
+ * @example
28
+ * for await (const event of parseWireEvents(response.body)) {
29
+ * if (event.type === 'text') append(event.content)
30
+ * }
31
+ */
32
+ declare function parseWireEvents(body: ReadableStream<Uint8Array>): AsyncGenerator<WireEvent | UnknownWireEvent>;
33
+ /**
34
+ * A whole reply, for callers that render nothing until the agent is done. Streaming is what the protocol is
35
+ * for — reach for this only when there is no progressive UI to feed.
36
+ */
37
+ declare function collectReply(events: AsyncIterable<WireEvent | UnknownWireEvent>): Promise<{
38
+ text: string;
39
+ products: ProductCard[];
40
+ suggestions: string[];
41
+ }>;
42
+
43
+ /**
44
+ * The marker the agent writes after each product name: `[product:<productId>]`. The card's own id, never
45
+ * `productParentId` — several cards can share a parent, so a parent id would resolve to an arbitrary one.
46
+ *
47
+ * The hyphen in the character class is load-bearing. A product id is a merchant-chosen string over
48
+ * `[A-Za-z0-9_-]`, and hyphens are common in one — so a `\w+` pattern misses every id that carries one, and
49
+ * a reply whose ids all carry one loses every card.
50
+ */
51
+ declare const PRODUCT_MARKER: RegExp;
52
+ type ContentSegment = {
53
+ type: 'text';
54
+ content: string;
55
+ } | {
56
+ type: 'product';
57
+ product: ProductCard;
58
+ };
59
+ /**
60
+ * A reply split into the pieces to render in order: prose, with each product card below the paragraph whose
61
+ * marker named it. Markers are consumed, so no `[product:…]` reaches the shopper — including the half of one
62
+ * that the tail of a streaming reply holds.
63
+ *
64
+ * The cut is at the paragraph boundary rather than at the marker, because the agent writes the marker inside
65
+ * a sentence — straight after the product name. Cutting at the marker itself puts the card between "The
66
+ * Gore-Tex Offshore Jacket" and "is the shell", breaking the sentence across it. So each text segment is one
67
+ * paragraph, trimmed, and the cards it named follow it in the order they were written.
68
+ *
69
+ * A marker whose id has no card is stripped silently, which is what the backend expects — the prose already
70
+ * names the product. Cards no marker referenced come back as `unmatched`, to render as a shelf below the
71
+ * reply. A product referenced twice gets one card, under the paragraph that mentions it first.
72
+ *
73
+ * There is deliberately no fuzzy name matching: a fallback that guesses degrades silently instead of failing,
74
+ * and any word list it needs is tenant- and language-specific.
75
+ */
76
+ declare function interleaveProducts(text: string, products: ProductCard[]): {
77
+ segments: ContentSegment[];
78
+ matched: ProductCard[];
79
+ unmatched: ProductCard[];
80
+ };
81
+ /**
82
+ * The reply with every marker removed, for a caller that renders prose and cards separately. A partial
83
+ * marker at the end of a streaming reply goes too, for the same reason it does in `interleaveProducts`.
84
+ */
85
+ declare function stripMarkers(text: string): string;
86
+
87
+ /**
88
+ * A session id for one conversation. The agent requires a session id of at least 33 characters; a bare
89
+ * UUID is 36 and passes, but the prefix makes that constraint visible and the value greppable in logs.
90
+ *
91
+ * The first turn pins the session to its store group and market. A later turn naming a different store is
92
+ * refused, so switching store means a new id.
93
+ */
94
+ declare function createSessionId(): string;
95
+
96
+ /**
97
+ * A card's price, formatted for display. Every amount on the wire is in minor units, so `59900` in `SEK` is
98
+ * 599,00 kr. Pass the card's own `currencyCode` — it is per product, not per store.
99
+ */
100
+ declare function formatPrice(amount: number, currencyCode: string, locale?: string): string;
101
+
102
+ export { type ContentSegment, PRODUCT_MARKER, ProductCard, UnknownWireEvent, WireEvent, collectReply, createSessionId, formatPrice, interleaveProducts, isWireEvent, parseWireEvents, stripMarkers };
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import {
2
+ AgentError
3
+ } from "./chunk-2MMTZSYZ.js";
4
+
5
+ // src/ndjson.ts
6
+ function parseLine(line) {
7
+ const trimmed = line.trim();
8
+ if (trimmed === "") return void 0;
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(trimmed);
12
+ } catch {
13
+ return void 0;
14
+ }
15
+ if (typeof parsed !== "object" || parsed === null) return void 0;
16
+ const event = parsed;
17
+ return typeof event.type === "string" ? event : void 0;
18
+ }
19
+ function isWireEvent(event, type) {
20
+ return event.type === type;
21
+ }
22
+ async function* parseWireEvents(body) {
23
+ const reader = body.getReader();
24
+ const decoder = new TextDecoder();
25
+ let buffer = "";
26
+ try {
27
+ for (; ; ) {
28
+ const { done, value } = await reader.read();
29
+ if (done) break;
30
+ buffer += decoder.decode(value, { stream: true });
31
+ let newline = buffer.indexOf("\n");
32
+ while (newline !== -1) {
33
+ const event = parseLine(buffer.slice(0, newline));
34
+ buffer = buffer.slice(newline + 1);
35
+ if (event !== void 0) yield event;
36
+ newline = buffer.indexOf("\n");
37
+ }
38
+ }
39
+ buffer += decoder.decode();
40
+ const last = parseLine(buffer);
41
+ if (last !== void 0) yield last;
42
+ } finally {
43
+ reader.releaseLock();
44
+ }
45
+ }
46
+ async function collectReply(events) {
47
+ let text = "";
48
+ let products = [];
49
+ let suggestions = [];
50
+ for await (const event of events) {
51
+ if (event.type === "text" && typeof event.content === "string") {
52
+ text += event.content;
53
+ continue;
54
+ }
55
+ if (event.type === "products" && Array.isArray(event.products)) {
56
+ products = event.products;
57
+ continue;
58
+ }
59
+ if (event.type === "suggestions" && Array.isArray(event.suggestions)) {
60
+ suggestions = event.suggestions;
61
+ }
62
+ }
63
+ return { text, products, suggestions };
64
+ }
65
+
66
+ // src/markers.ts
67
+ var PRODUCT_MARKER = /\[product:([\w-]+)\]/g;
68
+ var MARKER_OPEN = "[product:";
69
+ function hidePartialMarker(text) {
70
+ const open = text.lastIndexOf("[");
71
+ if (open === -1 || text.includes("]", open)) return text;
72
+ const tail = text.slice(open);
73
+ const partial = tail.startsWith(MARKER_OPEN) ? /^\[product:[\w-]*$/.test(tail) : MARKER_OPEN.startsWith(tail);
74
+ return partial ? text.slice(0, open) : text;
75
+ }
76
+ var PARAGRAPH_BREAK = /\n\s*\n/;
77
+ var MARKER_WITH_PADDING = /[^\S\r\n]*\[product:[\w-]+\][^\S\r\n]*/g;
78
+ function stripMarkersFrom(paragraph) {
79
+ return paragraph.replace(MARKER_WITH_PADDING, (gap) => /\s/.test(gap) ? " " : "").trim();
80
+ }
81
+ function interleaveProducts(text, products) {
82
+ const byId = new Map(products.map((product) => [product.productId, product]));
83
+ const segments = [];
84
+ const matched = [];
85
+ const placed = /* @__PURE__ */ new Set();
86
+ const visible = hidePartialMarker(text);
87
+ for (const paragraph of visible.split(PARAGRAPH_BREAK)) {
88
+ const pattern = new RegExp(PRODUCT_MARKER.source, PRODUCT_MARKER.flags);
89
+ const cards = [];
90
+ let match;
91
+ while ((match = pattern.exec(paragraph)) !== null) {
92
+ const product = byId.get(match[1]);
93
+ if (product === void 0 || placed.has(product.productId)) continue;
94
+ placed.add(product.productId);
95
+ matched.push(product);
96
+ cards.push(product);
97
+ }
98
+ const prose = stripMarkersFrom(paragraph);
99
+ if (prose !== "") segments.push({ type: "text", content: prose });
100
+ for (const product of cards) segments.push({ type: "product", product });
101
+ }
102
+ return { segments, matched, unmatched: products.filter((product) => !placed.has(product.productId)) };
103
+ }
104
+ function stripMarkers(text) {
105
+ return hidePartialMarker(text).replace(PRODUCT_MARKER, "");
106
+ }
107
+
108
+ // src/session.ts
109
+ function createSessionId() {
110
+ return `session-${crypto.randomUUID()}`;
111
+ }
112
+
113
+ // src/price.ts
114
+ function formatPrice(amount, currencyCode, locale) {
115
+ return new Intl.NumberFormat(locale, { style: "currency", currency: currencyCode }).format(amount / 100);
116
+ }
117
+ export {
118
+ AgentError,
119
+ PRODUCT_MARKER,
120
+ collectReply,
121
+ createSessionId,
122
+ formatPrice,
123
+ interleaveProducts,
124
+ isWireEvent,
125
+ parseWireEvents,
126
+ stripMarkers
127
+ };