@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 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/&lt;name&gt;/<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).