@noodleseed/agent-kit 0.14.0 → 0.16.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.
Files changed (118) hide show
  1. package/manifest.json +549 -9
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +6 -4
  4. package/skills/claude-code/examples/acme-bistro/README.md +51 -0
  5. package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +435 -0
  6. package/skills/claude-code/examples/acme-bistro/design/api-contract.md +161 -0
  7. package/skills/claude-code/examples/acme-bistro/design/wireframe.html +573 -0
  8. package/skills/claude-code/examples/acme-bistro/noodle.json +5 -0
  9. package/skills/claude-code/examples/acme-bistro/package.json +20 -0
  10. package/skills/claude-code/examples/acme-bistro/src/helpers.ts +7 -0
  11. package/skills/claude-code/examples/acme-bistro/src/server.ts +149 -0
  12. package/skills/claude-code/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
  13. package/skills/claude-code/examples/acme-bistro/src/views/widget-style.css +228 -0
  14. package/skills/claude-code/examples/acme-bistro/test/server.test.ts +21 -0
  15. package/skills/claude-code/examples/acme-bistro/vitest.config.ts +7 -0
  16. package/skills/claude-code/examples/acme-discovery/README.md +85 -0
  17. package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +441 -0
  18. package/skills/claude-code/examples/acme-discovery/design/wireframe.html +670 -0
  19. package/skills/claude-code/examples/acme-discovery/noodle.json +5 -0
  20. package/skills/claude-code/examples/acme-discovery/package.json +20 -0
  21. package/skills/claude-code/examples/acme-discovery/src/helpers.ts +7 -0
  22. package/skills/claude-code/examples/acme-discovery/src/server.ts +202 -0
  23. package/skills/claude-code/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
  24. package/skills/claude-code/examples/acme-discovery/src/views/widget-style.css +207 -0
  25. package/skills/claude-code/examples/acme-discovery/test/server.test.ts +24 -0
  26. package/skills/claude-code/examples/acme-discovery/vitest.config.ts +7 -0
  27. package/skills/claude-code/examples/acme-tasks/README.md +80 -0
  28. package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +277 -0
  29. package/skills/claude-code/examples/acme-tasks/design/wireframe.html +603 -0
  30. package/skills/claude-code/examples/acme-tasks/noodle.json +5 -0
  31. package/skills/claude-code/examples/acme-tasks/package.json +20 -0
  32. package/skills/claude-code/examples/acme-tasks/src/helpers.ts +6 -0
  33. package/skills/claude-code/examples/acme-tasks/src/server.ts +141 -0
  34. package/skills/claude-code/examples/acme-tasks/src/views/task-list.tsx +177 -0
  35. package/skills/claude-code/examples/acme-tasks/src/views/widget-style.css +227 -0
  36. package/skills/claude-code/examples/acme-tasks/test/server.test.ts +22 -0
  37. package/skills/claude-code/examples/acme-tasks/vitest.config.ts +7 -0
  38. package/skills/claude-code/examples/food-ordering/README.md +82 -0
  39. package/skills/claude-code/examples/food-ordering/noodle.json +5 -0
  40. package/skills/claude-code/examples/food-ordering/package.json +22 -0
  41. package/skills/claude-code/examples/food-ordering/src/helpers.ts +34 -0
  42. package/skills/claude-code/examples/food-ordering/src/server.ts +412 -0
  43. package/skills/claude-code/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
  44. package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
  45. package/skills/claude-code/examples/food-ordering/src/views/widget-style.css +682 -0
  46. package/skills/claude-code/examples/food-ordering/test/server.test.ts +58 -0
  47. package/skills/claude-code/examples/food-ordering/vitest.config.ts +30 -0
  48. package/skills/claude-code/examples/hello/README.md +13 -0
  49. package/skills/claude-code/examples/hello/noodle.json +5 -0
  50. package/skills/claude-code/examples/hello/package.json +16 -0
  51. package/skills/claude-code/examples/hello/src/server.ts +29 -0
  52. package/skills/claude-code/examples/hello/test/server.test.ts +8 -0
  53. package/skills/claude-code/examples/weather/README.md +85 -0
  54. package/skills/claude-code/examples/weather/noodle.json +4 -0
  55. package/skills/claude-code/examples/weather/package.json +16 -0
  56. package/skills/claude-code/examples/weather/src/server.ts +261 -0
  57. package/skills/claude-code/examples/weather/test/server.test.ts +29 -0
  58. package/skills/claude-code/references/connect-an-api.md +63 -1
  59. package/skills/claude-code/references/examples.md +16 -7
  60. package/skills/claude-code/references/troubleshooting.md +2 -0
  61. package/skills/codex/SKILL.md +6 -4
  62. package/skills/codex/examples/acme-bistro/README.md +51 -0
  63. package/skills/codex/examples/acme-bistro/design/UX-Document.md +435 -0
  64. package/skills/codex/examples/acme-bistro/design/api-contract.md +161 -0
  65. package/skills/codex/examples/acme-bistro/design/wireframe.html +573 -0
  66. package/skills/codex/examples/acme-bistro/noodle.json +5 -0
  67. package/skills/codex/examples/acme-bistro/package.json +20 -0
  68. package/skills/codex/examples/acme-bistro/src/helpers.ts +7 -0
  69. package/skills/codex/examples/acme-bistro/src/server.ts +149 -0
  70. package/skills/codex/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
  71. package/skills/codex/examples/acme-bistro/src/views/widget-style.css +228 -0
  72. package/skills/codex/examples/acme-bistro/test/server.test.ts +21 -0
  73. package/skills/codex/examples/acme-bistro/vitest.config.ts +7 -0
  74. package/skills/codex/examples/acme-discovery/README.md +85 -0
  75. package/skills/codex/examples/acme-discovery/design/UX-Document.md +441 -0
  76. package/skills/codex/examples/acme-discovery/design/wireframe.html +670 -0
  77. package/skills/codex/examples/acme-discovery/noodle.json +5 -0
  78. package/skills/codex/examples/acme-discovery/package.json +20 -0
  79. package/skills/codex/examples/acme-discovery/src/helpers.ts +7 -0
  80. package/skills/codex/examples/acme-discovery/src/server.ts +202 -0
  81. package/skills/codex/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
  82. package/skills/codex/examples/acme-discovery/src/views/widget-style.css +207 -0
  83. package/skills/codex/examples/acme-discovery/test/server.test.ts +24 -0
  84. package/skills/codex/examples/acme-discovery/vitest.config.ts +7 -0
  85. package/skills/codex/examples/acme-tasks/README.md +80 -0
  86. package/skills/codex/examples/acme-tasks/design/UX-Document.md +277 -0
  87. package/skills/codex/examples/acme-tasks/design/wireframe.html +603 -0
  88. package/skills/codex/examples/acme-tasks/noodle.json +5 -0
  89. package/skills/codex/examples/acme-tasks/package.json +20 -0
  90. package/skills/codex/examples/acme-tasks/src/helpers.ts +6 -0
  91. package/skills/codex/examples/acme-tasks/src/server.ts +141 -0
  92. package/skills/codex/examples/acme-tasks/src/views/task-list.tsx +177 -0
  93. package/skills/codex/examples/acme-tasks/src/views/widget-style.css +227 -0
  94. package/skills/codex/examples/acme-tasks/test/server.test.ts +22 -0
  95. package/skills/codex/examples/acme-tasks/vitest.config.ts +7 -0
  96. package/skills/codex/examples/food-ordering/README.md +82 -0
  97. package/skills/codex/examples/food-ordering/noodle.json +5 -0
  98. package/skills/codex/examples/food-ordering/package.json +22 -0
  99. package/skills/codex/examples/food-ordering/src/helpers.ts +34 -0
  100. package/skills/codex/examples/food-ordering/src/server.ts +412 -0
  101. package/skills/codex/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
  102. package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
  103. package/skills/codex/examples/food-ordering/src/views/widget-style.css +682 -0
  104. package/skills/codex/examples/food-ordering/test/server.test.ts +58 -0
  105. package/skills/codex/examples/food-ordering/vitest.config.ts +30 -0
  106. package/skills/codex/examples/hello/README.md +13 -0
  107. package/skills/codex/examples/hello/noodle.json +5 -0
  108. package/skills/codex/examples/hello/package.json +16 -0
  109. package/skills/codex/examples/hello/src/server.ts +29 -0
  110. package/skills/codex/examples/hello/test/server.test.ts +8 -0
  111. package/skills/codex/examples/weather/README.md +85 -0
  112. package/skills/codex/examples/weather/noodle.json +4 -0
  113. package/skills/codex/examples/weather/package.json +16 -0
  114. package/skills/codex/examples/weather/src/server.ts +261 -0
  115. package/skills/codex/examples/weather/test/server.test.ts +29 -0
  116. package/skills/codex/references/connect-an-api.md +63 -1
  117. package/skills/codex/references/examples.md +16 -7
  118. package/skills/codex/references/troubleshooting.md +2 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "entrypoint": "src/server.ts",
