@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.
- package/README.md +3 -3
- package/manifest.json +575 -5
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +7 -2
- package/skills/claude-code/examples/acme-bistro/README.md +51 -0
- package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +435 -0
- package/skills/claude-code/examples/acme-bistro/design/api-contract.md +161 -0
- package/skills/claude-code/examples/acme-bistro/design/wireframe.html +573 -0
- package/skills/claude-code/examples/acme-bistro/noodle.json +5 -0
- package/skills/claude-code/examples/acme-bistro/package.json +20 -0
- package/skills/claude-code/examples/acme-bistro/src/helpers.ts +7 -0
- package/skills/claude-code/examples/acme-bistro/src/server.ts +149 -0
- package/skills/claude-code/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
- package/skills/claude-code/examples/acme-bistro/src/views/widget-style.css +228 -0
- package/skills/claude-code/examples/acme-bistro/test/server.test.ts +21 -0
- package/skills/claude-code/examples/acme-bistro/vitest.config.ts +7 -0
- package/skills/claude-code/examples/acme-discovery/README.md +85 -0
- package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +441 -0
- package/skills/claude-code/examples/acme-discovery/design/wireframe.html +670 -0
- package/skills/claude-code/examples/acme-discovery/noodle.json +5 -0
- package/skills/claude-code/examples/acme-discovery/package.json +20 -0
- package/skills/claude-code/examples/acme-discovery/src/helpers.ts +7 -0
- package/skills/claude-code/examples/acme-discovery/src/server.ts +202 -0
- package/skills/claude-code/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
- package/skills/claude-code/examples/acme-discovery/src/views/widget-style.css +207 -0
- package/skills/claude-code/examples/acme-discovery/test/server.test.ts +24 -0
- package/skills/claude-code/examples/acme-discovery/vitest.config.ts +7 -0
- package/skills/claude-code/examples/acme-tasks/README.md +80 -0
- package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +277 -0
- package/skills/claude-code/examples/acme-tasks/design/wireframe.html +603 -0
- package/skills/claude-code/examples/acme-tasks/noodle.json +5 -0
- package/skills/claude-code/examples/acme-tasks/package.json +20 -0
- package/skills/claude-code/examples/acme-tasks/src/helpers.ts +6 -0
- package/skills/claude-code/examples/acme-tasks/src/server.ts +141 -0
- package/skills/claude-code/examples/acme-tasks/src/views/task-list.tsx +177 -0
- package/skills/claude-code/examples/acme-tasks/src/views/widget-style.css +227 -0
- package/skills/claude-code/examples/acme-tasks/test/server.test.ts +22 -0
- package/skills/claude-code/examples/acme-tasks/vitest.config.ts +7 -0
- package/skills/claude-code/examples/food-ordering/README.md +82 -0
- package/skills/claude-code/examples/food-ordering/noodle.json +5 -0
- package/skills/claude-code/examples/food-ordering/package.json +22 -0
- package/skills/claude-code/examples/food-ordering/src/helpers.ts +34 -0
- package/skills/claude-code/examples/food-ordering/src/server.ts +412 -0
- package/skills/claude-code/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
- package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
- package/skills/claude-code/examples/food-ordering/src/views/widget-style.css +682 -0
- package/skills/claude-code/examples/food-ordering/test/server.test.ts +58 -0
- package/skills/claude-code/examples/food-ordering/vitest.config.ts +30 -0
- package/skills/claude-code/examples/hello/README.md +13 -0
- package/skills/claude-code/examples/hello/noodle.json +5 -0
- package/skills/claude-code/examples/hello/package.json +16 -0
- package/skills/claude-code/examples/hello/src/server.ts +29 -0
- package/skills/claude-code/examples/hello/test/server.test.ts +8 -0
- package/skills/claude-code/examples/weather/README.md +54 -0
- package/skills/claude-code/examples/weather/noodle.json +4 -0
- package/skills/claude-code/examples/weather/package.json +16 -0
- package/skills/claude-code/examples/weather/src/server.ts +190 -0
- package/skills/claude-code/examples/weather/test/server.test.ts +8 -0
- package/skills/claude-code/references/chatgpt-compliance.md +63 -0
- package/skills/claude-code/references/connect-an-api.md +76 -0
- package/skills/claude-code/references/examples.md +16 -4
- package/skills/claude-code/references/experience-design.md +142 -0
- package/skills/codex/SKILL.md +7 -2
- package/skills/codex/examples/acme-bistro/README.md +51 -0
- package/skills/codex/examples/acme-bistro/design/UX-Document.md +435 -0
- package/skills/codex/examples/acme-bistro/design/api-contract.md +161 -0
- package/skills/codex/examples/acme-bistro/design/wireframe.html +573 -0
- package/skills/codex/examples/acme-bistro/noodle.json +5 -0
- package/skills/codex/examples/acme-bistro/package.json +20 -0
- package/skills/codex/examples/acme-bistro/src/helpers.ts +7 -0
- package/skills/codex/examples/acme-bistro/src/server.ts +149 -0
- package/skills/codex/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
- package/skills/codex/examples/acme-bistro/src/views/widget-style.css +228 -0
- package/skills/codex/examples/acme-bistro/test/server.test.ts +21 -0
- package/skills/codex/examples/acme-bistro/vitest.config.ts +7 -0
- package/skills/codex/examples/acme-discovery/README.md +85 -0
- package/skills/codex/examples/acme-discovery/design/UX-Document.md +441 -0
- package/skills/codex/examples/acme-discovery/design/wireframe.html +670 -0
- package/skills/codex/examples/acme-discovery/noodle.json +5 -0
- package/skills/codex/examples/acme-discovery/package.json +20 -0
- package/skills/codex/examples/acme-discovery/src/helpers.ts +7 -0
- package/skills/codex/examples/acme-discovery/src/server.ts +202 -0
- package/skills/codex/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
- package/skills/codex/examples/acme-discovery/src/views/widget-style.css +207 -0
- package/skills/codex/examples/acme-discovery/test/server.test.ts +24 -0
- package/skills/codex/examples/acme-discovery/vitest.config.ts +7 -0
- package/skills/codex/examples/acme-tasks/README.md +80 -0
- package/skills/codex/examples/acme-tasks/design/UX-Document.md +277 -0
- package/skills/codex/examples/acme-tasks/design/wireframe.html +603 -0
- package/skills/codex/examples/acme-tasks/noodle.json +5 -0
- package/skills/codex/examples/acme-tasks/package.json +20 -0
- package/skills/codex/examples/acme-tasks/src/helpers.ts +6 -0
- package/skills/codex/examples/acme-tasks/src/server.ts +141 -0
- package/skills/codex/examples/acme-tasks/src/views/task-list.tsx +177 -0
- package/skills/codex/examples/acme-tasks/src/views/widget-style.css +227 -0
- package/skills/codex/examples/acme-tasks/test/server.test.ts +22 -0
- package/skills/codex/examples/acme-tasks/vitest.config.ts +7 -0
- package/skills/codex/examples/food-ordering/README.md +82 -0
- package/skills/codex/examples/food-ordering/noodle.json +5 -0
- package/skills/codex/examples/food-ordering/package.json +22 -0
- package/skills/codex/examples/food-ordering/src/helpers.ts +34 -0
- package/skills/codex/examples/food-ordering/src/server.ts +412 -0
- package/skills/codex/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
- package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
- package/skills/codex/examples/food-ordering/src/views/widget-style.css +682 -0
- package/skills/codex/examples/food-ordering/test/server.test.ts +58 -0
- package/skills/codex/examples/food-ordering/vitest.config.ts +30 -0
- package/skills/codex/examples/hello/README.md +13 -0
- package/skills/codex/examples/hello/noodle.json +5 -0
- package/skills/codex/examples/hello/package.json +16 -0
- package/skills/codex/examples/hello/src/server.ts +29 -0
- package/skills/codex/examples/hello/test/server.test.ts +8 -0
- package/skills/codex/examples/weather/README.md +54 -0
- package/skills/codex/examples/weather/noodle.json +4 -0
- package/skills/codex/examples/weather/package.json +16 -0
- package/skills/codex/examples/weather/src/server.ts +190 -0
- package/skills/codex/examples/weather/test/server.test.ts +8 -0
- package/skills/codex/references/chatgpt-compliance.md +63 -0
- package/skills/codex/references/connect-an-api.md +76 -0
- package/skills/codex/references/examples.md +16 -4
- package/skills/codex/references/experience-design.md +142 -0
|
@@ -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,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.
|