@noodleseed/agent-kit 0.94.0 → 0.96.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 (104) hide show
  1. package/manifest.json +571 -363
  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 +7 -12
  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/building-mcp-apps/references/widgets-and-apps.md +1 -1
  10. package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
  11. package/skills/claude-code/connecting-apis-to-mcp/references/authoring-workflow.md +7 -12
  12. package/skills/claude-code/connecting-apis-to-mcp/references/connect-an-api.md +2 -2
  13. package/skills/claude-code/creating-product-agent-guides/SKILL.md +1 -1
  14. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
  15. package/skills/claude-code/debugging-mcp-delivery/references/verify-and-recover.md +2 -1
  16. package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
  17. package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
  18. package/skills/claude-code/designing-mcp-products/references/authoring-workflow.md +7 -12
  19. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
  20. package/skills/claude-code/embedding-mcp-assistants/references/authoring-workflow.md +7 -12
  21. package/skills/claude-code/embedding-mcp-assistants/references/embedded-assistant.md +33 -27
  22. package/skills/claude-code/examples/acme-tasks/README.md +14 -4
  23. package/skills/claude-code/examples/customer-auth/README.md +3 -0
  24. package/skills/claude-code/examples/hello/README.md +6 -2
  25. package/skills/claude-code/examples/stateful-draft/README.md +115 -0
  26. package/skills/claude-code/examples/stateful-draft/noodle.json +5 -0
  27. package/skills/claude-code/examples/stateful-draft/package.json +22 -0
  28. package/skills/claude-code/examples/stateful-draft/site/client.js +44 -0
  29. package/skills/claude-code/examples/stateful-draft/site/demo.mjs +158 -0
  30. package/skills/claude-code/examples/stateful-draft/site/index.html +41 -0
  31. package/skills/claude-code/examples/stateful-draft/src/helpers.ts +6 -0
  32. package/skills/claude-code/examples/stateful-draft/src/server.ts +159 -0
  33. package/skills/claude-code/examples/stateful-draft/src/views/draft-card.tsx +152 -0
  34. package/skills/claude-code/examples/stateful-draft/src/views/widget-style.css +93 -0
  35. package/skills/claude-code/examples/stateful-draft/test/draft-card.test.tsx +88 -0
  36. package/skills/claude-code/examples/stateful-draft/test/server.test.ts +58 -0
  37. package/skills/claude-code/examples/stateful-draft/vitest.config.ts +6 -0
  38. package/skills/claude-code/examples/weather/README.md +4 -0
  39. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  40. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
  41. package/skills/claude-code/references/authoring-workflow.md +7 -12
  42. package/skills/claude-code/references/build-an-mcp-app.md +2 -2
  43. package/skills/claude-code/references/build-an-mcp-server.md +2 -2
  44. package/skills/claude-code/references/connect-an-api.md +2 -2
  45. package/skills/claude-code/references/embedded-assistant.md +33 -27
  46. package/skills/claude-code/references/examples.md +1 -1
  47. package/skills/claude-code/references/verify-and-recover.md +2 -1
  48. package/skills/claude-code/references/widgets-and-apps.md +1 -1
  49. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
  50. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
  51. package/skills/claude-code/verifying-mcp-delivery/references/verify-and-recover.md +2 -1
  52. package/skills/claude-code/wrapping-existing-applications/SKILL.md +1 -1
  53. package/skills/claude-code/wrapping-existing-applications/references/authoring-workflow.md +7 -12
  54. package/skills/codex/SKILL.md +1 -1
  55. package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
  56. package/skills/codex/authoring-mcp-servers/references/authoring-workflow.md +7 -12
  57. package/skills/codex/authoring-mcp-servers/references/build-an-mcp-server.md +2 -2
  58. package/skills/codex/building-mcp-apps/SKILL.md +1 -1
  59. package/skills/codex/building-mcp-apps/references/build-an-mcp-app.md +2 -2
  60. package/skills/codex/building-mcp-apps/references/widgets-and-apps.md +1 -1
  61. package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
  62. package/skills/codex/connecting-apis-to-mcp/references/authoring-workflow.md +7 -12
  63. package/skills/codex/connecting-apis-to-mcp/references/connect-an-api.md +2 -2
  64. package/skills/codex/creating-product-agent-guides/SKILL.md +1 -1
  65. package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
  66. package/skills/codex/debugging-mcp-delivery/references/verify-and-recover.md +2 -1
  67. package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
  68. package/skills/codex/designing-mcp-products/SKILL.md +1 -1
  69. package/skills/codex/designing-mcp-products/references/authoring-workflow.md +7 -12
  70. package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
  71. package/skills/codex/embedding-mcp-assistants/references/authoring-workflow.md +7 -12
  72. package/skills/codex/embedding-mcp-assistants/references/embedded-assistant.md +33 -27
  73. package/skills/codex/examples/acme-tasks/README.md +14 -4
  74. package/skills/codex/examples/customer-auth/README.md +3 -0
  75. package/skills/codex/examples/hello/README.md +6 -2
  76. package/skills/codex/examples/stateful-draft/README.md +115 -0
  77. package/skills/codex/examples/stateful-draft/noodle.json +5 -0
  78. package/skills/codex/examples/stateful-draft/package.json +22 -0
  79. package/skills/codex/examples/stateful-draft/site/client.js +44 -0
  80. package/skills/codex/examples/stateful-draft/site/demo.mjs +158 -0
  81. package/skills/codex/examples/stateful-draft/site/index.html +41 -0
  82. package/skills/codex/examples/stateful-draft/src/helpers.ts +6 -0
  83. package/skills/codex/examples/stateful-draft/src/server.ts +159 -0
  84. package/skills/codex/examples/stateful-draft/src/views/draft-card.tsx +152 -0
  85. package/skills/codex/examples/stateful-draft/src/views/widget-style.css +93 -0
  86. package/skills/codex/examples/stateful-draft/test/draft-card.test.tsx +88 -0
  87. package/skills/codex/examples/stateful-draft/test/server.test.ts +58 -0
  88. package/skills/codex/examples/stateful-draft/vitest.config.ts +6 -0
  89. package/skills/codex/examples/weather/README.md +4 -0
  90. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  91. package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
  92. package/skills/codex/references/authoring-workflow.md +7 -12
  93. package/skills/codex/references/build-an-mcp-app.md +2 -2
  94. package/skills/codex/references/build-an-mcp-server.md +2 -2
  95. package/skills/codex/references/connect-an-api.md +2 -2
  96. package/skills/codex/references/embedded-assistant.md +33 -27
  97. package/skills/codex/references/examples.md +1 -1
  98. package/skills/codex/references/verify-and-recover.md +2 -1
  99. package/skills/codex/references/widgets-and-apps.md +1 -1
  100. package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
  101. package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
  102. package/skills/codex/verifying-mcp-delivery/references/verify-and-recover.md +2 -1
  103. package/skills/codex/wrapping-existing-applications/SKILL.md +1 -1
  104. package/skills/codex/wrapping-existing-applications/references/authoring-workflow.md +7 -12