3
+ "name": "acme-bistro",
4
+ "template": "widget"
5
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "acme-bistro",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "vitest run",
8
+ "validate": "noodle validate",
9
+ "dev": "noodle dev",
10
+ "deploy": "noodle deploy"
11
+ },
12
+ "devDependencies": {
13
+ "@vitejs/plugin-react": "latest",
14
+ "@noodleseed/one": "latest",
15
+ "react": "latest",
16
+ "react-dom": "latest",
17
+ "vite": "latest",
18
+ "vitest": "latest"
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ import type { ServerDefinition } from '@noodleseed/one';
2
+ import { generateHelpers } from '@noodleseed/one/react';
3
+
4
+ export type AppType = ServerDefinition;
5
+
6
+ export const { useCallTool, useLayout, useOpenExternal, useToolInfo, useViewState } =
7
+ generateHelpers<AppType>();
@@ -0,0 +1,149 @@
1
+ import { annotations, server, tool, toolForWidget, toolWithWidget, z } from '@noodleseed/one';
2
+
3
+ // Acme Bistro is a fictional restaurant. This app is deliberately END-TO-END: the customer browses the
4
+ // menu and builds the order in chat, and the order completes in chat — only *payment* hands off, via a
5
+ // checkout deep link (the card never touches the app). It is the flagship for the end-to-end pattern.
6
+ //
7
+ // Authoring notes:
8
+ // - A tool `fulfil` is *recorded*, not run as live JS. Inline inputs directly as `${input.x}` into
9
+ // output strings; do not transform them (no arithmetic/encode/filter) or substitution breaks. The
10
+ // cart total is summed in the widget (live React), not in a `fulfil`. The menu below is static data.
11
+
12
+ const menu = [
13
+ { id: 'stone_pizza', name: 'Stone-baked Margherita', price: 14, kind: 'Mains' },
14
+ { id: 'roast_bowl', name: 'Harvest Roast Bowl', price: 13, kind: 'Mains' },
15
+ { id: 'house_salad', name: 'House Garden Salad', price: 11, kind: 'Starters' },
16
+ { id: 'lemon_tart', name: 'Lemon Tart', price: 8, kind: 'Desserts' },
17
+ { id: 'sparkling', name: 'Sparkling Water', price: 4, kind: 'Drinks' },
18
+ ] as const;
19
+
20
+ const itemId = z.enum(['stone_pizza', 'roast_bowl', 'house_salad', 'lemon_tart', 'sparkling']);
21
+
22
+ const menuItemOutput = z.object({
23
+ id: z.string(),
24
+ name: z.string(),
25
+ price: z.number(),
26
+ kind: z.string(),
27
+ });
28
+
29
+ // Tool annotations for host planners: the menu read is read-only; cart edits are local writes; checkout
30
+ // opens an external (payment) link.
31
+ const readOnly = annotations.readOnly();
32
+ const localWrite = annotations.localAction({ destructive: false });
33
+ const openLink = annotations.openAction();
34
+
35
+ export default server(
36
+ 'acme_bistro',
37
+ {
38
+ title: 'Acme Bistro',
39
+ version: '1.0.0',
40
+ branding: {
41
+ name: 'Acme Bistro',
42
+ accent: '#B91C1C',
43
+ surface: '#FEF3F2',
44
+ surfaceDark: '#1A1211',
45
+ radius: 'lg',
46
+ density: 'comfortable',
47
+ },
48
+ // Payment is the only off-app step; the compiler derives ChatGPT redirect_domains from this so the
49
+ // signed checkout link opens without a safe-link warning. The card never reaches this app.
50
+ handoff: {
51
+ allowedDomains: ['https://pay.acme.example', 'https://acme.example'],
52
+ },
53
+ },
54
+ [
55
+ toolWithWidget('show_menu', {
56
+ description: 'Show the Acme Bistro menu and render the ordering widget.',
57
+ annotations: readOnly,
58
+ input: z.object({ customer: z.string().default('Guest') }),
59
+ output: z.object({
60
+ status: z.string(),
61
+ customer: z.string(),
62
+ items: z.array(menuItemOutput),
63
+ }),
64
+ fulfil: ({ input }) => ({
65
+ status: `Acme Bistro menu is ready for ${input.customer}. Build the order here; pay at checkout.`,
66
+ customer: input.customer,
67
+ items: menu,
68
+ }),
69
+ widgetTitle: 'Order at Acme Bistro',
70
+ widgetDescription: 'Browse the menu, build an order in chat, and hand off to pay.',
71
+ invoking: 'Loading the menu…',
72
+ invoked: 'Menu ready',
73
+ domain: 'https://order.acme.example',
74
+ view: {
75
+ component: 'menu-cart',
76
+ entry: './views/menu-cart.tsx',
77
+ },
78
+ csp: {
79
+ connectDomains: ['https://acme.example'],
80
+ resourceDomains: ['https://acme.example'],
81
+ frameDomains: ['https://acme.example'],
82
+ },
83
+ }),
84
+ // Widget-only cart edits — the model fills the item from natural language ("add two margheritas").
85
+ toolForWidget('add_to_cart', {
86
+ description: 'Add a menu item to the Acme Bistro order from the widget.',
87
+ annotations: localWrite,
88
+ input: z.object({
89
+ customer: z.string().default('Guest'),
90
+ item: itemId.default('stone_pizza'),
91
+ quantity: z.number().int().min(1).default(1),
92
+ notes: z.string().default(''),
93
+ }),
94
+ output: z.object({
95
+ status: z.string(),
96
+ item: z.string(),
97
+ quantity: z.number(),
98
+ notes: z.string(),
99
+ }),
100
+ fulfil: ({ input }) => ({
101
+ status: `Added ${input.quantity} × ${input.item} for ${input.customer}.`,
102
+ item: input.item,
103
+ quantity: input.quantity,
104
+ notes: input.notes,
105
+ }),
106
+ }),
107
+ toolForWidget('remove_from_cart', {
108
+ description: 'Remove a menu item from the Acme Bistro order.',
109
+ annotations: localWrite,
110
+ input: z.object({
111
+ customer: z.string().default('Guest'),
112
+ item: itemId.default('stone_pizza'),
113
+ }),
114
+ output: z.object({
115
+ status: z.string(),
116
+ item: z.string(),
117
+ }),
118
+ fulfil: ({ input }) => ({
119
+ status: `Removed ${input.item} for ${input.customer}.`,
120
+ item: input.item,
121
+ }),
122
+ }),
123
+ // The only handoff: payment. The widget computes the total (live React) and passes a url-safe cart
124
+ // token + total; the card is entered on Acme's PCI-scoped checkout, never in chat.
125
+ tool('create_checkout', {
126
+ description:
127
+ 'Create the Acme Bistro payment checkout link for the current order. Pass a url-safe cart token ' +
128
+ 'and the numeric total computed in the widget. Payment happens off-app; the card never reaches this app.',
129
+ annotations: openLink,
130
+ input: z.object({
131
+ customer: z.string().default('Guest'),
132
+ cartToken: z.string().default('cart'),
133
+ total: z.number().min(0).default(0),
134
+ }),
135
+ output: z.object({
136
+ status: z.string(),
137
+ summary: z.string(),
138
+ checkoutUrl: z.string(),
139
+ }),
140
+ // Do not place a literal `$` immediately before a token (`$${input.total}`) — it collides with the
141
+ // `${…}` substitution syntax and leaves the token unresolved. Keep the amount token standalone.
142
+ fulfil: ({ input }) => ({
143
+ status: `Ready to pay for ${input.customer}'s order.`,
144
+ summary: `${input.customer}'s Acme Bistro order · ${input.total} USD`,
145
+ checkoutUrl: `https://pay.acme.example/checkout?cart=${input.cartToken}&total=${input.total}&src=chatgpt`,
146
+ }),
147
+ }),
148
+ ],
149
+ );
@@ -0,0 +1,159 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { useCallTool, useLayout, useOpenExternal, useToolInfo, useViewState } from '../helpers.js';
3
+ import './widget-style.css';
4
+
5
+ type MenuItem = {
6
+ readonly id: string;
7
+ readonly name: string;
8
+ readonly price: number;
9
+ readonly kind: string;
10
+ };
11
+
12
+ function asMenu(value: unknown) {
13
+ return value as
14
+ | { readonly status?: string; readonly customer?: string; readonly items?: readonly MenuItem[] }
15
+ | undefined;
16
+ }
17
+
18
+ export default function MenuCart() {
19
+ const { displayMode, theme } = useLayout();
20
+ const openExternal = useOpenExternal();
21
+ const menuResult = asMenu(useToolInfo('show_menu').structuredContent);
22
+ const addToCart = useCallTool('add_to_cart');
23
+ const removeFromCart = useCallTool('remove_from_cart');
24
+ const checkout = useCallTool('create_checkout');
25
+
26
+ const items = menuResult?.items ?? [];
27
+ const [customer] = useViewState('customer', menuResult?.customer ?? 'Guest');
28
+ // Cart is session-local (id → quantity); the total is summed here in live React, not in a recorded fulfil.
29
+ const [cart, setCart] = useState<Record<string, number>>({});
30
+ const [status, setStatus] = useState(
31
+ menuResult?.status ?? 'Build your order, then check out to pay.',
32
+ );
33
+
34
+ const total = useMemo(
35
+ () => items.reduce((sum, item) => sum + item.price * (cart[item.id] ?? 0), 0),
36
+ [items, cart],
37
+ );
38
+ const lineCount = Object.values(cart).reduce((n, q) => n + q, 0);
39
+
40
+ async function add(item: MenuItem) {
41
+ setCart((current) => ({ ...current, [item.id]: (current[item.id] ?? 0) + 1 }));
42
+ const result = await addToCart.callTool({ customer, item: item.id, quantity: 1 });
43
+ const structured = result.structuredContent as { readonly status?: string } | undefined;
44
+ setStatus(structured?.status ?? `Added ${item.name}.`);
45
+ }
46
+
47
+ async function remove(item: MenuItem) {
48
+ setCart((current) => {
49
+ const next = { ...current };
50
+ const q = (next[item.id] ?? 0) - 1;
51
+ if (q <= 0) delete next[item.id];
52
+ else next[item.id] = q;
53
+ return next;
54
+ });
55
+ await removeFromCart.callTool({ customer, item: item.id });
56
+ }
57
+
58
+ async function payNow() {
59
+ if (lineCount === 0) return;
60
+ // The only handoff: payment. A url-safe cart token + the numeric total go to Acme's PCI checkout.
61
+ const cartToken = items
62
+ .filter((item) => cart[item.id])
63
+ .map((item) => `${item.id}x${cart[item.id]}`)
64
+ .join('-');
65
+ const result = await checkout.callTool({ customer, cartToken, total });
66
+ const structured = result.structuredContent as { readonly checkoutUrl?: string } | undefined;
67
+ if (structured?.checkoutUrl) openExternal(structured.checkoutUrl);
68
+ }
69
+
70
+ return (
71
+ <main
72
+ className={`nw-shell${theme === 'dark' ? ' dark' : ''}`}
73
+ data-llm={`Acme Bistro order for ${customer}: ${lineCount} item(s), total $${total}`}
74
+ >
75
+ <section className="nw-card">
76
+ <header className="nw-header">
77
+ <span className="nw-icon" aria-hidden="true">
78
+ <PlateIcon />
79
+ </span>
80
+ <div className="nw-title-block">
81
+ <h1 className="nw-title">Acme Bistro</h1>
82
+ <p className="nw-subtitle">{status}</p>
83
+ </div>
84
+ <span className="nw-chip">
85
+ {displayMode === 'fullscreen' ? 'Fullscreen' : `${lineCount} in cart`}
86
+ </span>
87
+ </header>
88
+
89
+ <ul className="nw-menu">
90
+ {items.map((item) => (
91
+ <li className="nw-row" key={item.id}>
92
+ <span className="nw-row-main">
93
+ <span className="nw-name">{item.name}</span>
94
+ <span className="nw-kind">{item.kind}</span>
95
+ </span>
96
+ <span className="nw-price">${item.price}</span>
97
+ <span className="nw-qty">
98
+ <button
99
+ aria-label={`Remove one ${item.name}`}
100
+ className="nw-step"
101
+ type="button"
102
+ disabled={!cart[item.id]}
103
+ onClick={() => remove(item)}
104
+ >
105
+
106
+ </button>
107
+ <span className="nw-count">{cart[item.id] ?? 0}</span>
108
+ <button
109
+ aria-label={`Add one ${item.name}`}
110
+ className="nw-step"
111
+ type="button"
112
+ onClick={() => add(item)}
113
+ >
114
+ +
115
+ </button>
116
+ </span>
117
+ </li>
118
+ ))}
119
+ </ul>
120
+
121
+ <footer className="nw-footer">
122
+ <span className="nw-total">
123
+ Total <strong>${total}</strong>
124
+ </span>
125
+ <button
126
+ className="nw-button nw-button-primary"
127
+ type="button"
128
+ disabled={lineCount === 0 || checkout.isPending}
129
+ onClick={payNow}
130
+ >
131
+ <CardIcon />
132
+ {checkout.isPending ? 'Opening checkout…' : 'Check out & pay'}
133
+ </button>
134
+ </footer>
135
+ <p className="nw-note">
136
+ Payment happens on acme.example — your card is never entered in chat.
137
+ </p>
138
+ </section>
139
+ </main>
140
+ );
141
+ }
142
+
143
+ function PlateIcon() {
144
+ return (
145
+ <svg viewBox="0 0 24 24" aria-hidden="true">
146
+ <circle cx="12" cy="12" r="9" />
147
+ <circle cx="12" cy="12" r="4" />
148
+ </svg>
149
+ );
150
+ }
151
+
152
+ function CardIcon() {
153
+ return (
154
+ <svg viewBox="0 0 24 24" aria-hidden="true">
155
+ <rect x="3" y="5" width="18" height="14" rx="2" />
156
+ <path d="M3 10h18" />
157
+ </svg>
158
+ );
159
+ }
@@ -0,0 +1,228 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family:
4
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
5
+ --nw-bg: #ffffff;
6
+ --nw-surface: #fdf4f3;
7
+ --nw-text: #1f1413;
8
+ --nw-muted: #7a5f5c;
9
+ --nw-border: #efd9d6;
10
+ --nw-accent: #b91c1c;
11
+ --nw-accent-strong: #991b1b;
12
+ --nw-accent-soft: #fdeae8;
13
+ --nw-radius: 10px;
14
+ --nw-shadow: 0 18px 50px rgb(60 20 20 / 12%);
15
+ }
16
+
17
+ .dark,
18
+ [data-theme="dark"] {
19
+ --nw-bg: #1a1211;
20
+ --nw-surface: #241816;
21
+ --nw-text: #f8ecea;
22
+ --nw-muted: #c3a29e;
23
+ --nw-border: #43302d;
24
+ --nw-accent: #f87171;
25
+ --nw-accent-strong: #ef4444;
26
+ --nw-accent-soft: #3a1f1d;
27
+ --nw-shadow: 0 18px 50px rgb(0 0 0 / 32%);
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ body {
35
+ margin: 0;
36
+ background: var(--nw-bg);
37
+ color: var(--nw-text);
38
+ }
39
+
40
+ button {
41
+ font: inherit;
42
+ }
43
+
44
+ .nw-shell {
45
+ min-height: 100vh;
46
+ padding: 14px;
47
+ background: var(--nw-bg);
48
+ color: var(--nw-text);
49
+ }
50
+
51
+ .nw-card {
52
+ max-width: 560px;
53
+ margin: 0 auto;
54
+ background: var(--nw-surface);
55
+ border: 1px solid var(--nw-border);
56
+ border-radius: var(--nw-radius);
57
+ box-shadow: var(--nw-shadow);
58
+ overflow: hidden;
59
+ }
60
+
61
+ .nw-header {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 12px;
65
+ padding: 16px;
66
+ border-bottom: 1px solid var(--nw-border);
67
+ }
68
+
69
+ .nw-icon svg {
70
+ width: 24px;
71
+ height: 24px;
72
+ fill: none;
73
+ stroke: var(--nw-accent);
74
+ stroke-width: 1.7;
75
+ stroke-linecap: round;
76
+ stroke-linejoin: round;
77
+ }
78
+
79
+ .nw-title-block {
80
+ flex: 1;
81
+ min-width: 0;
82
+ }
83
+
84
+ .nw-title {
85
+ margin: 0;
86
+ font-size: 17px;
87
+ font-weight: 700;
88
+ }
89
+
90
+ .nw-subtitle {
91
+ margin: 2px 0 0;
92
+ font-size: 13px;
93
+ color: var(--nw-muted);
94
+ }
95
+
96
+ .nw-chip {
97
+ padding: 4px 10px;
98
+ border-radius: 999px;
99
+ background: var(--nw-accent-soft);
100
+ color: var(--nw-accent-strong);
101
+ font-size: 12px;
102
+ font-weight: 600;
103
+ }
104
+
105
+ .nw-menu {
106
+ list-style: none;
107
+ margin: 0;
108
+ padding: 8px 16px;
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 4px;
112
+ }
113
+
114
+ .nw-row {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 12px;
118
+ padding: 10px 0;
119
+ border-bottom: 1px dashed var(--nw-border);
120
+ }
121
+
122
+ .nw-row-main {
123
+ flex: 1;
124
+ min-width: 0;
125
+ display: flex;
126
+ flex-direction: column;
127
+ }
128
+
129
+ .nw-name {
130
+ font-weight: 600;
131
+ }
132
+
133
+ .nw-kind {
134
+ font-size: 12px;
135
+ color: var(--nw-muted);
136
+ }
137
+
138
+ .nw-price {
139
+ color: var(--nw-accent-strong);
140
+ font-weight: 600;
141
+ font-size: 13px;
142
+ }
143
+
144
+ .nw-qty {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ gap: 8px;
148
+ }
149
+
150
+ .nw-step {
151
+ width: 26px;
152
+ height: 26px;
153
+ border: 1px solid var(--nw-border);
154
+ border-radius: 8px;
155
+ background: var(--nw-bg);
156
+ color: var(--nw-text);
157
+ cursor: pointer;
158
+ }
159
+
160
+ .nw-step:disabled {
161
+ opacity: 0.4;
162
+ cursor: default;
163
+ }
164
+
165
+ .nw-count {
166
+ min-width: 16px;
167
+ text-align: center;
168
+ font-variant-numeric: tabular-nums;
169
+ }
170
+
171
+ .nw-footer {
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: space-between;
175
+ gap: 12px;
176
+ padding: 12px 16px 4px;
177
+ }
178
+
179
+ .nw-total {
180
+ font-size: 14px;
181
+ color: var(--nw-muted);
182
+ }
183
+
184
+ .nw-total strong {
185
+ color: var(--nw-text);
186
+ font-size: 16px;
187
+ }
188
+
189
+ .nw-button {
190
+ display: inline-flex;
191
+ align-items: center;
192
+ gap: 6px;
193
+ padding: 9px 14px;
194
+ border: 1px solid var(--nw-border);
195
+ border-radius: 10px;
196
+ background: var(--nw-bg);
197
+ color: var(--nw-text);
198
+ cursor: pointer;
199
+ }
200
+
201
+ .nw-button svg {
202
+ width: 16px;
203
+ height: 16px;
204
+ fill: none;
205
+ stroke: currentColor;
206
+ stroke-width: 1.7;
207
+ stroke-linecap: round;
208
+ stroke-linejoin: round;
209
+ }
210
+
211
+ .nw-button-primary {
212
+ background: var(--nw-accent);
213
+ border-color: var(--nw-accent);
214
+ color: #ffffff;
215
+ font-weight: 600;
216
+ }
217
+
218
+ .nw-button-primary:disabled {
219
+ opacity: 0.6;
220
+ cursor: default;
221
+ }
222
+
223
+ .nw-note {
224
+ margin: 0;
225
+ padding: 8px 16px 16px;
226
+ font-size: 12px;
227
+ color: var(--nw-muted);
228
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import app from '../src/server.js';
3
+
4
+ describe('acme-bistro example', () => {
5
+ it('exports a Noodle server definition', () => {
6
+ expect(typeof app.toManifest).toBe('function');
7
+ });
8
+
9
+ it('declares the payment-only handoff domain', async () => {
10
+ // End-to-end: the order completes in chat; only payment hands off to Acme's checkout.
11
+ const text = JSON.stringify(await app.toManifest());
12
+ expect(text).toContain('https://pay.acme.example');
13
+ });
14
+
15
+ it('exposes the menu widget, cart helpers, and checkout tool', async () => {
16
+ const text = JSON.stringify(await app.toManifest());
17
+ expect(text).toContain('show_menu');
18
+ expect(text).toContain('add_to_cart');
19
+ expect(text).toContain('create_checkout');
20
+ });
21
+ });
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ // Local config so `npm test` (vitest run) discovers this example's own tests instead of inheriting a
4
+ // parent monorepo config's include globs.
5
+ export default defineConfig({
6
+ test: { include: ['test/**/*.test.ts'] },
7
+ });
@@ -0,0 +1,85 @@
1
+ # Acme Getaways — top-of-funnel discovery → handoff
2
+
3
+ A Noodle MCP App for **Acme Getaways**, a fictional travel brand. It is the flagship for the
4
+ **top-of-funnel discovery → handoff** pattern: discovery and configuration happen inside ChatGPT; the
5
+ booking/transaction happens off-app on Acme's own site, reached through a signed, attributable handoff
6
+ deep link. It pairs a `toolWithWidget` discovery carousel with a model-visible `create_handoff` tool and
7
+ server-level `handoff.allowedDomains`.
8
+
9
+ Capability slots: top-of-funnel funnel discipline, discovery carousel widget, `create_handoff` deep-link
10
+ handoff with attribution, `handoff.allowedDomains`, and a worked **design-first** artifact (the UX spec +
11
+ wireframe below). It shows the "design the experience, then build it" flow the `noodle-seed` skill's
12
+ `references/experience-design.md` teaches.
13
+
14
+ ## Design spec (write this before the code)
15
+
16
+ - **Funnel boundary** — Discover and shortlist a getaway in ChatGPT. Booking, payment, and the account
17
+ live on `acme.example`, reached only after the handoff. No payment or per-user auth in chat.
18
+ - **Personas** — the undecided browser ("somewhere warm in June?"), the near-decided planner (has a vibe
19
+ and month, wants options and a fast handoff).
20
+ - **Top-3 prioritized user flows**
21
+ 1. **Discover** — "warm beach trip in June for 2" → `discover_getaways` renders the carousel.
22
+ 2. **Shortlist** — pick a destination in the widget → `shortlist_getaway` (widget-only) records it.
23
+ 3. **Handoff** — "Continue on Acme" → `create_handoff` emits the deep link → opens off-app.
24
+ - **Tools** — `discover_getaways` (model-visible, renders the widget), `create_handoff` (model-visible,
25
+ emits the deep link), `shortlist_getaway` (widget-only helper, hidden from the model).
26
+ - **Widgets + display modes** — `DiscoveryCarousel` as an inline card that expands to fullscreen for
27
+ browsing. No picture-in-picture (nothing is live/ongoing).
28
+ - **Grounding sources** — the destination catalog in `src/server.ts` is Acme's own data; the widget never
29
+ invents a place, price, or best-month.
30
+ - **Handoff domains** — `https://book.acme.example`, `https://acme.example` (the server
31
+ `handoff.allowedDomains`; the deep link carries `dest`, `month`, `pax`, and `src=chatgpt` for
32
+ attribution).
33
+
34
+ ## Wireframe (one screen, then the off-app destination)
35
+
36
+ The carousel screen is in-app (solid frame); the booking screen is off-app (dashed frame) and reached
37
+ only after the handoff — it is Acme's own page, never wireframed as if it were in chat.
38
+
39
+ ```html
40
+ <div class="phone"> <!-- in-app: solid frame -->
41
+ <div class="chatgpt-header">ChatGPT · Acme Getaways</div>
42
+ <div class="msg user">somewhere warm in June, 2 of us</div>
43
+ <div class="tool-call">discover_getaways { vibe: "beach", month: "June", travelers: 2 }</div>
44
+ <div class="wcard">
45
+ <div class="wcard-head">DiscoveryCarousel</div> <!-- component name = code + spec -->
46
+ <div class="wcard-body">
47
+ <div class="dest">Coral Bay · from $890 · best May–Sep [Shortlist]</div>
48
+ <div class="dest">Harbor City · from $980 · best Sep–Nov [Shortlist]</div>
49
+ <a class="cta">Continue on Acme · Coral Bay</a> <!-- one primary action -->
50
+ </div>
51
+ </div>
52
+ </div>
53
+ <div class="phone offapp"> <!-- off-app: dashed frame -->
54
+ <div class="browser-header">book.acme.example/plan?dest=coral_bay&month=June&pax=2&src=chatgpt</div>
55
+ <div class="offapp-body">Acme booking — payment & account live here.</div>
56
+ </div>
57
+ ```
58
+
59
+ ## Local author loop
60
+
61
+ ```sh
62
+ noodle validate
63
+ noodle test
64
+ noodle dev
65
+ ```
66
+
67
+ In another terminal:
68
+
69
+ ```sh
70
+ noodle tools list
71
+ noodle tools call discover_getaways --args '{"vibe":"beach","month":"June","travelers":2}'
72
+ noodle tools call create_handoff --args '{"destination":"coral_bay","destinationName":"Coral Bay","month":"June","travelers":2}'
73
+ noodle check --target chatgpt
74
+ ```
75
+
76
+ ## Deploy
77
+
78
+ ```sh
79
+ noodle link --org demo --app acme-discovery
80
+ noodle deploy --access owner-only
81
+ noodle open
82
+ ```
83
+
84
+ This example has no connector secrets and does not include tokens, caller-key mechanisms, or
85
+ `.env.noodle` values. All destinations, prices, and URLs are fictional.