create-skybridge 0.0.0-dev.df84369 → 0.0.0-dev.e051a6d
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.js +335 -210
- package/package.json +9 -8
- package/templates/blank/.dockerignore +4 -0
- package/templates/blank/Dockerfile +53 -0
- package/templates/blank/README.md +92 -0
- package/{template → templates/blank}/_gitignore +3 -1
- package/templates/blank/node_modules/.bin/alpic +21 -0
- package/templates/blank/node_modules/.bin/sb +21 -0
- package/templates/blank/node_modules/.bin/skybridge +21 -0
- package/{template → templates/blank}/node_modules/.bin/tsc +2 -2
- package/{template → templates/blank}/node_modules/.bin/tsserver +2 -2
- package/{template → templates/blank}/node_modules/.bin/tsx +2 -2
- package/templates/blank/node_modules/.bin/vite +21 -0
- package/templates/blank/package.json +29 -0
- package/templates/blank/src/helpers.ts +4 -0
- package/templates/blank/src/server.ts +16 -0
- package/templates/blank/tsconfig.json +5 -0
- package/templates/blank/vite.config.ts +6 -0
- package/templates/demo/.dockerignore +4 -0
- package/templates/demo/AGENTS.md +1 -0
- package/templates/demo/Dockerfile +53 -0
- package/templates/demo/README.md +95 -0
- package/templates/demo/_gitignore +7 -0
- package/templates/demo/alpic.json +3 -0
- package/templates/demo/node_modules/.bin/alpic +21 -0
- package/templates/demo/node_modules/.bin/sb +21 -0
- package/templates/demo/node_modules/.bin/skybridge +21 -0
- package/templates/demo/node_modules/.bin/tsc +21 -0
- package/templates/demo/node_modules/.bin/tsserver +21 -0
- package/templates/demo/node_modules/.bin/tsx +21 -0
- package/templates/demo/node_modules/.bin/vite +21 -0
- package/templates/demo/package.json +41 -0
- package/templates/demo/src/helpers.ts +4 -0
- package/templates/demo/src/index.css +59 -0
- package/templates/demo/src/server.ts +94 -0
- package/templates/demo/src/views/components/doc-link.tsx +22 -0
- package/templates/demo/src/views/components/doc.tsx +21 -0
- package/templates/demo/src/views/components/nav.tsx +31 -0
- package/templates/demo/src/views/components/progress.tsx +35 -0
- package/templates/demo/src/views/components/steps/outro.tsx +68 -0
- package/templates/demo/src/views/components/steps/state.tsx +47 -0
- package/templates/demo/src/views/components/steps/tool-call.tsx +53 -0
- package/templates/demo/src/views/components/steps/tool-output.tsx +40 -0
- package/templates/demo/src/views/images/mascot/beret.png +0 -0
- package/templates/demo/src/views/images/mascot/chapka.png +0 -0
- package/templates/demo/src/views/images/mascot/cowboy-hat.png +0 -0
- package/templates/demo/src/views/images/mascot/fez.png +0 -0
- package/templates/demo/src/views/images/mascot/jester-hat.png +0 -0
- package/templates/demo/src/views/images/mascot/mitre.png +0 -0
- package/templates/demo/src/views/images/mascot/non-la.png +0 -0
- package/templates/demo/src/views/images/mascot/original.png +0 -0
- package/templates/demo/src/views/images/mascot/propeller-beanie.png +0 -0
- package/templates/demo/src/views/images/mascot/ski-mask.png +0 -0
- package/templates/demo/src/views/images/mascot/sombrero.png +0 -0
- package/templates/demo/src/views/images/mascot/top-hat.png +0 -0
- package/templates/demo/src/views/images/mascot/viking-helmet.png +0 -0
- package/templates/demo/src/views/onboarding.tsx +63 -0
- package/templates/demo/src/views/use-mascot.ts +60 -0
- package/templates/demo/tsconfig.json +11 -0
- package/templates/demo/vite.config.ts +14 -0
- package/templates/ecom/.dockerignore +4 -0
- package/templates/ecom/.env.template +2 -0
- package/templates/ecom/AGENTS.md +2 -0
- package/templates/ecom/Dockerfile +53 -0
- package/templates/ecom/README.md +92 -0
- package/templates/ecom/_gitignore +8 -0
- package/templates/ecom/alpic.json +3 -0
- package/templates/ecom/node_modules/.bin/alpic +21 -0
- package/templates/ecom/node_modules/.bin/sb +21 -0
- package/templates/ecom/node_modules/.bin/skybridge +21 -0
- package/templates/ecom/node_modules/.bin/tsc +21 -0
- package/templates/ecom/node_modules/.bin/tsserver +21 -0
- package/templates/ecom/node_modules/.bin/tsx +21 -0
- package/templates/ecom/node_modules/.bin/vite +21 -0
- package/templates/ecom/package.json +29 -0
- package/templates/ecom/src/config.ts +12 -0
- package/templates/ecom/src/helpers.ts +4 -0
- package/templates/ecom/src/server.ts +43 -0
- package/templates/ecom/src/tools/render-carousel.ts +233 -0
- package/templates/ecom/src/tools/search-products.ts +182 -0
- package/templates/ecom/src/types.ts +13 -0
- package/templates/ecom/tsconfig.json +5 -0
- package/templates/ecom/vite.config.ts +6 -0
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -33
- package/template/README.md +0 -97
- package/template/node_modules/.bin/alpic +0 -21
- package/template/node_modules/.bin/sb +0 -21
- package/template/node_modules/.bin/skybridge +0 -21
- package/template/node_modules/.bin/vite +0 -21
- package/template/package.json +0 -33
- package/template/server/src/index.ts +0 -62
- package/template/tsconfig.json +0 -13
- package/template/web/src/helpers.ts +0 -4
- package/template/web/src/index.css +0 -154
- package/template/web/src/widgets/magic-8-ball.tsx +0 -27
- package/template/web/vite.config.ts +0 -15
- /package/{template → templates/blank}/AGENTS.md +0 -0
- /package/{template → templates/blank}/alpic.json +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CAROUSEL_MAX_SIZE, CAROUSEL_RANGE } from "../config.js";
|
|
3
|
+
import {
|
|
4
|
+
type Attribute,
|
|
5
|
+
AttributeSchema,
|
|
6
|
+
type Price,
|
|
7
|
+
PriceSchema,
|
|
8
|
+
} from "../types.js";
|
|
9
|
+
|
|
10
|
+
// The `render-carousel` tool: takes the IDs the model curated and returns the
|
|
11
|
+
// matching products for the carousel view to render.
|
|
12
|
+
// Everything this tool needs lives in this file.
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Product model
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Model: variant-as-full-product. Each `Variant` is a complete, buyable product
|
|
18
|
+
// (its own title, price, media). A `Product` ties sibling variants together and
|
|
19
|
+
// declares the axes (`Option`s) they vary on. A product with no variations is
|
|
20
|
+
// just a product with a single variant and no options.
|
|
21
|
+
|
|
22
|
+
// One selectable value on an axis, e.g. the "Black" choice on the "Color" axis.
|
|
23
|
+
type OptionValue = {
|
|
24
|
+
id: string; // stable key referenced by Variant.selection, e.g. "black"
|
|
25
|
+
label: string; // shown to the user, e.g. "Black"
|
|
26
|
+
media?: string; // optional swatch / image representing this value
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// A variation axis the variants differ on, e.g. Color or Size.
|
|
30
|
+
type Option = {
|
|
31
|
+
id: string; // stable key, used as a key in Variant.selection, e.g. "color"
|
|
32
|
+
label: string; // shown to the user, e.g. "Color"
|
|
33
|
+
values: OptionValue[]; // in display order
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Display fields shared by a Variant and by a product's `card`.
|
|
37
|
+
type Meta = {
|
|
38
|
+
title: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
price?: Price;
|
|
41
|
+
media: string[]; // images for this item; media[0] is the primary/cover
|
|
42
|
+
url?: string; // link to this item's external product page
|
|
43
|
+
outOfStock?: boolean; // true = not purchasable
|
|
44
|
+
// Facts to display: spec rows, promo labels, etc.
|
|
45
|
+
attributes: Attribute[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// One buyable product: full display Meta plus which value it takes on each axis.
|
|
49
|
+
type Variant = Meta & {
|
|
50
|
+
id: string; // SKU / article number; unique within the catalog
|
|
51
|
+
// The chosen value per axis: keys are Option.id, values are OptionValue.id.
|
|
52
|
+
// e.g. { color: "black", size: "40" }
|
|
53
|
+
selection: Record<string, string>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// A product: one carousel card backed by one or more variants and the axes they
|
|
57
|
+
// vary on (none for a single-variant product).
|
|
58
|
+
type Product = {
|
|
59
|
+
id: string; // stable product key
|
|
60
|
+
options: Option[]; // the axes the variants vary on
|
|
61
|
+
// Only the variants that actually exist. A missing combination (e.g. no
|
|
62
|
+
// { color: "black", size: "40" }) is simply absent from this list — that is how
|
|
63
|
+
// contingent variations are expressed. Derive the selectable values for an axis
|
|
64
|
+
// by filtering this list on the choices already made.
|
|
65
|
+
variants: Variant[];
|
|
66
|
+
// What the carousel card shows for the whole product. Optional: when omitted, the
|
|
67
|
+
// first variant is used. Set it to showcase a specific variant or give the product
|
|
68
|
+
// its own title/price, e.g. a "from" price spanning the variants.
|
|
69
|
+
card?: Meta;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Input
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const inputSchema = {
|
|
77
|
+
ids: z
|
|
78
|
+
.array(z.string())
|
|
79
|
+
.min(1)
|
|
80
|
+
.max(CAROUSEL_MAX_SIZE)
|
|
81
|
+
.describe("Product IDs to present, in display order."),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type RenderInput = z.infer<z.ZodObject<typeof inputSchema>>;
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Output — model-facing grounding, for the LLM ONLY. The carousel view is NOT
|
|
88
|
+
// built from this; it renders from the full data in `_meta`. Keep it to what the
|
|
89
|
+
// model needs to reference and compare the displayed products afterward.
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const outputSchema = {
|
|
93
|
+
products: z
|
|
94
|
+
.array(
|
|
95
|
+
z.object({
|
|
96
|
+
id: z.string().describe("Product SKU or reference."),
|
|
97
|
+
title: z.string(),
|
|
98
|
+
options: z
|
|
99
|
+
.array(z.object({ label: z.string(), values: z.array(z.string()) }))
|
|
100
|
+
.describe("Variations available (e.g. colors, sizes)."),
|
|
101
|
+
description: z.string().optional(),
|
|
102
|
+
price: PriceSchema.optional(),
|
|
103
|
+
outOfStock: z.boolean(),
|
|
104
|
+
attributes: z
|
|
105
|
+
.array(AttributeSchema)
|
|
106
|
+
.describe("Product facts (specs, promo labels…)."),
|
|
107
|
+
}),
|
|
108
|
+
)
|
|
109
|
+
.describe(
|
|
110
|
+
"The products shown in the carousel, in display order. For your reference only — to curate, compare, and answer follow-ups. Ground every claim in this data; never invent attributes.",
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type RenderOutput = z.infer<z.ZodObject<typeof outputSchema>>;
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Data access
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
async function getProducts(_ids: string[]): Promise<Product[]> {
|
|
121
|
+
// @todo: fetch each id from your product API / DB and map the results into
|
|
122
|
+
// `Product`s (rename `_ids` -> `ids` once you use it).
|
|
123
|
+
//
|
|
124
|
+
// First decide your mapping strategy — it depends on your catalog:
|
|
125
|
+
// - no variants (simple products): one `Product`, a single variant, options: []
|
|
126
|
+
// - grouped: one `Product` per product; `card` = union of its variants, one picture per requested variant
|
|
127
|
+
// - one card per requested variant: `card` = that variant
|
|
128
|
+
// Either way, set `variants` to ALL variants the source returns for the product;
|
|
129
|
+
// the detail view reads them so the client can switch variant.
|
|
130
|
+
//
|
|
131
|
+
// Returns [] for now, so the carousel is empty.
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Mapping — trim products to the model-facing grounding (outputSchema). The full
|
|
137
|
+
// data stays in `_meta` for the view. The card, else the first variant,
|
|
138
|
+
// represents the product; options collapse to label + value labels.
|
|
139
|
+
// @todo: choose what the model sees per product. Grounding only — no
|
|
140
|
+
// presentational data (media, styling); that rides in `_meta` for the view.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function toStructuredContent(products: Product[]): RenderOutput {
|
|
144
|
+
const groundingProducts: RenderOutput["products"] = [];
|
|
145
|
+
|
|
146
|
+
for (const product of products) {
|
|
147
|
+
const rep = product.card ?? product.variants[0];
|
|
148
|
+
|
|
149
|
+
let outOfStock = true;
|
|
150
|
+
for (const variant of product.variants) {
|
|
151
|
+
if (!variant.outOfStock) {
|
|
152
|
+
outOfStock = false;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const options: { label: string; values: string[] }[] = [];
|
|
158
|
+
for (const option of product.options) {
|
|
159
|
+
const values: string[] = [];
|
|
160
|
+
for (const value of option.values) {
|
|
161
|
+
values.push(value.label);
|
|
162
|
+
}
|
|
163
|
+
options.push({ label: option.label, values });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
groundingProducts.push({
|
|
167
|
+
id: product.id,
|
|
168
|
+
title: rep.title,
|
|
169
|
+
description: rep.description,
|
|
170
|
+
price: rep.price,
|
|
171
|
+
outOfStock,
|
|
172
|
+
options,
|
|
173
|
+
attributes: rep.attributes,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { products: groundingProducts };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Tool (registered from server.ts to keep the typed tool chain intact)
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
export const renderCarouselDefinition = {
|
|
185
|
+
name: "render-carousel" as const,
|
|
186
|
+
|
|
187
|
+
// @todo: adapt the wording to your catalog and brand voice (tone, vocabulary,
|
|
188
|
+
// how to present products). The behavioral rules below apply to any catalog.
|
|
189
|
+
description: `\
|
|
190
|
+
Display the products you curated as an inline carousel for the client.
|
|
191
|
+
|
|
192
|
+
## When to call
|
|
193
|
+
Call this AFTER searching and curating, and BEFORE writing your recommendation. Do not describe the products in text first; the carousel shows them. Stay silent until it has rendered, then speak.
|
|
194
|
+
|
|
195
|
+
## What to pass
|
|
196
|
+
Pass the IDs of the ${CAROUSEL_RANGE} products you chose, in display order (most relevant first). Order is significant: the carousel shows them in this exact order and your recommendation must follow the same sequence. Pass distinct products, not several variants of the same one; the detail view lets the client explore a product's variants (colors, sizes, and so on).
|
|
197
|
+
|
|
198
|
+
## After the carousel
|
|
199
|
+
Recommend in carousel order so the client can follow along. The cards already show image, title, price, and key facts, so do not repeat them: add useful analysis tied to the client's need. Suggest a refinement the client has not addressed yet (from the available filters), never one they already used.
|
|
200
|
+
|
|
201
|
+
## Accuracy
|
|
202
|
+
Use only the data returned for each product. Never invent attributes, materials, or availability. If the client asks about something not present, open that product's detail or search again before answering.`,
|
|
203
|
+
annotations: {
|
|
204
|
+
readOnlyHint: true,
|
|
205
|
+
openWorldHint: false,
|
|
206
|
+
destructiveHint: false,
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// @todo: customize the status messages shown in ChatGPT while the tool runs.
|
|
210
|
+
_meta: {
|
|
211
|
+
"openai/toolInvocation/invoking": "Loading product carousel",
|
|
212
|
+
"openai/toolInvocation/invoked": "Loaded product carousel",
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
inputSchema,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export async function renderCarouselHandler({ ids }: RenderInput) {
|
|
219
|
+
const products = await getProducts(ids);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
// Full products (incl. variants) for the view; not in model context.
|
|
223
|
+
_meta: { products },
|
|
224
|
+
structuredContent: toStructuredContent(products),
|
|
225
|
+
content: [
|
|
226
|
+
{
|
|
227
|
+
type: "text" as const,
|
|
228
|
+
text: `Rendered ${products.length} product(s) in the carousel.`,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
isError: false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
CAROUSEL_MAX_SIZE,
|
|
4
|
+
CAROUSEL_RANGE,
|
|
5
|
+
MIN_SEARCH_ITERATIONS,
|
|
6
|
+
} from "../config.js";
|
|
7
|
+
import { AttributeSchema, PriceSchema } from "../types.js";
|
|
8
|
+
|
|
9
|
+
// The `search-products` tool: keyword + filters in, matching products out as
|
|
10
|
+
// structured output for the model. It has NO view — include only what the model
|
|
11
|
+
// needs to curate (ids + properties), never presentational data (images, media);
|
|
12
|
+
// render-carousel handles that. Everything this tool needs lives in this file.
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Input
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const inputSchema = {
|
|
19
|
+
// @todo: tune this guidance to your catalog's vocabulary. The model reads
|
|
20
|
+
// it to turn conversational input into a good keyword.
|
|
21
|
+
keyword: z.string().describe(
|
|
22
|
+
`\
|
|
23
|
+
Short noun phrases extracted from conversational input. Never pass full sentences. \
|
|
24
|
+
ALWAYS use the singular form, not the plural. \
|
|
25
|
+
Include color, material, and style descriptors for accuracy. \
|
|
26
|
+
For vague gift or occasion queries without a clear product type, use broad category terms. \
|
|
27
|
+
Preserve the existing keyword across filter refinements; only replace when the user makes a completely different request.`,
|
|
28
|
+
),
|
|
29
|
+
|
|
30
|
+
// @todo: set the sort options your backend supports (or remove).
|
|
31
|
+
sort: z.enum(["price-asc", "price-desc"]).optional().describe("Sort order."),
|
|
32
|
+
|
|
33
|
+
// @todo: declare the filters your catalog supports. Add one param per
|
|
34
|
+
// facet (category, color, size...), each optional so the model only sends
|
|
35
|
+
// what the user asked for. `priceRange` below is just an example, remove it.
|
|
36
|
+
priceRange: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(
|
|
40
|
+
"Price range in dollars. Format: 'min-max' e.g. '0-500', '1000-2000'.",
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type SearchInput = z.infer<z.ZodObject<typeof inputSchema>>;
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Output — model-facing grounding, returned in structuredContent.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const productSchema = z.object({
|
|
51
|
+
id: z.string().describe("Stable product ID; pass to render-carousel."),
|
|
52
|
+
title: z.string(),
|
|
53
|
+
description: z.string().optional(),
|
|
54
|
+
price: PriceSchema.optional(),
|
|
55
|
+
outOfStock: z
|
|
56
|
+
.boolean()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("True when the product is not purchasable."),
|
|
59
|
+
attributes: z
|
|
60
|
+
.array(AttributeSchema)
|
|
61
|
+
.describe("Facts to curate on (specs, materials, promo labels…)."),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const outputSchema = {
|
|
65
|
+
products: z.array(productSchema).describe("Matching products, sorted."),
|
|
66
|
+
pages: z
|
|
67
|
+
.object({
|
|
68
|
+
current: z.number(),
|
|
69
|
+
total: z.number(),
|
|
70
|
+
})
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Pagination: current page and total page count."),
|
|
73
|
+
totalHits: z
|
|
74
|
+
.number()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Total matching products across all pages."),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type SearchOutput = z.infer<z.ZodObject<typeof outputSchema>>;
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Data access
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
function search(_input: SearchInput): SearchOutput {
|
|
86
|
+
// @todo: plug in your product API / DB. Query it with the input params and map
|
|
87
|
+
// each result into `products` below. `pages` and `totalHits` are optional.
|
|
88
|
+
return {
|
|
89
|
+
products: [], // { id: string; properties: { name: PropertyName; value: string[] }[] }[]
|
|
90
|
+
pages: { current: 1, total: 1 },
|
|
91
|
+
totalHits: 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Narration — framing + next-step instructions for the model. The products
|
|
97
|
+
// themselves ride in structuredContent; this text carries NO result data.
|
|
98
|
+
// @todo: customize the NEXT STEPS guidance below for your flow (keep searching
|
|
99
|
+
// vs. curate and render). Remind the model to ground claims in the structured
|
|
100
|
+
// results and never invent attributes.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function narrate({ products }: SearchOutput): string {
|
|
104
|
+
const size = products.length;
|
|
105
|
+
|
|
106
|
+
if (size === 0) {
|
|
107
|
+
return `\
|
|
108
|
+
No products found.
|
|
109
|
+
|
|
110
|
+
NEXT STEP: Broaden the keyword or relax filters, then search again.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (size < CAROUSEL_MAX_SIZE) {
|
|
114
|
+
return `\
|
|
115
|
+
Only a few results.
|
|
116
|
+
|
|
117
|
+
NEXT STEPS:
|
|
118
|
+
1. If the client asked for a specific product by name, these are fine: curate and call render-carousel with the selected IDs.
|
|
119
|
+
2. Otherwise, search again with broader terms before rendering.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `\
|
|
123
|
+
Enough results to present.
|
|
124
|
+
|
|
125
|
+
NEXT STEPS:
|
|
126
|
+
1. Curate the best matches for the client's intent from the structured results.
|
|
127
|
+
2. Write your recommendation mentioning the selected products.
|
|
128
|
+
3. Call render-carousel with the selected IDs.`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Tool (registered from server.ts to keep the typed tool chain intact)
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
export const searchProductsDefinition = {
|
|
136
|
+
name: "search-products" as const,
|
|
137
|
+
|
|
138
|
+
// @todo: describe YOUR catalog and how the agent should search and curate
|
|
139
|
+
// it. This is the model's main guide: be specific about your categories,
|
|
140
|
+
// the multi-call search loop, and when to render vs. keep searching.
|
|
141
|
+
description: `\
|
|
142
|
+
Search the product catalog. Handles any query: specific products, broad categories, gifts, occasions, or open discovery.
|
|
143
|
+
Never assume a category is unavailable: always search before responding.
|
|
144
|
+
|
|
145
|
+
The response is data only: a list of matching products (name, ID, price, description, and any product-specific attributes) plus pagination and the available filters. The raw results are for your eyes only: the client never sees them.
|
|
146
|
+
|
|
147
|
+
NEVER describe or characterize the raw results to the client: do not mention how many there are, what categories they fall into, or that they look off-topic. While searching, stay silent: do not narrate what you are doing, announce searches, or report progress between tool calls. Speak only once the carousel is rendered.
|
|
148
|
+
|
|
149
|
+
Act on the response as follows:
|
|
150
|
+
|
|
151
|
+
- FIRST SEARCH: use the keyword only (plus sort if needed). Read the available filters in the response; these are the only valid filter options for follow-ups.
|
|
152
|
+
- REFINEMENT: if the user's intent matches an available filter, apply it. If it doesn't match any filter, start a new keyword search.
|
|
153
|
+
- ITERATE: a single search is never enough. Run several searches (${MIN_SEARCH_ITERATIONS} minimum) before rendering: vary the keyword, apply a filter, or page deeper, and compare results across them. Quality comes from this multi-call exploration; one call then render gives shallow results.
|
|
154
|
+
- CURATION: read results and pick the best matches for the client's intent, grounding your choice in each product's description. If zero results, broaden the keyword or relax filters and search again: do NOT call render-carousel. If fewer than 3 results and the user didn't ask for a specific product, search again with broader terms. Only after exploring across several searches, call render-carousel with the selected IDs.
|
|
155
|
+
- PRESENT: Once you have ${CAROUSEL_RANGE} distinct, relevant products, call render-carousel with their IDs. Recommend ONLY AFTER the carousel displays, in carousel order.
|
|
156
|
+
|
|
157
|
+
The sweet spot is ${CAROUSEL_RANGE} products.
|
|
158
|
+
`,
|
|
159
|
+
annotations: {
|
|
160
|
+
readOnlyHint: true,
|
|
161
|
+
openWorldHint: false,
|
|
162
|
+
destructiveHint: false,
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// @todo: customize the status messages shown in ChatGPT while the tool runs.
|
|
166
|
+
_meta: {
|
|
167
|
+
"openai/toolInvocation/invoking": "Searching the catalog",
|
|
168
|
+
"openai/toolInvocation/invoked": "Searched the catalog",
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
inputSchema,
|
|
172
|
+
outputSchema,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export function searchProductsHandler(input: SearchInput) {
|
|
176
|
+
const results = search(input);
|
|
177
|
+
return {
|
|
178
|
+
structuredContent: results,
|
|
179
|
+
content: [{ type: "text" as const, text: narrate(results) }],
|
|
180
|
+
isError: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const PriceSchema = z.object({
|
|
4
|
+
amount: z.number(),
|
|
5
|
+
currency: z.string(),
|
|
6
|
+
});
|
|
7
|
+
export type Price = z.infer<typeof PriceSchema>;
|
|
8
|
+
|
|
9
|
+
export const AttributeSchema = z.object({
|
|
10
|
+
name: z.string(),
|
|
11
|
+
value: z.string(),
|
|
12
|
+
});
|
|
13
|
+
export type Attribute = z.infer<typeof AttributeSchema>;
|
package/dist/index.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/index.test.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { init } from "./index.js";
|
|
6
|
-
describe("create-skybridge", () => {
|
|
7
|
-
let tempDirName;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
tempDirName = `test-${randomBytes(2).toString("hex")}`;
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(path.join(process.cwd(), tempDirName), {
|
|
13
|
-
recursive: true,
|
|
14
|
-
force: true,
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
it("should copy the template", async () => {
|
|
18
|
-
const name = `../../${tempDirName}//project$`;
|
|
19
|
-
await init([name]);
|
|
20
|
-
await fs.access(path.join(process.cwd(), tempDirName, "project", ".gitignore"));
|
|
21
|
-
expect(fs.access(path.join(process.cwd(), tempDirName, "project", ".npmrc"))).rejects.toThrowError();
|
|
22
|
-
});
|
|
23
|
-
it("should download template from repo", async () => {
|
|
24
|
-
const name = `../../${tempDirName}//project$`;
|
|
25
|
-
await init([
|
|
26
|
-
name,
|
|
27
|
-
"--repo",
|
|
28
|
-
"github:alpic-ai/skybridge/examples/ecom-carousel",
|
|
29
|
-
]);
|
|
30
|
-
await fs.access(path.join(process.cwd(), tempDirName, "project", ".gitignore"));
|
|
31
|
-
expect(fs.access(path.join(process.cwd(), tempDirName, "project", ".npmrc"))).rejects.toThrowError();
|
|
32
|
-
});
|
|
33
|
-
});
|
package/template/README.md
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# Skybridge Starter
|
|
2
|
-
|
|
3
|
-
A minimal TypeScript template for building MCP and ChatGPT Apps with the [Skybridge](https://docs.skybridge.tech/home) framework.
|
|
4
|
-
|
|
5
|
-
## Getting Started
|
|
6
|
-
|
|
7
|
-
### Prerequisites
|
|
8
|
-
|
|
9
|
-
- Node.js 24+
|
|
10
|
-
- HTTP tunnel such as [Alpic tunnel](https://docs.alpic.ai/cli/tunnel) if you want to test with remote MCP hosts like ChatGPT or Claude.ai.
|
|
11
|
-
|
|
12
|
-
### Local Development
|
|
13
|
-
|
|
14
|
-
#### 1. Install
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install
|
|
18
|
-
# or
|
|
19
|
-
yarn install
|
|
20
|
-
# or
|
|
21
|
-
pnpm install
|
|
22
|
-
# or
|
|
23
|
-
bun install
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
#### 2. Start your local server
|
|
27
|
-
|
|
28
|
-
Run the development server from the root directory:
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm run dev
|
|
32
|
-
# or
|
|
33
|
-
yarn dev
|
|
34
|
-
# or
|
|
35
|
-
pnpm dev
|
|
36
|
-
# or
|
|
37
|
-
bun dev
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
This command starts:
|
|
41
|
-
- Your MCP server at `http://localhost:3000/mcp`.
|
|
42
|
-
- Skybridge DevTools UI at `http://localhost:3000/`.
|
|
43
|
-
|
|
44
|
-
#### 3. Project structure
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
├── server/
|
|
48
|
-
│ └── src/
|
|
49
|
-
│ └── index.ts # Server entry point
|
|
50
|
-
├── web/
|
|
51
|
-
│ ├── src/
|
|
52
|
-
│ │ ├── widgets/ # React components (one per widget)
|
|
53
|
-
│ │ ├── helpers.ts # Shared utilities
|
|
54
|
-
│ │ └── index.css # Global styles
|
|
55
|
-
│ └── vite.config.ts
|
|
56
|
-
├── alpic.json # Deployment config
|
|
57
|
-
├── nodemon.json # Dev server config
|
|
58
|
-
└── package.json
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Create your first widget
|
|
62
|
-
|
|
63
|
-
#### 1. Add a new widget
|
|
64
|
-
|
|
65
|
-
- Register a widget in `server/src/server.ts` with a unique name (e.g., `my-widget`) using [`registerWidget`](https://docs.skybridge.tech/api-reference/register-widget)
|
|
66
|
-
- Create a matching React component at `web/src/widgets/my-widget.tsx`. **The file name must match the widget name exactly**.
|
|
67
|
-
|
|
68
|
-
#### 2. Edit widgets with Hot Module Replacement (HMR)
|
|
69
|
-
|
|
70
|
-
Edit and save components in `web/src/widgets/` — changes will appear instantly inside your App.
|
|
71
|
-
|
|
72
|
-
#### 3. Edit server code
|
|
73
|
-
|
|
74
|
-
Modify files in `server/` and refresh the connection with your testing MCP Client to see the changes.
|
|
75
|
-
|
|
76
|
-
### Testing your App
|
|
77
|
-
|
|
78
|
-
You can test your App locally by using our DevTools UI on `localhost:3000` while running the `pnpm dev` command.
|
|
79
|
-
|
|
80
|
-
To test your app with other MCP Clients like ChatGPT, Claude or VSCode, see [Testing Your App](https://docs.skybridge.tech/quickstart/test-your-app).
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
## Deploy to Production
|
|
84
|
-
|
|
85
|
-
Skybridge is infrastructure vendor agnostic, and your app can be deployed on any cloud platform supporting MCP.
|
|
86
|
-
|
|
87
|
-
The simplest way to deploy your App in minutes is [Alpic](https://alpic.ai/).
|
|
88
|
-
1. Create an account on [Alpic platform](https://app.alpic.ai/).
|
|
89
|
-
2. Connect your GitHub repository to automatically deploy at each commit.
|
|
90
|
-
3. Use your remote App URL to connect it to MCP Clients, or use the Alpic Playground to easily test your App.
|
|
91
|
-
|
|
92
|
-
## Resources
|
|
93
|
-
- [Skybridge Documentation](https://docs.skybridge.tech/)
|
|
94
|
-
- [Apps SDK Documentation](https://developers.openai.com/apps-sdk)
|
|
95
|
-
- [MCP Apps Documentation](https://github.com/modelcontextprotocol/ext-apps/tree/main)
|
|
96
|
-
- [Model Context Protocol Documentation](https://modelcontextprotocol.io/)
|
|
97
|
-
- [Alpic Documentation](https://docs.alpic.ai/)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/alpic@1.102.2_@opentelemetry+api@1.9.0_arktype@2.1.27_rxjs@7.8.2_typescript@6.0.2/node_modules/alpic/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/alpic@1.102.2_@opentelemetry+api@1.9.0_arktype@2.1.27_rxjs@7.8.2_typescript@6.0.2/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/alpic@1.102.2_@opentelemetry+api@1.9.0_arktype@2.1.27_rxjs@7.8.2_typescript@6.0.2/node_modules/alpic/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/alpic@1.102.2_@opentelemetry+api@1.9.0_arktype@2.1.27_rxjs@7.8.2_typescript@6.0.2/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../alpic/bin/run.js" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../alpic/bin/run.js" "$@"
|
|
21
|
-
fi
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules/skybridge/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules/skybridge/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../skybridge/bin/run.js" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../skybridge/bin/run.js" "$@"
|
|
21
|
-
fi
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules/skybridge/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules/skybridge/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/skybridge@0.35.10_@modelcontextprotocol+sdk@1.28.0_zod@4.3.6__@skybridge+devtools@0.35._0d7046891e4ac5be15117fcc268d8dab/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../skybridge/bin/run.js" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../skybridge/bin/run.js" "$@"
|
|
21
|
-
fi
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
-
|
|
4
|
-
case `uname` in
|
|
5
|
-
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
-
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
-
basedir=`cygpath -w "$basedir"`
|
|
8
|
-
fi
|
|
9
|
-
;;
|
|
10
|
-
esac
|
|
11
|
-
|
|
12
|
-
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/vite@8.0.3_@types+node@25.5.0_esbuild@0.27.2_jiti@2.6.1_terser@5.44.1_tsx@4.21.0_yaml@2.8.2/node_modules/vite/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/vite@8.0.3_@types+node@25.5.0_esbuild@0.27.2_jiti@2.6.1_terser@5.44.1_tsx@4.21.0_yaml@2.8.2/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules"
|
|
14
|
-
else
|
|
15
|
-
export NODE_PATH="/home/runner/work/skybridge/skybridge/node_modules/.pnpm/vite@8.0.3_@types+node@25.5.0_esbuild@0.27.2_jiti@2.6.1_terser@5.44.1_tsx@4.21.0_yaml@2.8.2/node_modules/vite/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/vite@8.0.3_@types+node@25.5.0_esbuild@0.27.2_jiti@2.6.1_terser@5.44.1_tsx@4.21.0_yaml@2.8.2/node_modules:/home/runner/work/skybridge/skybridge/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
-
fi
|
|
17
|
-
if [ -x "$basedir/node" ]; then
|
|
18
|
-
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
|
19
|
-
else
|
|
20
|
-
exec node "$basedir/../vite/bin/vite.js" "$@"
|
|
21
|
-
fi
|