@@ -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
+ }
@@ -0,0 +1,88 @@
1
+ // @vitest-environment happy-dom
2
+ import { act } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { afterEach, beforeEach, expect, it, vi } from 'vitest';
5
+
6
+ const callTool = vi.fn();
7
+ const followUp = vi.fn();
8
+ const initial = {
9
+ value: { title: 'Team launch', audience: 'New teammates', goal: 'Complete their first project' },
10
+ revision: 4,
11
+ status: 'active',
12
+ };
13
+ let entry: unknown = initial;
14
+ vi.mock('../src/helpers.js', () => ({
15
+ useToolInfo: () => ({ structuredContent: entry }),
16
+ useCallTool: () => ({ callTool }),
17
+ useSendFollowUpMessage: () => followUp,
18
+ }));
19
+
20
+ import DraftCard from '../src/views/draft-card.js';
21
+
22
+ let host: HTMLDivElement;
23
+ let root: ReturnType<typeof createRoot>;
24
+ beforeEach(async () => {
25
+ vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
26
+ callTool.mockReset();
27
+ followUp.mockReset();
28
+ entry = initial;
29
+ host = document.createElement('div');
30
+ document.body.append(host);
31
+ root = createRoot(host);
32
+ await act(async () => root.render(<DraftCard />));
33
+ });
34
+ afterEach(() => {
35
+ act(() => root.unmount());
36
+ host.remove();
37
+ });
38
+ function button(label: string) {
39
+ const found = [...host.querySelectorAll('button')].find((entry) => entry.textContent === label);
40
+ if (!found) throw new Error(`Missing button: ${label}`);
41
+ return found;
42
+ }
43
+
44
+ it('saves using the server revision and displays only the returned result', async () => {
45
+ callTool.mockResolvedValue({ structuredContent: { ...initial, revision: 8 } });
46
+ await act(async () => button('Save brief').click());
47
+ expect(callTool).toHaveBeenCalledWith({ ...initial.value, expectedRevision: 4 });
48
+ expect(host.textContent).toContain('Your brief is saved.');
49
+ expect(button('Continue with an account').disabled).toBe(false);
50
+ });
51
+
52
+ it('shows a proposed brief without pretending it is already saved', async () => {
53
+ entry = { value: {}, revision: 0, status: 'active', proposal: initial.value };
54
+ await act(async () => root.render(<DraftCard />));
55
+ expect(host.querySelector('input')?.value).toBe('Team launch');
56
+ expect(button('Continue with an account').disabled).toBe(true);
57
+ callTool.mockResolvedValue({ structuredContent: { ...initial, revision: 1 } });
58
+ await act(async () => button('Save brief').click());
59
+ expect(callTool).toHaveBeenCalledWith({ ...initial.value, expectedRevision: 0 });
60
+ });
61
+
62
+ it('does not invent a save when confirmation is pending or the response is missing', async () => {
63
+ callTool.mockResolvedValue({});
64
+ await act(async () => button('Save brief').click());
65
+ expect(host.textContent).not.toContain('Your brief is saved.');
66
+ expect(host.textContent).toContain('No save was confirmed.');
67
+ });
68
+
69
+ it('retains edits on a stale write and requires a reload before another save', async () => {
70
+ callTool.mockResolvedValue({ isError: true });
71
+ await act(async () => button('Save brief').click());
72
+ expect(host.querySelector('input')?.value).toBe('Team launch');
73
+ expect(button('Save brief').disabled).toBe(true);
74
+ expect(host.textContent).toContain('Reload saved');
75
+ callTool.mockResolvedValue({ structuredContent: { ...initial, revision: 7 } });
76
+ await act(async () => button('Reload saved').click());
77
+ await act(async () => button('Save brief').click());
78
+ expect(callTool).toHaveBeenLastCalledWith({ ...initial.value, expectedRevision: 7 });
79
+ });
80
+
81
+ it('keeps continuing separate from saving and makes no project-creation claim', async () => {
82
+ await act(async () => button('Continue with an account').click());
83
+ expect(callTool).not.toHaveBeenCalled();
84
+ expect(followUp).toHaveBeenCalledWith({
85
+ prompt: 'I would like to continue with my saved brief in an account.',
86
+ });
87
+ expect(host.textContent).not.toContain('Project created');
88
+ });
@@ -0,0 +1,58 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { validate } from '@noodleseed/one';
3
+ import { describe, expect, it } from 'vitest';
4
+ import app from '../src/server.js';
5
+
6
+ describe('stateful draft onboarding reference', () => {
7
+ it('reads and saves authoritative state instead of a widget-only copy', async () => {
8
+ const manifest = await app.toManifest();
9
+ expect(manifest.tools.find((entry) => entry.name === 'open_draft')?.fulfilment.steps).toEqual([
10
+ expect.objectContaining({ use: 'state.read_state', args: { handle: 'draft' } }),
11
+ ]);
12
+ expect(manifest.tools.find((entry) => entry.name === 'save_draft')).toMatchObject({
13
+ annotations: { readOnlyHint: false, confirm: true },
14
+ fulfilment: {
15
+ steps: [
16
+ expect.objectContaining({
17
+ use: 'state.patch_state',
18
+ args: {
19
+ handle: 'draft',
20
+ expectedRevision: '${input.expectedRevision}',
21
+ value: {
22
+ title: '${input.title}',
23
+ audience: '${input.audience}',
24
+ goal: '${input.goal}',
25
+ },
26
+ },
27
+ }),
28
+ ],
29
+ },
30
+ });
31
+ });
32
+
33
+ it('limits anonymous access and transfers only an expiring draft after verified login', async () => {
34
+ const manifest = await app.toManifest();
35
+ expect(manifest.state?.handles.draft).toMatchObject({
36
+ scope: 'caller',
37
+ ttlSeconds: 86400,
38
+ claimOnAuthentication: true,
39
+ });
40
+ expect(manifest.server.assistant?.surfaces?.map((surface) => surface.mode)).toEqual([
41
+ 'mixed',
42
+ 'authenticated',
43
+ ]);
44
+ const continued = manifest.tools.find((entry) => entry.name === 'continue_draft');
45
+ expect(continued?.annotations?.readOnlyHint).toBe(true);
46
+ expect(continued?.fulfilment.output).toMatchObject({ accountId: '${user.id}' });
47
+ expect(continued?.fulfilment.steps).toEqual([
48
+ expect.objectContaining({ use: 'state.read_state', args: { handle: 'draft' } }),
49
+ ]);
50
+ });
51
+
52
+ it('compiles through the public validator, including anonymous action confirmation', async () => {
53
+ const result = await validate({
54
+ manifestPath: fileURLToPath(new URL('../src/server.ts', import.meta.url)),
55
+ });
56
+ expect(result.ok, JSON.stringify(result.ok ? [] : result.errors)).toBe(true);
57
+ });
58
+ });
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ oxc: { jsx: { runtime: 'automatic' } },
5
+ test: { include: ['test/**/*.test.{ts,tsx}'], testTimeout: 30_000, maxWorkers: 2 },
6
+ });
@@ -7,6 +7,10 @@ Capability slots: HTTP connector authoring, ordered fulfilment flows, query/resp
7
7
  **list-returning connector output** (a connector that returns a live, variable-length array), and
