@noodleseed/agent-kit 0.13.0 → 0.15.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 (121) hide show
  1. package/README.md +3 -3
  2. package/manifest.json +575 -5
  3. package/package.json +1 -1
  4. package/skills/claude-code/SKILL.md +7 -2
  5. package/skills/claude-code/examples/acme-bistro/README.md +51 -0
  6. package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +435 -0
  7. package/skills/claude-code/examples/acme-bistro/design/api-contract.md +161 -0
  8. package/skills/claude-code/examples/acme-bistro/design/wireframe.html +573 -0
  9. package/skills/claude-code/examples/acme-bistro/noodle.json +5 -0
  10. package/skills/claude-code/examples/acme-bistro/package.json +20 -0
  11. package/skills/claude-code/examples/acme-bistro/src/helpers.ts +7 -0
  12. package/skills/claude-code/examples/acme-bistro/src/server.ts +149 -0
  13. package/skills/claude-code/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
  14. package/skills/claude-code/examples/acme-bistro/src/views/widget-style.css +228 -0
  15. package/skills/claude-code/examples/acme-bistro/test/server.test.ts +21 -0
  16. package/skills/claude-code/examples/acme-bistro/vitest.config.ts +7 -0
  17. package/skills/claude-code/examples/acme-discovery/README.md +85 -0
  18. package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +441 -0
  19. package/skills/claude-code/examples/acme-discovery/design/wireframe.html +670 -0
  20. package/skills/claude-code/examples/acme-discovery/noodle.json +5 -0
  21. package/skills/claude-code/examples/acme-discovery/package.json +20 -0
  22. package/skills/claude-code/examples/acme-discovery/src/helpers.ts +7 -0
  23. package/skills/claude-code/examples/acme-discovery/src/server.ts +202 -0
  24. package/skills/claude-code/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
  25. package/skills/claude-code/examples/acme-discovery/src/views/widget-style.css +207 -0
  26. package/skills/claude-code/examples/acme-discovery/test/server.test.ts +24 -0
  27. package/skills/claude-code/examples/acme-discovery/vitest.config.ts +7 -0
  28. package/skills/claude-code/examples/acme-tasks/README.md +80 -0
  29. package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +277 -0
  30. package/skills/claude-code/examples/acme-tasks/design/wireframe.html +603 -0
  31. package/skills/claude-code/examples/acme-tasks/noodle.json +5 -0
  32. package/skills/claude-code/examples/acme-tasks/package.json +20 -0
  33. package/skills/claude-code/examples/acme-tasks/src/helpers.ts +6 -0
  34. package/skills/claude-code/examples/acme-tasks/src/server.ts +141 -0
  35. package/skills/claude-code/examples/acme-tasks/src/views/task-list.tsx +177 -0
  36. package/skills/claude-code/examples/acme-tasks/src/views/widget-style.css +227 -0
  37. package/skills/claude-code/examples/acme-tasks/test/server.test.ts +22 -0
  38. package/skills/claude-code/examples/acme-tasks/vitest.config.ts +7 -0
  39. package/skills/claude-code/examples/food-ordering/README.md +82 -0
  40. package/skills/claude-code/examples/food-ordering/noodle.json +5 -0
  41. package/skills/claude-code/examples/food-ordering/package.json +22 -0
  42. package/skills/claude-code/examples/food-ordering/src/helpers.ts +34 -0
  43. package/skills/claude-code/examples/food-ordering/src/server.ts +412 -0
  44. package/skills/claude-code/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
  45. package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
  46. package/skills/claude-code/examples/food-ordering/src/views/widget-style.css +682 -0
  47. package/skills/claude-code/examples/food-ordering/test/server.test.ts +58 -0
  48. package/skills/claude-code/examples/food-ordering/vitest.config.ts +30 -0
  49. package/skills/claude-code/examples/hello/README.md +13 -0
  50. package/skills/claude-code/examples/hello/noodle.json +5 -0
  51. package/skills/claude-code/examples/hello/package.json +16 -0
  52. package/skills/claude-code/examples/hello/src/server.ts +29 -0
  53. package/skills/claude-code/examples/hello/test/server.test.ts +8 -0
  54. package/skills/claude-code/examples/weather/README.md +54 -0
  55. package/skills/claude-code/examples/weather/noodle.json +4 -0
  56. package/skills/claude-code/examples/weather/package.json +16 -0
  57. package/skills/claude-code/examples/weather/src/server.ts +190 -0
  58. package/skills/claude-code/examples/weather/test/server.test.ts +8 -0
  59. package/skills/claude-code/references/chatgpt-compliance.md +63 -0
  60. package/skills/claude-code/references/connect-an-api.md +76 -0
  61. package/skills/claude-code/references/examples.md +16 -4
  62. package/skills/claude-code/references/experience-design.md +142 -0
  63. package/skills/codex/SKILL.md +7 -2
  64. package/skills/codex/examples/acme-bistro/README.md +51 -0
  65. package/skills/codex/examples/acme-bistro/design/UX-Document.md +435 -0
  66. package/skills/codex/examples/acme-bistro/design/api-contract.md +161 -0
  67. package/skills/codex/examples/acme-bistro/design/wireframe.html +573 -0
  68. package/skills/codex/examples/acme-bistro/noodle.json +5 -0
  69. package/skills/codex/examples/acme-bistro/package.json +20 -0
  70. package/skills/codex/examples/acme-bistro/src/helpers.ts +7 -0
  71. package/skills/codex/examples/acme-bistro/src/server.ts +149 -0
  72. package/skills/codex/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
  73. package/skills/codex/examples/acme-bistro/src/views/widget-style.css +228 -0
  74. package/skills/codex/examples/acme-bistro/test/server.test.ts +21 -0
  75. package/skills/codex/examples/acme-bistro/vitest.config.ts +7 -0
  76. package/skills/codex/examples/acme-discovery/README.md +85 -0
  77. package/skills/codex/examples/acme-discovery/design/UX-Document.md +441 -0
  78. package/skills/codex/examples/acme-discovery/design/wireframe.html +670 -0
  79. package/skills/codex/examples/acme-discovery/noodle.json +5 -0
  80. package/skills/codex/examples/acme-discovery/package.json +20 -0
  81. package/skills/codex/examples/acme-discovery/src/helpers.ts +7 -0
  82. package/skills/codex/examples/acme-discovery/src/server.ts +202 -0
  83. package/skills/codex/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
  84. package/skills/codex/examples/acme-discovery/src/views/widget-style.css +207 -0
  85. package/skills/codex/examples/acme-discovery/test/server.test.ts +24 -0
  86. package/skills/codex/examples/acme-discovery/vitest.config.ts +7 -0
  87. package/skills/codex/examples/acme-tasks/README.md +80 -0
  88. package/skills/codex/examples/acme-tasks/design/UX-Document.md +277 -0
  89. package/skills/codex/examples/acme-tasks/design/wireframe.html +603 -0
  90. package/skills/codex/examples/acme-tasks/noodle.json +5 -0
  91. package/skills/codex/examples/acme-tasks/package.json +20 -0
  92. package/skills/codex/examples/acme-tasks/src/helpers.ts +6 -0
  93. package/skills/codex/examples/acme-tasks/src/server.ts +141 -0
  94. package/skills/codex/examples/acme-tasks/src/views/task-list.tsx +177 -0
  95. package/skills/codex/examples/acme-tasks/src/views/widget-style.css +227 -0
  96. package/skills/codex/examples/acme-tasks/test/server.test.ts +22 -0
  97. package/skills/codex/examples/acme-tasks/vitest.config.ts +7 -0
  98. package/skills/codex/examples/food-ordering/README.md +82 -0
  99. package/skills/codex/examples/food-ordering/noodle.json +5 -0
  100. package/skills/codex/examples/food-ordering/package.json +22 -0
  101. package/skills/codex/examples/food-ordering/src/helpers.ts +34 -0
  102. package/skills/codex/examples/food-ordering/src/server.ts +412 -0
  103. package/skills/codex/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
  104. package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
  105. package/skills/codex/examples/food-ordering/src/views/widget-style.css +682 -0
  106. package/skills/codex/examples/food-ordering/test/server.test.ts +58 -0
  107. package/skills/codex/examples/food-ordering/vitest.config.ts +30 -0
  108. package/skills/codex/examples/hello/README.md +13 -0
  109. package/skills/codex/examples/hello/noodle.json +5 -0
  110. package/skills/codex/examples/hello/package.json +16 -0
  111. package/skills/codex/examples/hello/src/server.ts +29 -0
  112. package/skills/codex/examples/hello/test/server.test.ts +8 -0
  113. package/skills/codex/examples/weather/README.md +54 -0
  114. package/skills/codex/examples/weather/noodle.json +4 -0
  115. package/skills/codex/examples/weather/package.json +16 -0
  116. package/skills/codex/examples/weather/src/server.ts +190 -0
  117. package/skills/codex/examples/weather/test/server.test.ts +8 -0
  118. package/skills/codex/references/chatgpt-compliance.md +63 -0
  119. package/skills/codex/references/connect-an-api.md +76 -0
  120. package/skills/codex/references/examples.md +16 -4
  121. package/skills/codex/references/experience-design.md +142 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ "entrypoint": "src/server.ts",
