@waniwani/kit 0.1.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/LICENSE +21 -0
- package/README.md +566 -0
- package/cli/account.mjs +264 -0
- package/cli/codegen.mjs +1069 -0
- package/cli/framework.mjs +244 -0
- package/cli/index.mjs +563 -0
- package/cli/log.mjs +177 -0
- package/cli/scan.mjs +84 -0
- package/cli/template.mjs +152 -0
- package/cli/tunnel.mjs +140 -0
- package/cli/validate.mjs +248 -0
- package/dist/index.d.ts +113 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +71 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +212 -0
- package/dist/server.js.map +1 -0
- package/dist/web.d.ts +39 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +35 -0
- package/dist/web.js.map +1 -0
- package/package.json +88 -0
- package/src/index.ts +138 -0
- package/src/server.ts +285 -0
- package/src/web.tsx +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WaniWani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
# @waniwani/kit
|
|
2
|
+
|
|
3
|
+
**Build an MCP app as a folder.** You write tools, widgets, flows and docs into
|
|
4
|
+
a directory, and one CLI turns that directory into a deployable MCP server. Your
|
|
5
|
+
repo holds none of the plumbing: server bootstrap, transport wiring, build
|
|
6
|
+
configuration.
|
|
7
|
+
|
|
8
|
+
Your side of this is the distribution MCP itself, meaning the tools, the
|
|
9
|
+
widgets, the funnel and the content. We own everything technical underneath it,
|
|
10
|
+
which covers the server, the transport, bundling, the deploy files, and keeping
|
|
11
|
+
up with framework upgrades.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
oney/ # what you write
|
|
15
|
+
├── waniwani.config.ts
|
|
16
|
+
├── tools/check-eligibility.ts
|
|
17
|
+
├── widgets/select-plan/{widget.ts,ui.tsx}
|
|
18
|
+
├── flows/split-payment.ts
|
|
19
|
+
└── docs/*.md
|
|
20
|
+
|
|
21
|
+
waniwani build # → .waniwani/, an ordinary npm project
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The machinery underneath is in [INTERNALS.md](https://github.com/WaniWani-AI/kit/blob/main/INTERNALS.md): template
|
|
25
|
+
resolution, dependency overrides, CLI output, publishing, the full gap list.
|
|
26
|
+
|
|
27
|
+
## The three packages
|
|
28
|
+
|
|
29
|
+
Three packages ship under the `@waniwani` scope. The pair people mix up is the
|
|
30
|
+
kit and the SDK, so start there.
|
|
31
|
+
|
|
32
|
+
| | what it is | you use it when |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| **`@waniwani/sdk`** | A **library**. Flows (typed state graphs that compile to one MCP tool), event tracking, knowledge base, chat widget. You supply the `McpServer`, the transport and the build. | You already have an MCP server, or you want one you control down to the last line, and you want funnels, tracking or KB inside it. |
|
|
35
|
+
| **`@waniwani/kit`** | A **framework**. Folder convention, build CLI, shared server runtime. It owns the server, the transport, the bundler and the deploy files, so your repo can hold none of them. | You want to ship an MCP app and own no plumbing. |
|
|
36
|
+
| **`@waniwani/cli`** | The **platform CLI**. `login`, `logout`, `switch`, `connect`. OAuth into WaniWani, bind a repo to a hosted agent, run against the hosted playground. | You want your local server wired to app.waniwani.ai. |
|
|
37
|
+
|
|
38
|
+
### Kit against SDK, in code
|
|
39
|
+
|
|
40
|
+
With the SDK, the server is a file you write and keep:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/server.ts, yours to maintain
|
|
44
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
45
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
46
|
+
|
|
47
|
+
const server = new McpServer({ name: "oney", version: "1.0.0" });
|
|
48
|
+
|
|
49
|
+
server.registerTool(
|
|
50
|
+
{ name: "check-eligibility", title, description, inputSchema, annotations },
|
|
51
|
+
async (input) => { /* … */ },
|
|
52
|
+
);
|
|
53
|
+
await flow.register(server);
|
|
54
|
+
await server.connect(new StreamableHTTPServerTransport(/* … */));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Around that file you also own a `tsconfig.json`, a bundler config for any widget
|
|
58
|
+
UI, a `Dockerfile` and a `vercel.json`.
|
|
59
|
+
|
|
60
|
+
With the kit, you write the part that answers the question and nothing around
|
|
61
|
+
it:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// tools/check-eligibility.ts
|
|
65
|
+
export default defineTool({ title, description, input, output, hints, run });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The kit finds that file, derives its tool name, registers it, bundles any widget
|
|
69
|
+
that goes with it, and emits a deployable project. One copy of the plumbing
|
|
70
|
+
exists, it lives in this package, and fixing it costs one publish plus a
|
|
71
|
+
dependency bump per app.
|
|
72
|
+
|
|
73
|
+
The dependency arrow points one way. The kit depends on the SDK, and the SDK
|
|
74
|
+
knows nothing about the kit. Inside a kit app, `createFlow(...)` comes from the
|
|
75
|
+
SDK, while `defineApp`, `defineTool`, `defineWidget` and the server that
|
|
76
|
+
registers them come from the kit.
|
|
77
|
+
|
|
78
|
+
> **Bin collision, today.** `@waniwani/cli@0.1.15` also claims the `waniwani`
|
|
79
|
+
> binary on npm, so installing both conflicts. The plan is to absorb `login`,
|
|
80
|
+
> `logout`, `switch` and `connect` into this package and deprecate the other.
|
|
81
|
+
> See [Status](#status).
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
There is no `waniwani init` yet, so the first files go in by hand.
|
|
86
|
+
[examples/oney](https://github.com/WaniWani-AI/kit/blob/main/examples/oney) is this same app finished, if you would rather
|
|
87
|
+
read it than type it.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
mkdir oney && cd oney
|
|
91
|
+
npm init -y
|
|
92
|
+
npm i @waniwani/kit @waniwani/sdk react react-dom zod
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Set `"type": "module"` and the scripts:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"scripts": {
|
|
100
|
+
"check": "waniwani check",
|
|
101
|
+
"dev": "waniwani dev",
|
|
102
|
+
"build": "waniwani build",
|
|
103
|
+
"start": "waniwani start"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**1. Name the app and tell the model how to behave.**
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
// waniwani.config.ts
|
|
112
|
+
import { defineApp } from "@waniwani/kit";
|
|
113
|
+
|
|
114
|
+
export default defineApp({
|
|
115
|
+
name: "oney",
|
|
116
|
+
title: "Oney: split your payment",
|
|
117
|
+
instructions: `You help shoppers split a purchase into instalments with Oney.
|
|
118
|
+
|
|
119
|
+
RULES:
|
|
120
|
+
- Never quote a monthly amount yourself. Call check-eligibility and let it do the arithmetic.
|
|
121
|
+
- Never list the plans in text. Show the select-plan widget and let it render them.`,
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`instructions` reaches the host LLM once, before any tool call.
|
|
126
|
+
|
|
127
|
+
**2. Write a tool.** The filename becomes the tool name.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// tools/check-eligibility.ts
|
|
131
|
+
import { defineTool } from "@waniwani/kit";
|
|
132
|
+
import { z } from "zod";
|
|
133
|
+
import { buildPlans } from "../lib/plans.js";
|
|
134
|
+
|
|
135
|
+
export default defineTool({
|
|
136
|
+
title: "Check instalment eligibility",
|
|
137
|
+
description:
|
|
138
|
+
"Work out which instalment plans a basket qualifies for. Call this before showing any plans, and before quoting any figure.",
|
|
139
|
+
input: {
|
|
140
|
+
amount: z.number().positive().describe("Basket total in euros, e.g. 249.90"),
|
|
141
|
+
country: z.enum(["FR", "ES", "PT"]).default("FR"),
|
|
142
|
+
},
|
|
143
|
+
output: {
|
|
144
|
+
eligible: z.boolean(),
|
|
145
|
+
plans: z.array(z.object({ id: z.string(), monthly: z.number(), fee: z.number() })),
|
|
146
|
+
},
|
|
147
|
+
hints: { readOnly: true },
|
|
148
|
+
run: ({ amount, country }) =>
|
|
149
|
+
amount < 50
|
|
150
|
+
? { eligible: false, plans: [] }
|
|
151
|
+
: { eligible: true, plans: buildPlans(amount, country) },
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`input` and `output` are Zod shapes, written as plain objects rather than
|
|
156
|
+
`z.object({ … })`. `hints` becomes MCP annotations, with the runtime filling in
|
|
157
|
+
the `title` that Claude's Connectors Directory requires.
|
|
158
|
+
|
|
159
|
+
**3. Write a widget.** The folder name becomes the tool name, and the widget
|
|
160
|
+
takes two files.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// widgets/select-plan/widget.ts
|
|
164
|
+
import { defineWidget } from "@waniwani/kit";
|
|
165
|
+
import { z } from "zod";
|
|
166
|
+
|
|
167
|
+
const plan = z.object({
|
|
168
|
+
id: z.string(),
|
|
169
|
+
label: z.string().describe("Short label, e.g. '3×'."),
|
|
170
|
+
monthly: z.number(),
|
|
171
|
+
fee: z.number(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export default defineWidget({
|
|
175
|
+
title: "Choose an instalment plan",
|
|
176
|
+
description:
|
|
177
|
+
"Show the instalment plan picker. Call this once check-eligibility has returned plans, passing them straight through. The widget renders every figure itself: do NOT list the plans in text.",
|
|
178
|
+
data: {
|
|
179
|
+
amount: z.number(),
|
|
180
|
+
plans: z.array(plan).describe("Plans returned by check-eligibility, unmodified."),
|
|
181
|
+
},
|
|
182
|
+
llmText: (data) =>
|
|
183
|
+
`The picker is on screen with ${data.plans.length} plans. Wait for the user to pick one.`,
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
// widgets/select-plan/ui.tsx
|
|
189
|
+
import { useSendFollowUpMessage, useWidget } from "@waniwani/kit/web";
|
|
190
|
+
import widget from "./widget.js";
|
|
191
|
+
|
|
192
|
+
export default function SelectPlan() {
|
|
193
|
+
const { data } = useWidget(widget);
|
|
194
|
+
const sendFollowUp = useSendFollowUpMessage();
|
|
195
|
+
if (!data) return <div className="font-sans text-ink-muted">Loading your plans…</div>;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="font-sans text-ink">
|
|
199
|
+
{data.plans.map((plan) => (
|
|
200
|
+
<button
|
|
201
|
+
key={plan.id}
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => sendFollowUp(`I'll take the ${plan.label} plan.`)}
|
|
204
|
+
>
|
|
205
|
+
{plan.label}: €{plan.monthly}/month
|
|
206
|
+
</button>
|
|
207
|
+
))}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**4. Run it.**
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run dev
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`dev` watches the folder, mirrors changes into `.waniwani/`, and leaves nodemon
|
|
220
|
+
and Vite HMR to do the rest. An edit to `tools/check-eligibility.ts` reaches the
|
|
221
|
+
MCP endpoint in about a second. Point a client at `/mcp`, or run
|
|
222
|
+
[scripts/probe.mjs](https://github.com/WaniWani-AI/kit/blob/main/scripts/probe.mjs) against it to exercise the server without
|
|
223
|
+
a chat client.
|
|
224
|
+
|
|
225
|
+
## The folder convention
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
oney/
|
|
229
|
+
├── waniwani.config.ts defineApp({ name, title, instructions })
|
|
230
|
+
├── tools/
|
|
231
|
+
│ └── check-eligibility.ts export default defineTool({ ..., run })
|
|
232
|
+
├── widgets/
|
|
233
|
+
│ └── select-plan/
|
|
234
|
+
│ ├── widget.ts export default defineWidget({ ..., data })
|
|
235
|
+
│ └── ui.tsx export default function Component()
|
|
236
|
+
├── flows/
|
|
237
|
+
│ └── split-payment.ts export default createFlow(...).compile() ← SDK
|
|
238
|
+
├── docs/
|
|
239
|
+
│ ├── fees.md becomes the `search_docs` tool
|
|
240
|
+
│ └── eligibility.md
|
|
241
|
+
└── lib/ anything else is just modules
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Names come from the filesystem, verbatim. `tools/check-eligibility.ts` registers
|
|
245
|
+
as `check-eligibility`, and `widgets/select-plan/` registers as `select-plan`.
|
|
246
|
+
Nothing has to be listed in a registry, so no widget can sit defined and
|
|
247
|
+
unwired.
|
|
248
|
+
|
|
249
|
+
| folder | becomes | notes |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `tools/<name>.ts` | one MCP tool | `.ts`, `.tsx` and `.mts` are picked up |
|
|
252
|
+
| `widgets/<name>/` | one MCP tool plus a `ui://` resource | needs `widget.ts` and `ui.tsx` |
|
|
253
|
+
| `flows/<name>.ts` | one MCP tool, registered from the SDK unchanged | whatever `.compile()` returns |
|
|
254
|
+
| `docs/*.md` | a single `search_docs` tool over all of them | the first `# heading` becomes the title |
|
|
255
|
+
| anything else | plain modules | the CLI leaves it alone |
|
|
256
|
+
|
|
257
|
+
The app folder imports `@waniwani/kit`, plus `@waniwani/sdk` when it uses flows,
|
|
258
|
+
and nothing else. Skybridge, transports and build configuration all stay outside
|
|
259
|
+
it.
|
|
260
|
+
|
|
261
|
+
### Why a widget is two files
|
|
262
|
+
|
|
263
|
+
`widget.ts` gets imported by the server and by the browser bundle, so it stays
|
|
264
|
+
free of React and CSS. It carries one `data` schema, which serves as the tool's
|
|
265
|
+
input schema, its structured output, and the type the component receives:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
// widgets/select-plan/widget.ts
|
|
269
|
+
export default defineWidget({
|
|
270
|
+
title: "Choose an instalment plan",
|
|
271
|
+
description: "Show the instalment plan picker. …",
|
|
272
|
+
data: { amount: z.number(), plans: z.array(plan) },
|
|
273
|
+
llmText: (data) => `… ${data.plans.length} plans …`,
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
// widgets/select-plan/ui.tsx
|
|
279
|
+
const { data } = useWidget(widget); // typed off `data`, no generated helpers
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The usual approach puts `generateHelpers<AppType>()` in a shared file typed
|
|
283
|
+
against the server, which makes a widget's type depend on the server's shape.
|
|
284
|
+
Here the widget owns its own contract, so the two cannot drift.
|
|
285
|
+
|
|
286
|
+
`data` arrives as soon as the host has the tool input, which on most hosts
|
|
287
|
+
happens before the server responds, so render optimistically and reach for
|
|
288
|
+
`isReady` when you need the final value.
|
|
289
|
+
|
|
290
|
+
### Flows come from the SDK
|
|
291
|
+
|
|
292
|
+
A flow is an SDK primitive used unchanged. `createFlow(...).compile()` returns
|
|
293
|
+
something the runtime registers directly, with no wrapper and no adapter, so
|
|
294
|
+
everything the SDK documents about flows applies here as written.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// flows/split-payment.ts
|
|
298
|
+
import { createFlow, END, MemoryKvStore, START } from "@waniwani/sdk/mcp";
|
|
299
|
+
|
|
300
|
+
export default createFlow({ id: "split_payment", title, description, state })
|
|
301
|
+
.addNode({
|
|
302
|
+
id: "ask_amount",
|
|
303
|
+
run: ({ interrupt }) => interrupt({ amount: { question: "How much is the basket?" } }),
|
|
304
|
+
})
|
|
305
|
+
.addNode({
|
|
306
|
+
id: "show_plans",
|
|
307
|
+
run: ({ state, showWidget }) =>
|
|
308
|
+
showWidget({ tool: "select-plan", field: "selectedPlanId", data: { /* … */ } }),
|
|
309
|
+
})
|
|
310
|
+
.addEdge(START, "ask_amount")
|
|
311
|
+
.addEdge("ask_amount", "show_plans")
|
|
312
|
+
.addEdge("show_plans", END)
|
|
313
|
+
.compile({ store: new MemoryKvStore() });
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`showWidget({ tool: "select-plan" })` names a widget by its folder name, and the
|
|
317
|
+
build check verifies that the folder exists.
|
|
318
|
+
|
|
319
|
+
### Styling is Tailwind, and only Tailwind
|
|
320
|
+
|
|
321
|
+
A widget styles itself with utility classes in its `ui.tsx`. There is no
|
|
322
|
+
`styles.css` at any level of an app folder, and nothing imports one:
|
|
323
|
+
|
|
324
|
+
```tsx
|
|
325
|
+
<span className="text-xs font-bold tracking-wide text-ink-muted dark:text-slate-400">
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`text-ink-muted` is not a Tailwind default. It comes from the distribution
|
|
329
|
+
template's `src/index.css`, which is the Tailwind entry and the design system in
|
|
330
|
+
one file:
|
|
331
|
+
|
|
332
|
+
```css
|
|
333
|
+
@import "tailwindcss";
|
|
334
|
+
|
|
335
|
+
/* The host hands the colour scheme to the view rather than to the browser, so
|
|
336
|
+
`dark:` hangs off a class instead of `prefers-color-scheme`. */
|
|
337
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
338
|
+
|
|
339
|
+
@theme {
|
|
340
|
+
--font-sans: "Inter", system-ui, sans-serif;
|
|
341
|
+
--color-ink: #0a1334;
|
|
342
|
+
--color-ink-muted: #5a628a;
|
|
343
|
+
--color-surface: #ffffff;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Every entry under `@theme` becomes a utility, so `--color-ink` gives you
|
|
348
|
+
`text-ink` and `bg-ink`. Rebranding every app on the template is a matter of
|
|
349
|
+
editing those four values in the template repo. The generator writes one import
|
|
350
|
+
of that file into each `src/views/<widget>.tsx` entry, and since each view is its
|
|
351
|
+
own bundle, Tailwind emits only the utilities that view's source actually uses.
|
|
352
|
+
|
|
353
|
+
Two details the kit handles so an app author does not have to:
|
|
354
|
+
|
|
355
|
+
- **The `dark` class.** A view is mounted alone in its own iframe, so there is no
|
|
356
|
+
shared ancestor to hang the variant off. Each widget puts the class on its own
|
|
357
|
+
root, driven by the theme the host reports:
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
const { theme } = useLayout();
|
|
361
|
+
return <div className={theme === "dark" ? "dark" : ""}>…</div>;
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- **The stylesheet's origins.** `src/index.css` pulls Inter from Google Fonts,
|
|
365
|
+
and a host that enforces the widget CSP drops undeclared requests without
|
|
366
|
+
erroring, so the font falls back and the widget looks subtly wrong. Codegen
|
|
367
|
+
reads the origins off the stylesheet and the runtime merges them into every
|
|
368
|
+
widget's `resourceDomains`, alongside whatever the widget declares itself.
|
|
369
|
+
`fonts.googleapis.com` brings `fonts.gstatic.com` with it, since the second is
|
|
370
|
+
only reachable by following the first.
|
|
371
|
+
|
|
372
|
+
Dropping app-level CSS is what makes this hold together. Tailwind v4 rejects
|
|
373
|
+
`@apply` in any file that has not imported Tailwind itself:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
Cannot apply unknown utility class `text-ink`. Are you using CSS modules or
|
|
377
|
+
similar and missing `@reference`?
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Fixing that from an app folder means writing a `@reference` at a path into the
|
|
381
|
+
generated tree, which does not exist in the author's own repo. One place for a
|
|
382
|
+
class name beats two, so the build check names a stray `styles.css` rather than
|
|
383
|
+
letting it sit there doing nothing:
|
|
384
|
+
|
|
385
|
+
```
|
|
386
|
+
widgets/select-plan/styles.css
|
|
387
|
+
└ app CSS is not bundled — nothing imports this file
|
|
388
|
+
style with Tailwind utility classes in ui.tsx; the template's src/index.css
|
|
389
|
+
carries the @theme tokens and the `dark` variant
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Commands
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
waniwani check # validate the folder
|
|
396
|
+
waniwani dev # generate + dev server + regenerate on change
|
|
397
|
+
waniwani build # generate + production build
|
|
398
|
+
waniwani start # run the production build
|
|
399
|
+
waniwani deploy # generate + vercel deploy
|
|
400
|
+
waniwani eject [--out dir] # hand the plumbing over and step out
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Every one of them runs the same four stages before doing its own work.
|
|
404
|
+
|
|
405
|
+
```mermaid
|
|
406
|
+
flowchart LR
|
|
407
|
+
subgraph app["oney/ (what you own)"]
|
|
408
|
+
cfg["waniwani.config.ts"]
|
|
409
|
+
tools["tools/*.ts"]
|
|
410
|
+
widgets["widgets/<name>/<br/>widget.ts + ui.tsx"]
|
|
411
|
+
flows["flows/*.ts"]
|
|
412
|
+
docs["docs/*.md"]
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
subgraph tpl["WaniWani-AI/mcp-distribution-template (public, separate repo)"]
|
|
416
|
+
raw["vite.config.ts · package.json · tsconfig.json<br/>src/index.css (Tailwind) · vercel.json<br/>alpic.json · Dockerfile"]
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
subgraph cli["@waniwani/kit (what we own)"]
|
|
420
|
+
scan["scan<br/><i>convention → manifest</i>"]
|
|
421
|
+
check["check<br/><i>fail at build time</i>"]
|
|
422
|
+
gen["codegen<br/><i>emit a real project</i>"]
|
|
423
|
+
runtime["src/server.ts<br/><i>registerApp()</i>"]
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
subgraph out[".waniwani/ (build output, disposable)"]
|
|
427
|
+
server["src/server.ts · src/waniwani.ts"]
|
|
428
|
+
views["src/views/*.tsx"]
|
|
429
|
+
appsrc["src/app/ (your source, copied)"]
|
|
430
|
+
deployfiles["vercel.json · Dockerfile"]
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
app --> scan --> check --> gen --> out
|
|
434
|
+
runtime -.imported by.-> server
|
|
435
|
+
raw -.fetched at a pinned SHA, copied byte for byte.-> deployfiles
|
|
436
|
+
out --> deploy["dev · build · start<br/>vercel deploy"]
|
|
437
|
+
app -.waniwani eject.-> ejected["a plain repo<br/><i>no CLI, no @waniwani/kit</i>"]
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
1. **scan** walks the folder and turns convention into a manifest.
|
|
441
|
+
2. **check** validates structure from the filesystem, then imports every
|
|
442
|
+
server-safe module for real.
|
|
443
|
+
3. **codegen** resolves the distribution template at a pinned SHA, copies its
|
|
444
|
+
plumbing byte for byte, generates registration and view entries from the
|
|
445
|
+
manifest, and copies your source under `src/app/`.
|
|
446
|
+
4. **run** hands the result to Skybridge's `dev`, `build` or `start`, with the
|
|
447
|
+
output rewritten in WaniWani's voice.
|
|
448
|
+
|
|
449
|
+
`.waniwani/` is disposable and safe to delete. It is also an ordinary npm
|
|
450
|
+
project carrying a `vercel.json`, which is what lets `vercel deploy` inside it
|
|
451
|
+
work with no special support.
|
|
452
|
+
|
|
453
|
+
### What the build check catches
|
|
454
|
+
|
|
455
|
+
Errors that would otherwise surface as a 500 at request time, or as a widget
|
|
456
|
+
that silently never renders:
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
✗ Build check failed
|
|
460
|
+
|
|
461
|
+
widgets/broken
|
|
462
|
+
└ missing widget.ts
|
|
463
|
+
every widget folder needs a widget.ts with `export default defineWidget({ ... })`
|
|
464
|
+
|
|
465
|
+
flows/split-payment.ts
|
|
466
|
+
└ showWidget references the widget "select-plans", which does not exist
|
|
467
|
+
known widgets: broken, select-plan
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Structure comes from the filesystem. The rest comes from importing every
|
|
471
|
+
server-safe module, so a flow that fails to compile fails the build, as does a
|
|
472
|
+
missing default export, a tool with no description, or a runtime configuration
|
|
473
|
+
mistake:
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
flows/no-store.ts
|
|
477
|
+
└ failed to load
|
|
478
|
+
[waniwani] createFlow "no_store": no flow store configured. …
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
## What a request does
|
|
482
|
+
|
|
483
|
+
```mermaid
|
|
484
|
+
sequenceDiagram
|
|
485
|
+
participant Host as ChatGPT / Claude
|
|
486
|
+
participant Server as registerApp() (the shared runtime)
|
|
487
|
+
participant App as your code
|
|
488
|
+
|
|
489
|
+
Host->>Server: tools/call select-plan
|
|
490
|
+
Server->>Server: validate against the widget's `data` schema
|
|
491
|
+
Server->>App: load(input), optional
|
|
492
|
+
App-->>Server: data
|
|
493
|
+
Server->>Server: structuredContent + llmText + annotations
|
|
494
|
+
Server-->>Host: result + ui:// resource
|
|
495
|
+
Host->>Server: resources/read ui://widgets/.../select-plan.html
|
|
496
|
+
Server-->>Host: HTML pointing at the built bundle
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Every arrow that leaves `App` out is runtime code. Error envelopes, annotation
|
|
500
|
+
defaults (including the `title` Claude's Connectors Directory requires), the
|
|
501
|
+
"do not narrate the widget" instruction, the CSP block and tracking via
|
|
502
|
+
`withWaniwani` all sit in one place, for every app.
|
|
503
|
+
|
|
504
|
+
One consequence worth knowing while you write a tool: a `run` that throws
|
|
505
|
+
returns an error envelope telling the host to offer a retry rather than invent a
|
|
506
|
+
result, so exceptions are safe to let propagate.
|
|
507
|
+
|
|
508
|
+
## Eject
|
|
509
|
+
|
|
510
|
+
`waniwani eject` writes the plumbing into the repo itself and leaves. What comes
|
|
511
|
+
out is the same tree a build was producing all along, with your source moved
|
|
512
|
+
under `src/app/`:
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
oney/
|
|
516
|
+
├── src/app/{tools,widgets,flows,docs,lib}/ your code, moved
|
|
517
|
+
├── src/_runtime/ the runtime, vendored as source
|
|
518
|
+
├── src/{server,waniwani,docs}.ts entry, registration, inlined docs
|
|
519
|
+
├── src/views/<widget>.tsx view entries
|
|
520
|
+
├── vite.config.ts vercel.json alpic.json bundling and deploy
|
|
521
|
+
├── Dockerfile .dockerignore container deploy
|
|
522
|
+
└── tsconfig.json package.json
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Every `@waniwani/kit` import gets rewritten to `./_runtime/…`, and the
|
|
526
|
+
dependency drops out of `package.json`. From then on the repo runs on
|
|
527
|
+
Skybridge's own CLI (`dev`, `build`, `start`) with no WaniWani in the loop.
|
|
528
|
+
|
|
529
|
+
Ejecting in place moves the files instead of copying them, so the originals go
|
|
530
|
+
once the copy is on disk and the repo is never left holding two versions of a
|
|
531
|
+
file that can drift. `eject --out <dir>` leaves the source repo untouched.
|
|
532
|
+
Either way the CLI prints what moved. Eject refuses to overwrite existing
|
|
533
|
+
plumbing unless you pass `--force`, and it runs one way, with nothing to turn an
|
|
534
|
+
ejected repo back.
|
|
535
|
+
|
|
536
|
+
Your source has to move under `src/` for the compiled server to land where
|
|
537
|
+
Skybridge's entry wrapper looks for it. The `rootDir` constraint behind that is
|
|
538
|
+
written up in
|
|
539
|
+
[the internals](https://github.com/WaniWani-AI/kit/blob/main/INTERNALS.md#why-eject-moves-the-source).
|
|
540
|
+
|
|
541
|
+
### The trade eject makes
|
|
542
|
+
|
|
543
|
+
What an ejected repo gives up is the generator, and with it:
|
|
544
|
+
|
|
545
|
+
- **the build check**, so `showWidget("typo")` becomes a runtime failure again
|
|
546
|
+
- **name-from-filesystem**, so adding a widget means editing `src/waniwani.ts`
|
|
547
|
+
and adding an entry under `src/views/`
|
|
548
|
+
- **docs auto-scan**, since `src/docs.ts` is a snapshot and new `docs/*.md` need
|
|
549
|
+
hand-wiring
|
|
550
|
+
- **runtime fixes**, since `src/_runtime/` is a fork from the moment it lands
|
|
551
|
+
|
|
552
|
+
## Status
|
|
553
|
+
|
|
554
|
+
`@waniwani/kit@0.0.1`, unpublished. What bites an app author today:
|
|
555
|
+
|
|
556
|
+
- **`@waniwani/cli` owns the `waniwani` bin on npm**, so installing both
|
|
557
|
+
collides.
|
|
558
|
+
- **No `waniwani init`**, so a new app folder starts by hand.
|
|
559
|
+
- **`waniwani deploy` is wired but unrun** against a real Vercel account.
|
|
560
|
+
- **`useWidget` does not track yet.** Emitting `widget_render` and click events
|
|
561
|
+
through `useWaniwani` automatically is the next step.
|
|
562
|
+
- **Docs search is a term-match** rather than the hosted KB. Swapping it for
|
|
563
|
+
`wani.kb.search` when `WANIWANI_API_KEY` is set is a runtime change only.
|
|
564
|
+
|
|
565
|
+
Template pinning, the CI contract, publishing requirements and the rest of the
|
|
566
|
+
gap list are in [INTERNALS.md](https://github.com/WaniWani-AI/kit/blob/main/INTERNALS.md).
|