8
8
  sandboxed compute, including an explicit least-privilege per-operation response-size bound.
9
9
 
10
+ For a different API with an OpenAPI document, start with `noodle import openapi <file>` in a separate
11
+ directory. Its shared schemas and offline test establish the contract, not live behavior; follow the
12
+ [connector guide](https://docs.noodleseed.dev/docs/guides/connectors) before replacing this curated flow.
13
+
10
14
  1. **`geo.search`** → geocode the city to coordinates (Open-Meteo Geocoding API)
11
15
  2. **`forecast.current`** → fetch current weather for those coordinates (Open-Meteo Forecast API)
12
16
  3. **`brief.summarize`** → derive a human-readable briefing in a **WASM/QuickJS compute sandbox**
@@ -3,7 +3,7 @@ name: executing-noodle-plans
3
3
  description: "Use when the user asks to execute an approved, decision-complete implementation plan for a Noodle Seed project task by task with test-first changes, review, recovery, and final verification."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.94.0 hash:6a9f132ddb79352e -->
6
+ <!-- noodle-skill version:0.96.0 hash:6a9f132ddb79352e -->
7
7
 
8
8
  # Execute a Noodle Seed implementation plan
9
9
 
@@ -3,7 +3,7 @@ name: publishing-mcp-integrations
3
3
  description: "Use when preparing, reviewing, or submitting a Noodle Seed MCP integration to a host or app directory."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.94.0 hash:0ccafb222038f553 -->
6
+ <!-- noodle-skill version:0.96.0 hash:0ccafb222038f553 -->
7
7
 
8
8
  # publishing-mcp-integrations
9
9
 
@@ -22,8 +22,9 @@
22
22
  ## Input paths
23
23
 
24
24
  1. **Website scrape** — if the user gives a URL, scrape it for surface hints (products, services, hours, contact, pricing). Stop there: the URL does not reveal CRM, booking systems, custom APIs, auth model, eligibility rules, quoting logic, or approval flows. Those live in the business systems and the owner’s head — ask.
25
- 2. **OpenAPI import** — `noodle import openapi <file>` emits a starter `server.ts` from a spec. Use it when the user provides an OpenAPI document.
25
+ 2. **OpenAPI import** — `noodle import openapi <file>` writes a pinned project at `src/server.ts`, shared operation/tool schemas, managed auth references, and an offline contract test. It does not install dependencies or call the backend. Follow its README, run `agent:check`, and add a reviewed sandbox-operation test; compile success is not live integration proof. Review unsupported-body/auth warnings before deployment. Modified files are preserved unless `--force` is explicit.
26
26
  3. **Upstream MCP import** — `noodle import mcp <url> --name <slug> --output <dir>` discovers `tools/list` once, validates and freezes tool schemas into TypeScript, and writes a secret-free drift snapshot. Upstream annotations are untrusted, so generated tools remain destructive confirmed actions until an author verifies and narrows them. Use `--header-env <header>=<ENV_NAME>` for import-only auth and `--check` for classified, non-mutating drift detection. Runtime never performs discovery.
27
+ Both imports use `src/server.ts` as the declared entrypoint, include an offline compile test and `.env.example`, and write files only. Install the pinned dependencies, run the generated checks, then `noodle agents setup --apply`. Never treat generated contract tests as customer authentication or business-workflow evidence.
27
28
  4. **User interview** — Noodle does not interview; you do. Cover custom APIs/integrations, eligibility rules, quoting/approval logic, and private schemas (SQL DDL or JSON samples for custom `connector` declarations). Ask for concrete examples and sample payloads; do not guess a schema from a URL or invent endpoints.
28
29
 
29
30
  ## Fit check
@@ -393,20 +394,14 @@ Compute `run` functions are serialized and sandboxed: no imports, no closure cap
393
394
 
394
395
  ## Tests
395
396
 
396
- Use Vitest for app-local tests. The generated `npm test` command scans only the project-owned `test/` directory; skill-local example tests are reference material, not part of the app suite. Keep fixtures project-local; do not import from `examples/`. A minimum test suite imports the default server, checks the intended definitions compile, then lets `noodle test --json` perform the loopback MCP smoke.
397
+ Start from the generated `test/server.test.ts`, not a blank test file. `npm test` runs `vitest run --dir test` over the project-owned `test/` directory; skill-local example tests are reference material. The generated suite compiles the actual source/view, lists tools, asserts a representative result and rejects invalid input. It copies source into temporary fixture storage and uses synthetic configuration, never customer secrets or saved hosted targets. The HTTP profile proves one local backend read and zero extra calls for invalid input.
397
398
 
398
- ```ts
399
- import { describe, expect, it } from 'vitest';
400
- import app from '../src/server.js';
401
-
402
- describe('server', () => {
403
- it('declares the expected tool surface', () => {
404
- expect(app.name).toBe('support_assistant');
405
- });
406
- });
399
+ ```sh
400
+ npm test
401
+ npm run agent:check
407
402
  ```
408
403
 
409
- After focused tests pass, run `noodle validate --json`, `noodle test --json`, and then `noodle dev` for interactive local verification.
404
+ Adapt the representative arguments and assertions when business contracts change; keep invalid-input and authorization cases. A name/export assertion or `noodle test --json` without `--tool` proves no useful call. After fixture tests pass, bind the real local configuration, run an authorized representative read, and use `noodle dev` for interactive checks. Never use a production mutation or fixture success as customer-readiness evidence.
410
405
 
411
406
  ## Secrets and variables
412
407
 
@@ -25,14 +25,14 @@ Before implementation, capture a short design spec: target user, conversational
25
25
  4. **Preserve fallback.** Every tool that launches a widget must still return useful text without the widget, so unsupported hosts and failed rendering remain usable.
26
26
  5. **Decide product-guide coverage.** Record the required product-guide decision and its reason, then use `references/product-agent-guides.md` as the canonical selection and authoring guidance.
27
27
  6. **Author and wire the App contract.** Follow `references/widgets-and-apps.md` for the canonical component guidance, view registration, hooks, state, CSP, tool visibility, and output shaping. Keep tool effects and confirmation semantics correct independently of the UI.
28
- 7. **Validate the local artifact.** Run `noodle validate --json`, `noodle test --json`, and `noodle check --json`. Repair failures at the layer that produced them.
28
+ 7. **Validate the local artifact.** Run the generated/adapted `npm test`, `noodle validate --json` and `noodle check --json`. Prove a representative result and negative case, not just registration. Synthetic preference previews do not save; replace their backend seam and verify the authorized effect before showing success.
29
29
  8. **Inspect the experience.** Run `noodle devtools` and verify loading, empty, error, success, responsive layout, focus/keyboard behavior, and the text fallback.
30
30
  9. **Escalate evidence only on request.** Run a host test only when the user requested host verification. Run host-specific compliance only when preparing that host submission; select the exact host-testing or compliance entry from the router lookup catalog only after that evidence level is explicitly requested.
31
31
 
32
32
  ## Verification evidence
33
33
 
34
34
  - **Product:** the design spec states the user benefit, UI fit decision, and product-guide decision with its reason.
35
- - **Server:** `noodle validate --json` and `noodle test --json` succeeded.
35
+ - **Server:** compilation, a representative call and negative input/authorization cases passed; report whether fixtures or a real authorized backend were used.
36
36
  - **App contract:** `noodle check --json` succeeded.
37
37
  - **Local UX:** `noodle devtools` exercised the relevant states and the useful text fallback without the widget.
38
38
  - **Host/compliance:** report each requested host or compliance check with its evidence; report every unperformed higher level as not run.