3
+ "name": "acme-tasks",
4
+ "template": "widget"
5
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "acme-tasks",
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,6 @@
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, useToolInfo, useViewState } = generateHelpers<AppType>();
@@ -0,0 +1,141 @@
1
+ import { annotations, server, tool, toolForWidget, toolWithWidget, z } from '@noodleseed/one';
2
+
3
+ // Acme Tasks is a fictional productivity app. It is a two-way (read + write) experience rather than a
4
+ // top-of-funnel handoff: the top-3 prioritized user flows all complete in chat — Capture, Prioritize,
5
+ // and Complete. It is the flagship for designing an app around its prioritized flows (see the README).
6
+ //
7
+ // Authoring notes:
8
+ // - A tool `fulfil` is *recorded*, not run as live JS. Place inputs directly into an output string as
9
+ // `${input.x}` so they substitute at runtime; do not transform them (no arithmetic/filter on inputs).
10
+ // The seed list below is static data the runtime returns verbatim — the "today" view.
11
+ // - A real deployment would connect the user's account with the end-user auth pattern (see the
12
+ // `customer-auth` example); this example keeps a seeded list so the focus stays on the flows.
13
+
14
+ const today = [
15
+ {
16
+ id: 'email_vendor',
17
+ title: 'Email the vendor about the Q3 quote',
18
+ priority: 'high',
19
+ done: false,
20
+ },
21
+ { id: 'review_pr', title: 'Review the analytics pull request', priority: 'medium', done: false },
22
+ { id: 'book_offsite', title: 'Book flights for the team offsite', priority: 'low', done: false },
23
+ ] as const;
24
+
25
+ const priority = z.enum(['high', 'medium', 'low']);
26
+
27
+ // Tool annotations for host planners: listing is read-only; capture/complete/re-prioritize are local
28
+ // non-destructive writes.
29
+ const readOnly = annotations.readOnly();
30
+ const localWrite = annotations.localAction({ destructive: false });
31
+
32
+ const taskOutput = z.object({
33
+ id: z.string(),
34
+ title: z.string(),
35
+ priority,
36
+ done: z.boolean(),
37
+ });
38
+
39
+ export default server(
40
+ 'acme_tasks',
41
+ {
42
+ title: 'Acme Tasks',
43
+ version: '1.0.0',
44
+ branding: {
45
+ name: 'Acme Tasks',
46
+ accent: '#7C3AED',
47
+ surface: '#F5F3FF',
48
+ surfaceDark: '#161228',
49
+ radius: 'lg',
50
+ density: 'comfortable',
51
+ },
52
+ },
53
+ [
54
+ // Flow 2 — Prioritize / Today: render the list so the human triages and the model can speak to it.
55
+ toolWithWidget('list_today', {
56
+ description: 'Show today’s Acme Tasks and render the task-list widget.',
57
+ annotations: readOnly,
58
+ input: z.object({ focus: z.string().default('today') }),
59
+ output: z.object({
60
+ status: z.string(),
61
+ focus: z.string(),
62
+ tasks: z.array(taskOutput),
63
+ }),
64
+ fulfil: ({ input }) => ({
65
+ status: `Acme Tasks for ${input.focus}: ${today.length} open items, highest priority first.`,
66
+ focus: input.focus,
67
+ tasks: today,
68
+ }),
69
+ widgetTitle: 'Today’s tasks',
70
+ widgetDescription: 'A prioritized task list: capture, re-prioritize, and complete in place.',
71
+ // ChatGPT host status copy (openai/toolInvocation/*) — required for widget-opening tools.
72
+ invoking: 'Loading your tasks…',
73
+ invoked: 'Tasks ready',
74
+ domain: 'https://tasks.acme.example',
75
+ view: {
76
+ component: 'task-list',
77
+ entry: './views/task-list.tsx',
78
+ },
79
+ csp: {
80
+ connectDomains: ['https://acme.example'],
81
+ resourceDomains: ['https://acme.example'],
82
+ frameDomains: ['https://acme.example'],
83
+ },
84
+ }),
85
+ // Flow 1 — Capture: add a task from natural language ("remind me to email the vendor").
86
+ tool('add_task', {
87
+ description: 'Capture a new Acme task with a title and priority.',
88
+ annotations: localWrite,
89
+ input: z.object({
90
+ title: z.string(),
91
+ priority: priority.default('medium'),
92
+ }),
93
+ output: z.object({
94
+ status: z.string(),
95
+ title: z.string(),
96
+ priority,
97
+ }),
98
+ fulfil: ({ input }) => ({
99
+ status: `Added “${input.title}” (${input.priority}).`,
100
+ title: input.title,
101
+ priority: input.priority,
102
+ }),
103
+ }),
104
+ // Flow 3 — Complete: mark a task done. Model-visible so the model can complete on request.
105
+ tool('complete_task', {
106
+ description: 'Mark an Acme task complete. Pass the task id and its title.',
107
+ annotations: localWrite,
108
+ input: z.object({
109
+ task: z.string(),
110
+ title: z.string().min(1),
111
+ }),
112
+ output: z.object({
113
+ status: z.string(),
114
+ task: z.string(),
115
+ }),
116
+ fulfil: ({ input }) => ({
117
+ status: `Completed “${input.title}”.`,
118
+ task: input.task,
119
+ }),
120
+ }),
121
+ // Flow 2 helper (widget-only): re-prioritize a task from the list widget.
122
+ toolForWidget('set_priority', {
123
+ description: 'Re-prioritize a task from the list widget.',
124
+ annotations: localWrite,
125
+ input: z.object({
126
+ task: z.string(),
127
+ priority,
128
+ }),
129
+ output: z.object({
130
+ status: z.string(),
131
+ task: z.string(),
132
+ priority,
133
+ }),
134
+ fulfil: ({ input }) => ({
135
+ status: `Set ${input.task} to ${input.priority} priority.`,
136
+ task: input.task,
137
+ priority: input.priority,
138
+ }),
139
+ }),
140
+ ],
141
+ );
@@ -0,0 +1,177 @@
1
+ import { useState } from 'react';
2
+ import { useCallTool, useLayout, useToolInfo, useViewState } from '../helpers.js';
3
+ import './widget-style.css';
4
+
5
+ type Priority = 'high' | 'medium' | 'low';
6
+
7
+ type Task = {
8
+ readonly id: string;
9
+ readonly title: string;
10
+ readonly priority: Priority;
11
+ readonly done: boolean;
12
+ };
13
+
14
+ const PRIORITIES: readonly Priority[] = ['high', 'medium', 'low'];
15
+
16
+ function asToday(value: unknown) {
17
+ return value as { readonly status?: string; readonly tasks?: readonly Task[] } | undefined;
18
+ }
19
+
20
+ export default function TaskList() {
21
+ const { displayMode, theme } = useLayout();
22
+ const today = asToday(useToolInfo('list_today').structuredContent);
23
+ const addTask = useCallTool('add_task');
24
+ const completeTask = useCallTool('complete_task');
25
+ const setPriorityTool = useCallTool('set_priority');
26
+
27
+ const seeded = today?.tasks ?? [];
28
+ // Local, session-scoped state layered over the seeded list — the three flows write here and record
29
+ // the change through a tool call. Captured tasks live in `added` so the list updates in place.
30
+ const [added, setAdded] = useState<readonly Task[]>([]);
31
+ const [done, setDone] = useState<readonly string[]>([]);
32
+ const [priority, setPriority] = useState<Record<string, Priority>>({});
33
+ const [draft, setDraft] = useViewState('draft', '');
34
+ const [status, setStatus] = useState(today?.status ?? 'Capture, prioritize, and complete.');
35
+
36
+ const tasks: readonly Task[] = [...seeded, ...added];
37
+ const openCount = tasks.filter((task) => !done.includes(task.id)).length;
38
+
39
+ // Each flow updates local state optimistically, then records the change through a tool call. If the
40
+ // call fails, revert the optimistic change and surface a failure message instead of a false success.
41
+ async function capture() {
42
+ const title = draft.trim();
43
+ if (title.length === 0) return;
44
+ const id = `added_${added.length}`;
45
+ setAdded((current) => [...current, { id, title, priority: 'medium', done: false }]);
46
+ setDraft('');
47
+ try {
48
+ const result = await addTask.callTool({ title, priority: 'medium' });
49
+ const structured = result.structuredContent as { readonly status?: string } | undefined;
50
+ setStatus(structured?.status ?? `Added “${title}”.`);
51
+ } catch {
52
+ setAdded((current) => current.filter((task) => task.id !== id));
53
+ setStatus(`Couldn't add “${title}” — try again.`);
54
+ }
55
+ }
56
+
57
+ async function reprioritize(task: Task, next: Priority) {
58
+ const prev = priority[task.id];
59
+ setPriority((current) => ({ ...current, [task.id]: next }));
60
+ try {
61
+ const result = await setPriorityTool.callTool({ task: task.id, priority: next });
62
+ const structured = result.structuredContent as { readonly status?: string } | undefined;
63
+ setStatus(structured?.status ?? `Set ${task.title} to ${next}.`);
64
+ } catch {
65
+ setPriority((current) => {
66
+ const restored = { ...current };
67
+ if (prev === undefined) delete restored[task.id];
68
+ else restored[task.id] = prev;
69
+ return restored;
70
+ });
71
+ setStatus(`Couldn't re-prioritize ${task.title} — try again.`);
72
+ }
73
+ }
74
+
75
+ async function complete(task: Task) {
76
+ setDone((current) => [...current, task.id]);
77
+ try {
78
+ const result = await completeTask.callTool({ task: task.id, title: task.title });
79
+ const structured = result.structuredContent as { readonly status?: string } | undefined;
80
+ setStatus(structured?.status ?? `Completed “${task.title}”.`);
81
+ } catch {
82
+ setDone((current) => current.filter((id) => id !== task.id));
83
+ setStatus(`Couldn't complete “${task.title}” — try again.`);
84
+ }
85
+ }
86
+
87
+ return (
88
+ <main
89
+ className={`nw-shell${theme === 'dark' ? ' dark' : ''}`}
90
+ data-llm={`Acme Tasks: ${openCount} open of ${tasks.length}; ${done.length} completed this session`}
91
+ >
92
+ <section className="nw-card">
93
+ <header className="nw-header">
94
+ <span className="nw-icon" aria-hidden="true">
95
+ <CheckIcon />
96
+ </span>
97
+ <div className="nw-title-block">
98
+ <h1 className="nw-title">Acme Tasks</h1>
99
+ <p className="nw-subtitle" aria-live="polite">
100
+ {status}
101
+ </p>
102
+ </div>
103
+ <span className="nw-chip">
104
+ {displayMode === 'fullscreen' ? 'Fullscreen' : `${openCount} open`}
105
+ </span>
106
+ </header>
107
+
108
+ {/* Flow 1 — Capture */}
109
+ <div className="nw-capture">
110
+ <input
111
+ className="nw-input"
112
+ placeholder="Add a task…"
113
+ value={draft}
114
+ onChange={(event) => setDraft(event.currentTarget.value)}
115
+ onKeyDown={(event) => {
116
+ if (event.key === 'Enter') capture();
117
+ }}
118
+ />
119
+ <button
120
+ className="nw-button nw-button-primary"
121
+ type="button"
122
+ disabled={addTask.isPending}
123
+ onClick={capture}
124
+ >
125
+ Add
126
+ </button>
127
+ </div>
128
+
129
+ {/* Flow 2 — Prioritize, Flow 3 — Complete */}
130
+ <ul className="nw-list">
131
+ {tasks.map((task) => {
132
+ const isDone = done.includes(task.id);
133
+ const level = priority[task.id] ?? task.priority;
134
+ return (
135
+ <li className={`nw-task${isDone ? ' nw-task-done' : ''}`} key={task.id}>
136
+ <button
137
+ aria-label={isDone ? 'Completed' : 'Complete task'}
138
+ className={`nw-check${isDone ? ' nw-check-on' : ''}`}
139
+ type="button"
140
+ disabled={isDone}
141
+ onClick={() => complete(task)}
142
+ >
143
+ {isDone ? '✓' : ''}
144
+ </button>
145
+ <span className="nw-task-title">{task.title}</span>
146
+ <select
147
+ aria-label="Priority"
148
+ className={`nw-priority nw-priority-${level}`}
149
+ value={level}
150
+ disabled={isDone}
151
+ onChange={(event) => reprioritize(task, event.currentTarget.value as Priority)}
152
+ >
153
+ {PRIORITIES.map((entry) => (
154
+ <option key={entry} value={entry}>
155
+ {entry}
156
+ </option>
157
+ ))}
158
+ </select>
159
+ </li>
160
+ );
161
+ })}
162
+ </ul>
163
+ <p className="nw-note">
164
+ Two-way in chat: read your list, capture, re-prioritize, and complete.
165
+ </p>
166
+ </section>
167
+ </main>
168
+ );
169
+ }
170
+
171
+ function CheckIcon() {
172
+ return (
173
+ <svg viewBox="0 0 24 24" aria-hidden="true">
174
+ <path d="M4 12.5 9 17l11-11" />
175
+ </svg>
176
+ );
177
+ }
@@ -0,0 +1,227 @@
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: #f7f5ff;
7
+ --nw-text: #1c1830;
8
+ --nw-muted: #635d80;
9
+ --nw-border: #e2ddf5;
10
+ --nw-accent: #7c3aed;
11
+ --nw-accent-strong: #6d28d9;
12
+ --nw-accent-soft: #efe9ff;
13
+ --nw-high: #dc2626;
14
+ --nw-medium: #b45309;
15
+ --nw-low: #2563eb;
16
+ --nw-radius: 10px;
17
+ --nw-shadow: 0 18px 50px rgb(30 20 60 / 12%);
18
+ }
19
+
20
+ .dark,
21
+ [data-theme="dark"] {
22
+ --nw-bg: #161228;
23
+ --nw-surface: #1d1735;
24
+ --nw-text: #f2eeff;
25
+ --nw-muted: #a99fce;
26
+ --nw-border: #342a54;
27
+ --nw-accent: #a78bfa;
28
+ --nw-accent-strong: #8b5cf6;
29
+ --nw-accent-soft: #2a2148;
30
+ --nw-high: #f87171;
31
+ --nw-medium: #fbbf24;
32
+ --nw-low: #60a5fa;
33
+ --nw-shadow: 0 18px 50px rgb(0 0 0 / 32%);
34
+ }
35
+
36
+ * {
37
+ box-sizing: border-box;
38
+ }
39
+
40
+ body {
41
+ margin: 0;
42
+ background: var(--nw-bg);
43
+ color: var(--nw-text);
44
+ }
45
+
46
+ button,
47
+ input,
48
+ select {
49
+ font: inherit;
50
+ }
51
+
52
+ .nw-shell {
53
+ min-height: 100vh;
54
+ padding: 14px;
55
+ background: var(--nw-bg);
56
+ color: var(--nw-text);
57
+ }
58
+
59
+ .nw-card {
60
+ max-width: 620px;
61
+ margin: 0 auto;
62
+ background: var(--nw-surface);
63
+ border: 1px solid var(--nw-border);
64
+ border-radius: var(--nw-radius);
65
+ box-shadow: var(--nw-shadow);
66
+ overflow: hidden;
67
+ }
68
+
69
+ .nw-header {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 12px;
73
+ padding: 16px;
74
+ border-bottom: 1px solid var(--nw-border);
75
+ }
76
+
77
+ .nw-icon svg {
78
+ width: 24px;
79
+ height: 24px;
80
+ fill: none;
81
+ stroke: var(--nw-accent);
82
+ stroke-width: 2;
83
+ stroke-linecap: round;
84
+ stroke-linejoin: round;
85
+ }
86
+
87
+ .nw-title-block {
88
+ flex: 1;
89
+ min-width: 0;
90
+ }
91
+
92
+ .nw-title {
93
+ margin: 0;
94
+ font-size: 17px;
95
+ font-weight: 700;
96
+ }
97
+
98
+ .nw-subtitle {
99
+ margin: 2px 0 0;
100
+ font-size: 13px;
101
+ color: var(--nw-muted);
102
+ }
103
+
104
+ .nw-chip {
105
+ padding: 4px 10px;
106
+ border-radius: 999px;
107
+ background: var(--nw-accent-soft);
108
+ color: var(--nw-accent-strong);
109
+ font-size: 12px;
110
+ font-weight: 600;
111
+ }
112
+
113
+ .nw-capture {
114
+ display: flex;
115
+ gap: 8px;
116
+ padding: 14px 16px 4px;
117
+ }
118
+
119
+ .nw-input {
120
+ flex: 1;
121
+ padding: 9px 12px;
122
+ border: 1px solid var(--nw-border);
123
+ border-radius: 10px;
124
+ background: var(--nw-bg);
125
+ color: var(--nw-text);
126
+ }
127
+
128
+ .nw-button {
129
+ display: inline-flex;
130
+ align-items: center;
131
+ gap: 6px;
132
+ padding: 9px 14px;
133
+ border: 1px solid var(--nw-border);
134
+ border-radius: 10px;
135
+ background: var(--nw-bg);
136
+ color: var(--nw-text);
137
+ cursor: pointer;
138
+ }
139
+
140
+ .nw-button-primary {
141
+ background: var(--nw-accent);
142
+ border-color: var(--nw-accent);
143
+ color: #ffffff;
144
+ font-weight: 600;
145
+ }
146
+
147
+ .nw-button-primary:disabled {
148
+ opacity: 0.6;
149
+ cursor: default;
150
+ }
151
+
152
+ .nw-list {
153
+ list-style: none;
154
+ margin: 0;
155
+ padding: 8px 16px 4px;
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 8px;
159
+ }
160
+
161
+ .nw-task {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 10px;
165
+ padding: 10px 12px;
166
+ border: 1px solid var(--nw-border);
167
+ border-radius: 12px;
168
+ background: var(--nw-bg);
169
+ }
170
+
171
+ .nw-task-title {
172
+ flex: 1;
173
+ min-width: 0;
174
+ }
175
+
176
+ .nw-task-done {
177
+ opacity: 0.55;
178
+ }
179
+
180
+ .nw-task-done .nw-task-title {
181
+ text-decoration: line-through;
182
+ }
183
+
184
+ .nw-check {
185
+ width: 22px;
186
+ height: 22px;
187
+ border: 1.5px solid var(--nw-border);
188
+ border-radius: 999px;
189
+ background: transparent;
190
+ color: #ffffff;
191
+ cursor: pointer;
192
+ flex: none;
193
+ }
194
+
195
+ .nw-check-on {
196
+ background: var(--nw-accent);
197
+ border-color: var(--nw-accent);
198
+ }
199
+
200
+ .nw-priority {
201
+ padding: 5px 8px;
202
+ border: 1px solid var(--nw-border);
203
+ border-radius: 8px;
204
+ background: var(--nw-bg);
205
+ color: var(--nw-text);
206
+ font-size: 12px;
207
+ font-weight: 600;
208
+ }
209
+
210
+ .nw-priority-high {
211
+ color: var(--nw-high);
212
+ }
213
+
214
+ .nw-priority-medium {
215
+ color: var(--nw-medium);
216
+ }
217
+
218
+ .nw-priority-low {
219
+ color: var(--nw-low);
220
+ }
221
+
222
+ .nw-note {
223
+ margin: 0;
224
+ padding: 8px 16px 16px;
225
+ font-size: 12px;
226
+ color: var(--nw-muted);
227
+ }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import app from '../src/server.js';
3
+
4
+ describe('acme-tasks example', () => {
5
+ it('exports a Noodle server definition', () => {
6
+ expect(typeof app.toManifest).toBe('function');
7
+ });
8
+
9
+ it('exposes a tool for each of the top-3 prioritized flows', async () => {
10
+ // Capture → add_task, Prioritize → list_today (+ set_priority helper), Complete → complete_task.
11
+ const text = JSON.stringify(await app.toManifest());
12
+ expect(text).toContain('add_task');
13
+ expect(text).toContain('list_today');
14
+ expect(text).toContain('complete_task');
15
+ expect(text).toContain('set_priority');
16
+ });
17
+
18
+ it('seeds today’s list highest-priority first', async () => {
19
+ const text = JSON.stringify(await app.toManifest());
20
+ expect(text).toMatch(/"tasks":\[\{"id":"email_vendor".*"priority":"high"/);
21
+ });
22
+ });
@@ -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,82 @@
1
+ # Food Ordering
2
+
3
+ **Owns:** The flagship consumer ordering MCP App example: React view authoring, app-only helper tools,
4
+ caller-scoped cart state handles, packaged image assets, checkout handoff policy, host actions,
5
+ CSP/permissions metadata, and widget preview coverage.
6
+
7
+ Food Ordering is a generic, synthetic version of a live marketplace ordering app. It lets a user search
8
+ stores, browse menus, customize an item, build a multi-line cart, review the order, and hand off checkout to
9
+ an allowlisted example domain. It does not use real restaurant APIs, real checkout, customer credentials, or
10
+ private customer data.
11
+
12
+ ## What It Shows
13
+
14
+ | Capability | Example |
15
+ | :--- | :--- |
16
+ | Public entry tool | `open_ordering` returns structured fallback content and renders the React widget |
17
+ | App-only helper tools | `search_stores`, `load_menu`, `load_item`, `read_cart`, `sync_cart`, `prepare_checkout` |
18
+ | Durable cart state | `server(..., { state: { handles: { cart } }, use: { state } })` with caller scope and revision checks |
19
+ | React app runtime kit | `@noodleseed/one/react` supplies app flow, shell/nav/view, async state, form, quantity, choice, and handoff primitives |
20
+ | Multi-step widget flow | One React shell navigates stores, menu, item customization, cart, review, and handoff views through `useAppFlow` |
21
+ | Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
22
+ | Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
23
+
24
+ ## Local Author Loop
25
+
26
+ ```sh
27
+ noodle validate
28
+ noodle test
29
+ noodle dev
30
+ ```
31
+
32
+ In another terminal:
33
+
34
+ ```sh
35
+ noodle tools list
36
+ noodle tools call open_ordering --args '{"customer":"Asha","query":"noodles"}'
37
+ noodle tools call summarize_ordering_options --args '{}'
38
+ ```
39
+
40
+ For Apps metadata conformance, start `noodle dev`, copy the loopback MCP endpoint, then run:
41
+
42
+ ```sh
43
+ npx @mcpjam/cli@latest apps conformance --url http://127.0.0.1:<port>/o/demo/food-ordering/mcp --quiet --format json
44
+ ```
45
+
46
+ ## Client Setup
47
+
48
+ Use the CLI to print the exact setup flow for your MCP client:
49
+
50
+ ```sh
51
+ noodle connect claude
52
+ noodle connect chatgpt
53
+ noodle connect inspector
54
+ ```
55
+
56
+ ## Deploy
57
+
58
+ ```sh
59
+ noodle link --org demo --app food-ordering
60
+ noodle deploy --access owner-only
61
+ noodle open
62
+ ```
63
+
64
+ Use `--access org-members` for an org-wide internal demo. This example has no connector secrets and does not
65
+ include tokens, caller-key mechanisms, or `.env.noodle` values.
66
+
67
+ ## Demo Assets
68
+
69
+ The packaged demo images live under `assets/` and are public web assets when deployed. The current app uses
70
+ `assets/noodle-bowl.jpg` as the server branding image.
71
+
72
+ Image sources:
73
+
74
+ - `assets/noodle-bowl.jpg` — Unsplash photo
75
+ [`IRv8V9Hb8gI`](https://unsplash.com/photos/IRv8V9Hb8gI), downloaded from Unsplash.
76
+ - `assets/lentil-soup.jpg` — Unsplash image
77
+ [`photo-1510431198580-7727c9fa1e3a`](https://images.unsplash.com/photo-1510431198580-7727c9fa1e3a), downloaded from Unsplash.
78
+ - `assets/mint-lemonade.jpg` — Unsplash photo
79
+ [`X7Nx327NtuA`](https://unsplash.com/photos/X7Nx327NtuA) by Imad 786.
80
+
81
+ Unsplash photos are free to use under the [Unsplash License](https://unsplash.com/license); attribution is
82
+ not required, but source notes are kept here for provenance.
@@ -0,0 +1,5 @@
1
+ {
2
+ "entrypoint": "src/server.ts",
3
+ "name": "food-ordering",
4
+ "template": "widget"
5
+ }