@noodleseed/agent-kit 0.94.0 → 0.95.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 (98) hide show
  1. package/manifest.json +565 -357
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/authoring-mcp-servers/SKILL.md +1 -1
  5. package/skills/claude-code/authoring-mcp-servers/references/authoring-workflow.md +5 -11
  6. package/skills/claude-code/authoring-mcp-servers/references/build-an-mcp-server.md +2 -2
  7. package/skills/claude-code/building-mcp-apps/SKILL.md +1 -1
  8. package/skills/claude-code/building-mcp-apps/references/build-an-mcp-app.md +2 -2
  9. package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
  10. package/skills/claude-code/connecting-apis-to-mcp/references/authoring-workflow.md +5 -11
  11. package/skills/claude-code/connecting-apis-to-mcp/references/connect-an-api.md +2 -2
  12. package/skills/claude-code/creating-product-agent-guides/SKILL.md +1 -1
  13. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
  14. package/skills/claude-code/debugging-mcp-delivery/references/verify-and-recover.md +2 -1
  15. package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
  16. package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
  17. package/skills/claude-code/designing-mcp-products/references/authoring-workflow.md +5 -11
  18. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
  19. package/skills/claude-code/embedding-mcp-assistants/references/authoring-workflow.md +5 -11
  20. package/skills/claude-code/embedding-mcp-assistants/references/embedded-assistant.md +33 -27
  21. package/skills/claude-code/examples/acme-tasks/README.md +14 -4
  22. package/skills/claude-code/examples/customer-auth/README.md +3 -0
  23. package/skills/claude-code/examples/hello/README.md +4 -2
  24. package/skills/claude-code/examples/stateful-draft/README.md +115 -0
  25. package/skills/claude-code/examples/stateful-draft/noodle.json +5 -0
  26. package/skills/claude-code/examples/stateful-draft/package.json +22 -0
  27. package/skills/claude-code/examples/stateful-draft/site/client.js +44 -0
  28. package/skills/claude-code/examples/stateful-draft/site/demo.mjs +158 -0
  29. package/skills/claude-code/examples/stateful-draft/site/index.html +41 -0
  30. package/skills/claude-code/examples/stateful-draft/src/helpers.ts +6 -0
  31. package/skills/claude-code/examples/stateful-draft/src/server.ts +159 -0
  32. package/skills/claude-code/examples/stateful-draft/src/views/draft-card.tsx +152 -0
  33. package/skills/claude-code/examples/stateful-draft/src/views/widget-style.css +93 -0
  34. package/skills/claude-code/examples/stateful-draft/test/draft-card.test.tsx +88 -0
  35. package/skills/claude-code/examples/stateful-draft/test/server.test.ts +58 -0
  36. package/skills/claude-code/examples/stateful-draft/vitest.config.ts +6 -0
  37. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  38. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
  39. package/skills/claude-code/references/authoring-workflow.md +5 -11
  40. package/skills/claude-code/references/build-an-mcp-app.md +2 -2
  41. package/skills/claude-code/references/build-an-mcp-server.md +2 -2
  42. package/skills/claude-code/references/connect-an-api.md +2 -2
  43. package/skills/claude-code/references/embedded-assistant.md +33 -27
  44. package/skills/claude-code/references/examples.md +1 -1
  45. package/skills/claude-code/references/verify-and-recover.md +2 -1
  46. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
  47. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
  48. package/skills/claude-code/verifying-mcp-delivery/references/verify-and-recover.md +2 -1
  49. package/skills/claude-code/wrapping-existing-applications/SKILL.md +1 -1
  50. package/skills/claude-code/wrapping-existing-applications/references/authoring-workflow.md +5 -11
  51. package/skills/codex/SKILL.md +1 -1
  52. package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
  53. package/skills/codex/authoring-mcp-servers/references/authoring-workflow.md +5 -11
  54. package/skills/codex/authoring-mcp-servers/references/build-an-mcp-server.md +2 -2
  55. package/skills/codex/building-mcp-apps/SKILL.md +1 -1
  56. package/skills/codex/building-mcp-apps/references/build-an-mcp-app.md +2 -2
  57. package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
  58. package/skills/codex/connecting-apis-to-mcp/references/authoring-workflow.md +5 -11
  59. package/skills/codex/connecting-apis-to-mcp/references/connect-an-api.md +2 -2
  60. package/skills/codex/creating-product-agent-guides/SKILL.md +1 -1
  61. package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
  62. package/skills/codex/debugging-mcp-delivery/references/verify-and-recover.md +2 -1
  63. package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
  64. package/skills/codex/designing-mcp-products/SKILL.md +1 -1
  65. package/skills/codex/designing-mcp-products/references/authoring-workflow.md +5 -11
  66. package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
  67. package/skills/codex/embedding-mcp-assistants/references/authoring-workflow.md +5 -11
  68. package/skills/codex/embedding-mcp-assistants/references/embedded-assistant.md +33 -27
  69. package/skills/codex/examples/acme-tasks/README.md +14 -4
  70. package/skills/codex/examples/customer-auth/README.md +3 -0
  71. package/skills/codex/examples/hello/README.md +4 -2
  72. package/skills/codex/examples/stateful-draft/README.md +115 -0
  73. package/skills/codex/examples/stateful-draft/noodle.json +5 -0
  74. package/skills/codex/examples/stateful-draft/package.json +22 -0
  75. package/skills/codex/examples/stateful-draft/site/client.js +44 -0
  76. package/skills/codex/examples/stateful-draft/site/demo.mjs +158 -0
  77. package/skills/codex/examples/stateful-draft/site/index.html +41 -0
  78. package/skills/codex/examples/stateful-draft/src/helpers.ts +6 -0
  79. package/skills/codex/examples/stateful-draft/src/server.ts +159 -0
  80. package/skills/codex/examples/stateful-draft/src/views/draft-card.tsx +152 -0
  81. package/skills/codex/examples/stateful-draft/src/views/widget-style.css +93 -0
  82. package/skills/codex/examples/stateful-draft/test/draft-card.test.tsx +88 -0
  83. package/skills/codex/examples/stateful-draft/test/server.test.ts +58 -0
  84. package/skills/codex/examples/stateful-draft/vitest.config.ts +6 -0
  85. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  86. package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
  87. package/skills/codex/references/authoring-workflow.md +5 -11
  88. package/skills/codex/references/build-an-mcp-app.md +2 -2
  89. package/skills/codex/references/build-an-mcp-server.md +2 -2
  90. package/skills/codex/references/connect-an-api.md +2 -2
  91. package/skills/codex/references/embedded-assistant.md +33 -27
  92. package/skills/codex/references/examples.md +1 -1
  93. package/skills/codex/references/verify-and-recover.md +2 -1
  94. package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
  95. package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
  96. package/skills/codex/verifying-mcp-delivery/references/verify-and-recover.md +2 -1
  97. package/skills/codex/wrapping-existing-applications/SKILL.md +1 -1
  98. package/skills/codex/wrapping-existing-applications/references/authoring-workflow.md +5 -11
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "stateful-draft",
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
+ "site": "node site/demo.mjs"
12
+ },
13
+ "devDependencies": {
14
+ "@noodleseed/assistant": "^1.33.0",
15
+ "@vitejs/plugin-react": "latest",
16
+ "@noodleseed/one": "^0.154.0",
17
+ "react": "latest",
18
+ "react-dom": "latest",
19
+ "vite": "latest",
20
+ "vitest": "latest"
21
+ }
22
+ }
@@ -0,0 +1,44 @@
1
+ import '/sdk/index.js';
2
+
3
+ const config = await fetch('/demo-config').then((response) => response.json());
4
+ const mount = document.querySelector('#assistant');
5
+ const status = document.querySelector('#status');
6
+ if (config.authenticated && !config.signedIn) {
7
+ document.querySelector('#demo-login').hidden = false;
8
+ status.textContent =
9
+ 'Your draft is ready to come with you. This next step simulates the product’s signup.';
10
+ } else {
11
+ const assistant = document.createElement('noodle-assistant');
12
+ assistant.setAttribute('theme', 'light');
13
+ if (config.authenticated) {
14
+ assistant.setAttribute('session-endpoint', '/assistant-session');
15
+ status.textContent =
16
+ 'You are using a synthetic demo account. Open the chat to continue with your saved brief.';
17
+ } else {
18
+ assistant.setAttribute('embed-id', config.embedId);
19
+ assistant.setAttribute('service-url', config.serviceUrl);
20
+ status.textContent =
21
+ 'Start without an account. Open the chat and describe what you want to achieve.';
22
+ }
23
+ mount.append(assistant);
24
+ }
25
+
26
+ document.addEventListener('assistant-sign-in-requested', async (event) => {
27
+ status.textContent = 'Preparing your account handoff…';
28
+ try {
29
+ const response = await fetch('/start', {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ signInTicket: event.detail.signInTicket }),
33
+ });
34
+ const result = await response.json();
35
+ if (!response.ok) {
36
+ status.textContent = result.error ?? 'Handoff not started';
37
+ return;
38
+ }
39
+ window.location.assign(result.destination);
40
+ } catch {
41
+ status.textContent =
42
+ 'The handoff did not start. Your brief has not been moved. Please try again.';
43
+ }
44
+ });
@@ -0,0 +1,158 @@
1
+ /** Loopback-only synthetic host. Replace its demo identity with your existing login in a real app. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createAssistantSession } from '@noodleseed/assistant/server';
8
+
9
+ const publicOrigin = 'http://localhost:3001';
10
+ const appOrigin = 'http://localhost:3002';
11
+ const serviceUrl = process.env.NOODLE_SERVICE_URL ?? 'https://cloud.noodleseed.dev';
12
+ const embedId = process.env.NOODLE_EMBED_ID;
13
+ const credentialFile = process.env.NOODLE_ASSISTANT_CREDENTIALS_FILE;
14
+ if (!embedId?.match(/^pub_[a-z0-9]+$/i)) {
15
+ throw new Error('Set NOODLE_EMBED_ID from the hosted deployment.');
16
+ }
17
+ if (new URL(serviceUrl).protocol !== 'https:')
18
+ throw new Error('The hosted service must use HTTPS.');
19
+ // Consume only the explicitly supplied backend credential file. Its content is never sent to the browser.
20
+ const credentials = credentialFile ? JSON.parse(await readFile(credentialFile, 'utf8')) : undefined;
21
+ if (
22
+ credentials &&
23
+ (typeof credentials.clientId !== 'string' || typeof credentials.clientSecret !== 'string')
24
+ ) {
25
+ throw new Error('The backend credential file is not a CLI-created assistant client file.');
26
+ }
27
+ const sdkDir = dirname(fileURLToPath(import.meta.resolve('@noodleseed/assistant')));
28
+ const siteDir = dirname(fileURLToPath(import.meta.url));
29
+ const transactions = new Map();
30
+ const lifetime = 10 * 60 * 1000;
31
+
32
+ function send(res, status, value, type = 'application/json') {
33
+ res.writeHead(status, {
34
+ 'Content-Type': type,
35
+ 'Cache-Control': 'no-store',
36
+ 'Referrer-Policy': 'no-referrer',
37
+ 'X-Content-Type-Options': 'nosniff',
38
+ });
39
+ res.end(type === 'application/json' ? JSON.stringify(value) : value);
40
+ }
41
+ function redirect(res, destination) {
42
+ res.writeHead(303, { Location: destination, 'Cache-Control': 'no-store' });
43
+ res.end();
44
+ }
45
+ function current(req) {
46
+ const id = /(?:^|;\s*)brief_demo=([a-f0-9-]+)/.exec(req.headers.cookie ?? '')?.[1];
47
+ return id ? transactions.get(id) : undefined;
48
+ }
49
+ async function jsonBody(req) {
50
+ let text = '';
51
+ for await (const chunk of req) {
52
+ text += chunk.toString();
53
+ if (Buffer.byteLength(text) > 8192) throw new Error('Request too large');
54
+ }
55
+ return JSON.parse(text);
56
+ }
57
+
58
+ function handler(origin) {
59
+ return async (req, res) => {
60
+ // The fixed loopback host is intentional: never deploy this synthetic-login server to the internet.
61
+ if (req.headers.host !== new URL(origin).host) return send(res, 403, { error: 'Wrong host' });
62
+ for (const [id, transaction] of transactions) {
63
+ if (transaction.expiresAt <= Date.now()) transactions.delete(id);
64
+ }
65
+ const path = new URL(req.url ?? '/', origin).pathname;
66
+ try {
67
+ if (req.method === 'GET' && path === '/') {
68
+ return send(
69
+ res,
70
+ 200,
71
+ await readFile(join(siteDir, 'index.html')),
72
+ 'text/html; charset=utf-8',
73
+ );
74
+ }
75
+ if (req.method === 'GET' && path === '/demo-config') {
76
+ return send(res, 200, {
77
+ serviceUrl,
78
+ embedId,
79
+ authenticated: origin === appOrigin,
80
+ signedIn: Boolean(current(req)?.user),
81
+ });
82
+ }
83
+ if (req.method === 'GET' && path === '/client.js') {
84
+ return send(res, 200, await readFile(join(siteDir, 'client.js')), 'text/javascript');
85
+ }
86
+ if (req.method === 'GET' && /^\/sdk\/[a-z0-9._-]+\.js$/i.test(path)) {
87
+ return send(res, 200, await readFile(join(sdkDir, path.slice(5))), 'text/javascript');
88
+ }
89
+ if (req.method === 'POST') {
90
+ if (req.headers.origin !== origin) return send(res, 403, { error: 'Wrong origin' });
91
+ if (origin === publicOrigin && path === '/start') {
92
+ if (!credentials)
93
+ return send(res, 503, {
94
+ error:
95
+ 'Signup demonstration needs an authorized backend client. The anonymous brief is still available.',
96
+ });
97
+ if (transactions.size >= 40) return send(res, 429, { error: 'Please try again later' });
98
+ const body = await jsonBody(req);
99
+ if (typeof body.signInTicket !== 'string' || body.signInTicket.length > 4096) {
100
+ return send(res, 400, { error: 'Missing continuation ticket' });
101
+ }
102
+ const id = randomUUID();
103
+ transactions.set(id, {
104
+ signInTicket: body.signInTicket,
105
+ expiresAt: Date.now() + lifetime,
106
+ });
107
+ res.setHeader(
108
+ 'Set-Cookie',
109
+ `brief_demo=${id}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`,
110
+ );
111
+ return send(res, 200, { destination: appOrigin });
112
+ }
113
+ if (origin === appOrigin && path === '/demo-login') {
114
+ const transaction = current(req);
115
+ if (!transaction)
116
+ return send(res, 410, { error: 'Demo expired. Start again on the public page.' });
117
+ // Synthetic identity only. A production route must verify its existing application session.
118
+ transaction.user ??= { id: `demo_${randomUUID()}`, name: 'Demo visitor' };
119
+ return redirect(res, '/');
120
+ }
121
+ }
122
+ if (origin === appOrigin && req.method === 'GET' && path === '/assistant-session') {
123
+ if (!credentials) return send(res, 503, { error: 'Configure the backend client first' });
124
+ if (req.headers['sec-fetch-site'] !== 'same-origin')
125
+ return send(res, 403, { error: 'Same-origin request required' });
126
+ const transaction = current(req);
127
+ if (!transaction?.user)
128
+ return send(res, 401, { error: 'Choose the synthetic demo account first' });
129
+ // Reuse the in-flight result so concurrent mounts cannot spend the single-use ticket twice.
130
+ transaction.session ??= createAssistantSession({
131
+ serviceUrl,
132
+ clientId: credentials.clientId,
133
+ clientSecret: credentials.clientSecret,
134
+ origin: appOrigin,
135
+ user: transaction.user,
136
+ signInTicket: transaction.signInTicket,
137
+ });
138
+ const session = await transaction.session;
139
+ delete transaction.signInTicket;
140
+ return send(res, 200, session);
141
+ }
142
+ return send(res, 404, { error: 'Not found' });
143
+ } catch {
144
+ // Do not log credentials, tickets, requests, or raw provider errors.
145
+ return send(res, 409, {
146
+ error: 'Continuation was not completed. Return to the public page and start again.',
147
+ });
148
+ }
149
+ };
150
+ }
151
+ for (const [port, origin] of [
152
+ [3001, publicOrigin],
153
+ [3002, appOrigin],
154
+ ]) {
155
+ createServer(handler(origin)).listen(port, '127.0.0.1');
156
+ }
157
+ console.log(`Synthetic onboarding demo: ${publicOrigin}`);
158
+ console.log('Loopback only. Uses the hosted assistant; demo signup is not a real account.');
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>First Brief · Conversational onboarding demo</title>
7
+ <style>
8
+ :root { font-family: system-ui, sans-serif; color: #172033; background: #f6f8fc; }
9
+ body { margin: 0; }
10
+ main { max-width: 880px; margin: 0 auto; padding: 64px 28px; }
11
+ .label { font-size: 12px; font-weight: 700; letter-spacing: .14em; color: #526077; }
12
+ h1 { max-width: 640px; font-size: clamp(36px, 6vw, 64px); line-height: 1.08; letter-spacing: -.055em; }
13
+ p { max-width: 570px; line-height: 1.6; color: #526077; }
14
+ .sample { padding: 24px; border: 1px solid #dce3ee; border-radius: 16px; background: white; margin: 28px 0; max-width: 550px; }
15
+ a { color: #174bbd; }
16
+ button { border: none; background: #2563eb; color: white; padding: 14px 20px; border-radius: 9px; font: inherit; cursor: pointer; }
17
+ :focus-visible { outline: 3px solid #2563eb; outline-offset: 4px; }
18
+ .notice { font-size: 13px; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <main>
23
+ <span class="label">FIRST BRIEF · INTERNAL DEMONSTRATION</span>
24
+ <h1>Make something useful.<br>Then decide to sign up.</h1>
25
+ <p>Describe a goal. Refine a short brief. See the result before deciding whether to take it into an account.</p>
26
+ <div class="sample">
27
+ <strong>Try this in the chat</strong>
28
+ <p>“I want to help new teammates complete their first useful project in a week.”</p>
29
+ <p id="status" role="status" aria-live="polite">Connecting…</p>
30
+ </div>
31
+ <form id="demo-login" action="/demo-login" method="post" hidden>
32
+ <p><strong>Simulated signup</strong><br>This creates a temporary demo identity. It does not register an account with any SaaS product.</p>
33
+ <button type="submit">Continue as a demo user</button>
34
+ <p><a href="http://localhost:3001">Cancel and return to the public page</a></p>
35
+ </form>
36
+ <p class="notice">Use synthetic information only. The assistant and draft storage are hosted by Noodle Seed. Signup is simulated locally; no customer backend is connected. Drafts expire within 24 hours. <a href="https://noodleseed.com/privacy" target="_blank" rel="noreferrer">Privacy</a></p>
37
+ </main>
38
+ <div id="assistant"></div>
39
+ <script type="module" src="/client.js"></script>
40
+ </body>
41
+ </html>
@@ -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, useToolInfo, useSendFollowUpMessage } = generateHelpers<AppType>();
@@ -0,0 +1,159 @@
1
+ import {
2
+ annotations,
3
+ authenticatedWebsite,
4
+ connector,
5
+ embeddedAssistant,
6
+ noodleManaged,
7
+ publicWebsite,
8
+ server,
9
+ tool,
10
+ z,
11
+ } from '@noodleseed/one';
12
+
13
+ const draft = z.object({
14
+ title: z.string().trim().min(1).max(120).optional(),
15
+ audience: z.string().trim().min(1).max(240).optional(),
16
+ goal: z.string().trim().min(1).max(240).optional(),
17
+ });
18
+
19
+ const snapshot = z.object({
20
+ value: draft,
21
+ revision: z.number().int(),
22
+ status: z.string(),
23
+ });
24
+
25
+ const state = connector('noodle_state')
26
+ .version('1.0.0')
27
+ .operation('read_state', {
28
+ type: 'read',
29
+ input: z.object({ handle: z.string() }),
30
+ output: snapshot,
31
+ })
32
+ .operation('patch_state', {
33
+ type: 'action',
34
+ input: z.object({
35
+ handle: z.string(),
36
+ expectedRevision: z.number().int().min(0),
37
+ value: draft,
38
+ }),
39
+ output: snapshot,
40
+ });
41
+
42
+ const openDraft = tool('open_draft', {
43
+ title: 'Review your brief',
44
+ description:
45
+ 'Preview a proposed brief and read the saved brief with its revision. Pass the proposed title, audience, and goal in proposal to prefill the editable preview without saving it. Omit proposal to reload the saved record after a conflict.',
46
+ input: z.object({ proposal: draft.default({}) }),
47
+ output: snapshot.extend({ proposal: draft }),
48
+ fulfil: ({ input, connectors }) => {
49
+ const saved = connectors.state.readState({ handle: 'draft' });
50
+ return {
51
+ value: saved.value,
52
+ revision: saved.revision,
53
+ status: saved.status,
54
+ proposal: input.proposal,
55
+ };
56
+ },
57
+ viewTitle: 'Your project brief',
58
+ invoking: 'Loading your saved brief…',
59
+ invoked: 'Your brief is ready to review',
60
+ view: { component: 'draft-card', entry: './views/draft-card.tsx' },
61
+ annotations: annotations.readOnly(),
62
+ viewDescription: 'Review and edit a saved brief before deciding whether to create an account.',
63
+ csp: { connectDomains: [], resourceDomains: [], frameDomains: [] },
64
+ });
65
+
66
+ const saveDraft = tool('save_draft', {
67
+ title: 'Save your brief',
68
+ description:
69
+ 'Save this reviewed brief for up to 24 hours. This does not create an account or a project.',
70
+ input: draft.required().extend({
71
+ expectedRevision: z.number().int().min(0).meta({ title: 'Saved version' }),
72
+ }),
73
+ output: snapshot,
74
+ annotations: annotations.localAction({ destructive: false, confirm: true }),
75
+ fulfil: ({ input, connectors }) => {
76
+ const saved = connectors.state.patchState({
77
+ handle: 'draft',
78
+ expectedRevision: input.expectedRevision,
79
+ value: { title: input.title, audience: input.audience, goal: input.goal },
80
+ });
81
+ return { value: saved.value, revision: saved.revision, status: saved.status };
82
+ },
83
+ });
84
+
85
+ const continueDraft = tool('continue_draft', {
86
+ title: 'Continue with an account',
87
+ description:
88
+ 'Read the brief in the signed-in account. Call only when the visitor chooses to continue with an account, after showing useful value. Sign-in carries the saved draft forward; it does not submit, publish, or create a project.',
89
+ input: z.object({}),
90
+ output: snapshot.extend({ accountId: z.string() }),
91
+ annotations: annotations.readOnly(),
92
+ fulfil: ({ connectors, user }) => {
93
+ const saved = connectors.state.readState({ handle: 'draft' });
94
+ return {
95
+ value: saved.value,
96
+ revision: saved.revision,
97
+ status: saved.status,
98
+ accountId: user.id,
99
+ };
100
+ },
101
+ });
102
+
103
+ export default server(
104
+ 'stateful_draft',
105
+ {
106
+ title: 'Your first useful brief',
107
+ version: '1.0.0',
108
+ instructions:
109
+ 'Help someone turn their goal into a concise project brief: a title, audience, and desired outcome. Base the brief on information they supply. Propose a sensible editable title; never require a naming question. If the audience and outcome are already clear, immediately show the complete proposed brief. Otherwise ask one short question for missing information and skip anything already answered. Show the useful proposed brief in chat before offering to save it. Do not request an email, password, document, or external research. Read open_draft before save_draft. Saving requires review and confirmation; an unconfirmed save is not persisted. Never claim an account or project was created. After saving, offer continue_draft only as an optional next step; never require signup to see the brief. This is a synthetic onboarding example, not an integration with a specific SaaS product.',
110
+ branding: {
111
+ name: 'First Brief',
112
+ accent: '#2563EB',
113
+ surface: '#F8FAFC',
114
+ surfaceDark: '#111827',
115
+ radius: 'md',
116
+ density: 'comfortable',
117
+ typography: 'system',
118
+ colorScheme: 'auto',
119
+ },
120
+ use: { state },
121
+ state: {
122
+ handles: {
123
+ draft: {
124
+ kind: 'draft',
125
+ version: 'v2',
126
+ scope: 'caller',
127
+ ttlSeconds: 86400,
128
+ claimOnAuthentication: true,
129
+ schema: draft,
130
+ },
131
+ },
132
+ },
133
+ assistant: embeddedAssistant({
134
+ model: noodleManaged(),
135
+ privacyUrl: 'https://noodleseed.com/privacy',
136
+ behavior: { showConfirmationDetails: false },
137
+ access: [
138
+ publicWebsite({
139
+ origins: ['http://localhost:3001'],
140
+ capabilities: [openDraft, saveDraft, continueDraft],
141
+ signIn: true,
142
+ }),
143
+ authenticatedWebsite({
144
+ origins: ['http://localhost:3002'],
145
+ capabilities: [openDraft, saveDraft, continueDraft],
146
+ }),
147
+ ],
148
+ labels: {
149
+ welcomeHeading: 'What would you like to achieve?',
150
+ composerPlaceholder: 'Describe your goal…',
151
+ signInHeading: 'Take your brief with you',
152
+ signInBody: 'Continue with an account to use your saved brief inside the product.',
153
+ signInAction: 'Sign in',
154
+ signUpAction: 'Create account',
155
+ },
156
+ }),
157
+ },
158
+ [openDraft, saveDraft, continueDraft],
159
+ );
@@ -0,0 +1,152 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useCallTool, useSendFollowUpMessage, useToolInfo } from '../helpers.js';
3
+ import './widget-style.css';
4
+
5
+ type Brief = { title?: string; audience?: string; goal?: string };
6
+ type Snapshot = { value: Brief; revision: number; status: string; proposal?: Brief };
7
+
8
+ function displayBrief(data: Snapshot | undefined): Brief {
9
+ const proposal = data?.proposal;
10
+ return proposal && (proposal.title || proposal.audience || proposal.goal)
11
+ ? proposal
12
+ : (data?.value ?? {});
13
+ }
14
+
15
+ function snapshot(result: unknown): Snapshot | undefined {
16
+ if (!result || typeof result !== 'object') return;
17
+ const envelope = result as { isError?: boolean; structuredContent?: Partial<Snapshot> };
18
+ const data = envelope.structuredContent;
19
+ if (
20
+ !envelope.isError &&
21
+ data &&
22
+ data.value &&
23
+ typeof data.value === 'object' &&
24
+ Number.isInteger(data.revision) &&
25
+ typeof data.status === 'string'
26
+ ) {
27
+ return data as Snapshot;
28
+ }
29
+ }
30
+
31
+ export default function DraftCard() {
32
+ const info = useToolInfo('open_draft');
33
+ const read = useCallTool('open_draft');
34
+ const save = useCallTool('save_draft');
35
+ const followUp = useSendFollowUpMessage();
36
+ const [saved, setSaved] = useState(() => snapshot(info));
37
+ const [brief, setBrief] = useState<Brief>(() => displayBrief(snapshot(info)));
38
+ const [busy, setBusy] = useState(false);
39
+ const [reloadRequired, setReloadRequired] = useState(false);
40
+ const [message, setMessage] = useState('Review your brief. An account is optional.');
41
+ useEffect(() => {
42
+ const next = snapshot({ structuredContent: info.structuredContent });
43
+ if (next) {
44
+ setSaved(next);
45
+ setBrief(displayBrief(next));
46
+ }
47
+ }, [info.structuredContent]);
48
+ const complete = Boolean(brief.title?.trim() && brief.audience?.trim() && brief.goal?.trim());
49
+ const unchanged = ['title', 'audience', 'goal'].every(
50
+ (key) => brief[key as keyof Brief] === saved?.value[key as keyof Brief],
51
+ );
52
+
53
+ async function run(operation: 'load' | 'save') {
54
+ setBusy(true);
55
+ try {
56
+ const result =
57
+ operation === 'load'
58
+ ? await read.callTool({})
59
+ : await save.callTool({ ...brief, expectedRevision: saved?.revision });
60
+ const next = snapshot(result);
61
+ if (!next) throw new Error('No authoritative result');
62
+ setSaved(next);
63
+ setBrief(displayBrief(next));
64
+ setReloadRequired(false);
65
+ setMessage(operation === 'save' ? 'Your brief is saved.' : 'Saved brief loaded.');
66
+ } catch {
67
+ setReloadRequired(true);
68
+ setMessage(
69
+ 'No save was confirmed. Reload saved to check the latest brief before trying again.',
70
+ );
71
+ } finally {
72
+ setBusy(false);
73
+ }
74
+ }
75
+
76
+ return (
77
+ <main className="brief-shell">
78
+ <header>
79
+ <p className="brief-eyebrow">YOUR FIRST USEFUL STEP</p>
80
+ <h1>Your project brief</h1>
81
+ <p>Make something useful before creating an account.</p>
82
+ </header>
83
+ <section className="brief-fields">
84
+ <label>
85
+ Project title
86
+ <input
87
+ required
88
+ maxLength={120}
89
+ value={brief.title ?? ''}
90
+ placeholder="A better first week"
91
+ onChange={(event) => setBrief({ ...brief, title: event.currentTarget.value })}
92
+ />
93
+ </label>
94
+ <label>
95
+ Who is this for?
96
+ <textarea
97
+ required
98
+ maxLength={240}
99
+ rows={2}
100
+ value={brief.audience ?? ''}
101
+ placeholder="New teammates joining our product team"
102
+ onChange={(event) => setBrief({ ...brief, audience: event.currentTarget.value })}
103
+ />
104
+ </label>
105
+ <label>
106
+ What would success look like?
107
+ <textarea
108
+ required
109
+ maxLength={240}
110
+ rows={3}
111
+ value={brief.goal ?? ''}
112
+ placeholder="Complete their first useful project in a week"
113
+ onChange={(event) => setBrief({ ...brief, goal: event.currentTarget.value })}
114
+ />
115
+ </label>
116
+ <p role="status" aria-live="polite">
117
+ {busy ? 'Waiting for the result…' : message}
118
+ </p>
119
+ <div className="brief-actions">
120
+ <button type="button" disabled={busy} onClick={() => void run('load')}>
121
+ Reload saved
122
+ </button>
123
+ <button
124
+ className="brief-primary"
125
+ type="button"
126
+ onClick={() => void run('save')}
127
+ disabled={busy || !saved || !complete || reloadRequired}
128
+ >
129
+ Save brief
130
+ </button>
131
+ </div>
132
+ </section>
133
+ <footer>
134
+ <p>
135
+ A saved brief lasts up to 24 hours and can follow you when you sign in. No project has
136
+ been created.
137
+ </p>
138
+ <button
139
+ type="button"
140
+ disabled={
141
+ busy || !complete || !unchanged || !saved || saved.revision === 0 || reloadRequired
142
+ }
143
+ onClick={() =>
144
+ void followUp({ prompt: 'I would like to continue with my saved brief in an account.' })
145
+ }
146
+ >
147
+ Continue with an account
148
+ </button>
149
+ </footer>
150
+ </main>
151
+ );
152
+ }
@@ -0,0 +1,93 @@
1
+ @layer stateful-draft {
2
+ .brief-shell {
3
+ box-sizing: border-box;
4
+ max-width: 600px;
5
+ margin: 0 auto;
6
+ padding: 24px;
7
+ color: var(--ns-text-primary, #172033);
8
+ background: var(--ns-surface, #f8fafc);
9
+ font-family: var(--ns-font, system-ui, sans-serif);
10
+ line-height: 1.5;
11
+ }
12
+ .brief-shell * {
13
+ box-sizing: border-box;
14
+ }
15
+ .brief-shell h1 {
16
+ margin: 4px 0;
17
+ font-size: 26px;
18
+ letter-spacing: -0.03em;
19
+ }
20
+ .brief-shell p {
21
+ color: var(--ns-text-secondary, #526077);
22
+ }
23
+ .brief-eyebrow {
24
+ font-size: 11px;
25
+ font-weight: 700;
26
+ letter-spacing: 0.12em;
27
+ }
28
+ .brief-fields {
29
+ display: grid;
30
+ gap: 16px;
31
+ margin: 24px 0;
32
+ }
33
+ .brief-shell label {
34
+ display: grid;
35
+ gap: 6px;
36
+ font-size: 14px;
37
+ font-weight: 600;
38
+ }
39
+ .brief-shell input,
40
+ .brief-shell textarea {
41
+ width: 100%;
42
+ padding: 10px 12px;
43
+ border: 1px solid var(--ns-border, #cbd5e1);
44
+ border-radius: var(--ns-radius-md, 8px);
45
+ color: inherit;
46
+ background: var(--ns-surface-raised, #fff);
47
+ font: inherit;
48
+ font-weight: 400;
49
+ resize: vertical;
50
+ }
51
+ .brief-actions {
52
+ display: flex;
53
+ flex-wrap: wrap;
54
+ gap: 8px;
55
+ }
56
+ .brief-shell button {
57
+ padding: 10px 16px;
58
+ border: 1px solid var(--ns-border, #cbd5e1);
59
+ border-radius: var(--ns-radius-md, 8px);
60
+ background: var(--ns-surface-raised, #fff);
61
+ color: inherit;
62
+ font: inherit;
63
+ font-size: 14px;
64
+ font-weight: 600;
65
+ cursor: pointer;
66
+ }
67
+ .brief-shell .brief-primary {
68
+ background: var(--ns-accent, #2563eb);
69
+ color: var(--ns-accent-text, #fff);
70
+ border-color: transparent;
71
+ }
72
+ .brief-shell button:disabled {
73
+ opacity: 0.5;
74
+ cursor: default;
75
+ }
76
+ .brief-shell :focus-visible {
77
+ outline: 2px solid var(--ns-focus, #2563eb);
78
+ outline-offset: 3px;
79
+ }
80
+ .brief-shell footer {
81
+ border-top: 1px solid var(--ns-border, #cbd5e1);
82
+ padding-top: 12px;
83
+ font-size: 13px;
84
+ }
85
+ @media (max-width: 380px) {
86
+ .brief-shell {
87
+ padding: 16px;
88
+ }
89
+ .brief-actions button {
90
+ width: 100%;
91
+ }
92
+ }
93
+ }