@waniwani/kit 0.1.5 → 0.1.6
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/cli/codegen.mjs +152 -7
- package/cli/init.mjs +15 -22
- package/cli/peers.mjs +170 -0
- package/cli/template.mjs +45 -7
- package/cli/validate.mjs +65 -1
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -7
- package/src/index.ts +105 -0
package/cli/codegen.mjs
CHANGED
|
@@ -41,12 +41,38 @@ import {
|
|
|
41
41
|
} from "node:fs";
|
|
42
42
|
import { basename, dirname, join, relative } from "node:path";
|
|
43
43
|
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { compare, floorOf, installable } from "./peers.mjs";
|
|
44
45
|
|
|
45
46
|
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
46
47
|
const RUNTIME_SRC = join(PACKAGE_ROOT, "src");
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/** This package's own manifest, which is where every version below comes from. */
|
|
49
|
+
const MANIFEST = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
50
|
+
const PACKAGE_VERSION = MANIFEST.version;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A version this package declares, read back out so it is stated once.
|
|
54
|
+
*
|
|
55
|
+
* Every version the generator forces on an app is a version the generator was
|
|
56
|
+
* built and verified against, which makes this manifest the only honest source
|
|
57
|
+
* for it. Writing the same range a second time as a literal down in `PINS` gave
|
|
58
|
+
* one fact two homes, and a bump could update either one alone: the manifest
|
|
59
|
+
* carried `skybridge@^1.3.5` while the pin forced `1.4.0`, and they agreed only
|
|
60
|
+
* because that is what the lockfile happened to resolve.
|
|
61
|
+
*
|
|
62
|
+
* Missing throws rather than defaults. `undefined` here would land in a
|
|
63
|
+
* generated `package.json` as a dependency with no version and fail at install
|
|
64
|
+
* time in someone else's project, a long way from the rename that caused it.
|
|
65
|
+
*/
|
|
66
|
+
function declared(name, field = "dependencies") {
|
|
67
|
+
const version = MANIFEST[field]?.[name];
|
|
68
|
+
if (!version) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`@waniwani/kit declares no ${field}.${name}, and the generator pins apps to it — ` +
|
|
71
|
+
"add it back to packages/kit/package.json or drop it from PINS",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return version;
|
|
75
|
+
}
|
|
50
76
|
|
|
51
77
|
/**
|
|
52
78
|
* The template comes across whole, minus an explicit list.
|
|
@@ -160,17 +186,51 @@ const SEAM = { file: "src/server.ts", symbol: "registerApp" };
|
|
|
160
186
|
*
|
|
161
187
|
* Each entry carries its reason, and the CLI reports what it changed.
|
|
162
188
|
*/
|
|
163
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* Forced to what this package declares: the generated code is built against
|
|
191
|
+
* these, and `declared()` is what keeps the two statements of that one fact
|
|
192
|
+
* from drifting apart.
|
|
193
|
+
*/
|
|
164
194
|
const PINS = {
|
|
165
195
|
dependencies: {
|
|
166
196
|
skybridge: {
|
|
167
|
-
version: "
|
|
197
|
+
version: declared("skybridge"),
|
|
168
198
|
why: "the template's range floats within 1.x; the runtime is built and verified against this one",
|
|
169
199
|
},
|
|
170
|
-
"@waniwani/sdk": { version: "^0.19.5", why: "flows and tracking need the current SDK" },
|
|
171
200
|
},
|
|
172
201
|
devDependencies: {
|
|
173
|
-
"@skybridge/devtools": {
|
|
202
|
+
"@skybridge/devtools": {
|
|
203
|
+
version: declared("@skybridge/devtools", "devDependencies"),
|
|
204
|
+
why: "must match the framework",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Peer floors, checked against what the merge produced rather than forced over
|
|
211
|
+
* it.
|
|
212
|
+
*
|
|
213
|
+
* `@waniwani/sdk` was a `PINS` entry, which made this generator the authority
|
|
214
|
+
* on an app's SDK version. It was the wrong authority twice over: nothing under
|
|
215
|
+
* `src/` imports the SDK, so the version was never verified against anything
|
|
216
|
+
* here, and an app that disagreed kept its own choice and ended up with two
|
|
217
|
+
* copies in the tree — `createFlow()` compiling against the app's while this
|
|
218
|
+
* runtime registered the result against the kit's. It is a required peer now
|
|
219
|
+
* (see the manifest's `//sdk` note), so the app or the template names the
|
|
220
|
+
* version and this states the floor underneath both.
|
|
221
|
+
*
|
|
222
|
+
* Absent is filled in, and below the floor is reported. Nothing is forced
|
|
223
|
+
* upward: an app on a newer SDK than the template asked for is an app that
|
|
224
|
+
* upgraded, and overwriting that is how the second copy got there in the first
|
|
225
|
+
* place. The floor an app can act on is checked earlier and without a template
|
|
226
|
+
* download, in `checkPeers` in `./validate.mjs`; this covers the version a
|
|
227
|
+
* template contributed, which that check cannot see.
|
|
228
|
+
*/
|
|
229
|
+
const FLOORS = {
|
|
230
|
+
dependencies: {
|
|
231
|
+
"@waniwani/sdk": {
|
|
232
|
+
why: "below this, npm will not install the SDK next to skybridge 1.4.0 — see the manifest's //sdk note",
|
|
233
|
+
},
|
|
174
234
|
},
|
|
175
235
|
};
|
|
176
236
|
|
|
@@ -186,6 +246,36 @@ const ENSURED = {
|
|
|
186
246
|
},
|
|
187
247
|
};
|
|
188
248
|
|
|
249
|
+
/**
|
|
250
|
+
* What the vendored runtime needs declared, for the eject layout only.
|
|
251
|
+
*
|
|
252
|
+
* A build reaches the runtime through `@waniwani/kit`, so express, cors and
|
|
253
|
+
* their types arrive as that package's own dependencies — which is why it
|
|
254
|
+
* declares them (see its `//dependencies` and `//express` notes). Ejecting drops
|
|
255
|
+
* the package and copies `src/` in as source, and the imports come with it: the
|
|
256
|
+
* vendored tree imports `express` and `cors` by name, and `tsc` needs their
|
|
257
|
+
* types. Nothing was putting either back, so an ejected project installed and
|
|
258
|
+
* then failed to compile on ten TS7006/TS7016 errors, with express and cors
|
|
259
|
+
* present in `node_modules` only as a transitive hoist out of the framework.
|
|
260
|
+
*
|
|
261
|
+
* Only the two the runtime imports and the app does not already get: `skybridge`
|
|
262
|
+
* and `zod` are the other bare specifiers under `src/`, and both are declared
|
|
263
|
+
* for every layout already.
|
|
264
|
+
*/
|
|
265
|
+
const VENDORED = {
|
|
266
|
+
dependencies: {
|
|
267
|
+
express: { version: declared("express"), why: "the vendored runtime imports express" },
|
|
268
|
+
cors: { version: declared("cors"), why: "the vendored runtime mounts CORS per endpoint" },
|
|
269
|
+
},
|
|
270
|
+
devDependencies: {
|
|
271
|
+
"@types/express": {
|
|
272
|
+
version: declared("@types/express"),
|
|
273
|
+
why: "the vendored runtime is typed against express",
|
|
274
|
+
},
|
|
275
|
+
"@types/cors": { version: declared("@types/cors"), why: "same, for cors" },
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
189
279
|
/**
|
|
190
280
|
* Scripts the generated layout needs, added only when the template has no
|
|
191
281
|
* script by that name. The template's own scripts are left untouched.
|
|
@@ -625,6 +715,13 @@ export const app = {
|
|
|
625
715
|
title: config.title,
|
|
626
716
|
version: config.version ?? ${JSON.stringify(version ?? "0.0.0")},
|
|
627
717
|
instructions: config.instructions,
|
|
718
|
+
// Forwarded whole, for the template to read if it has anything to read them
|
|
719
|
+
// with: \`search\` tunes the search tool a template ships, \`tracking\` reaches
|
|
720
|
+
// the SDK's withWaniwani(). A template that uses neither ignores both, so
|
|
721
|
+
// emitting them unconditionally keeps one generator working across templates
|
|
722
|
+
// that read them and templates that do not.
|
|
723
|
+
search: config.search,
|
|
724
|
+
tracking: config.tracking,
|
|
628
725
|
};
|
|
629
726
|
|
|
630
727
|
export async function registerApp(server: McpServer): Promise<void> {
|
|
@@ -779,6 +876,32 @@ function generatePackageJson(app, appPackageJson, template, layout) {
|
|
|
779
876
|
overrides.push({ name, to: version, why });
|
|
780
877
|
}
|
|
781
878
|
|
|
879
|
+
// Same rule as ENSURED — an app or template declaring its own keeps it —
|
|
880
|
+
// but only where the runtime arrives as source rather than as a package.
|
|
881
|
+
if (layout.vendored) {
|
|
882
|
+
for (const [name, { version, why }] of Object.entries(VENDORED[kind] ?? {})) {
|
|
883
|
+
if (merged[name]) continue;
|
|
884
|
+
merged[name] = version;
|
|
885
|
+
overrides.push({ name, to: version, why });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
for (const [name, { why }] of Object.entries(FLOORS[kind] ?? {})) {
|
|
890
|
+
if (!merged[name]) {
|
|
891
|
+
merged[name] = installable(name);
|
|
892
|
+
overrides.push({ name, to: merged[name], why });
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
if (compare(merged[name], name) === "below") {
|
|
896
|
+
overrides.push({
|
|
897
|
+
name,
|
|
898
|
+
to: merged[name],
|
|
899
|
+
why: `below ${floorOf(name)}, which this kit needs: ${why}`,
|
|
900
|
+
conflict: true,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
782
905
|
return merged;
|
|
783
906
|
};
|
|
784
907
|
|
|
@@ -1082,6 +1205,28 @@ export function generate(app, { template, layout: layoutName = "build", outDir }
|
|
|
1082
1205
|
sha: template.sha,
|
|
1083
1206
|
local: template.local,
|
|
1084
1207
|
manifest: manifest ? MANIFEST_FILE : undefined,
|
|
1208
|
+
// Which generator wrote this tree, and the versions it was built
|
|
1209
|
+
// against. A deployed app misbehaving is the case this serves:
|
|
1210
|
+
// the tree itself then answers which template commit and which
|
|
1211
|
+
// SDK it was built from, without a guess from the app's lockfile
|
|
1212
|
+
// or from whatever the CLI happens to pin today.
|
|
1213
|
+
//
|
|
1214
|
+
// Two fields because there are two kinds of answer. `pins` is
|
|
1215
|
+
// what this generator forced, and `peers` is what the app or the
|
|
1216
|
+
// template chose while this generator only stated a floor — the
|
|
1217
|
+
// SDK moved from the first to the second when it became a peer,
|
|
1218
|
+
// and it is the one most worth reading back.
|
|
1219
|
+
kit: PACKAGE_VERSION,
|
|
1220
|
+
pins: Object.fromEntries(
|
|
1221
|
+
Object.values(PINS).flatMap((group) =>
|
|
1222
|
+
Object.entries(group).map(([name, pin]) => [name, pin.version]),
|
|
1223
|
+
),
|
|
1224
|
+
),
|
|
1225
|
+
peers: Object.fromEntries(
|
|
1226
|
+
Object.entries(FLOORS).flatMap(([kind, group]) =>
|
|
1227
|
+
Object.keys(group).map((name) => [name, packageJson[kind]?.[name]]),
|
|
1228
|
+
),
|
|
1229
|
+
),
|
|
1085
1230
|
// What survived to the end, copied and generated alike. The
|
|
1086
1231
|
// copy is the raw list minus whatever a generated file replaced,
|
|
1087
1232
|
// and the generated half is here so that a build which stops
|
package/cli/init.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { basename, dirname, join, relative } from "node:path";
|
|
|
33
33
|
import { createInterface } from "node:readline/promises";
|
|
34
34
|
import { fileURLToPath } from "node:url";
|
|
35
35
|
import { bold, dim, green, red, yellow } from "./log.mjs";
|
|
36
|
+
import { installable } from "./peers.mjs";
|
|
36
37
|
|
|
37
38
|
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
39
|
const MANIFEST = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
@@ -65,36 +66,28 @@ function cleanTitle(input) {
|
|
|
65
66
|
.trim();
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
/**
|
|
69
|
-
* A peer range is a floor, `>=19`, and a floor in an app's dependencies installs
|
|
70
|
-
* the next major on the day it lands. Cap it. Anything already ranged, `^4`,
|
|
71
|
-
* passes through as it is.
|
|
72
|
-
*/
|
|
73
|
-
function installable(name, range) {
|
|
74
|
-
if (!range) {
|
|
75
|
-
throw new Error(`@waniwani/kit declares no peer range for ${name}: this package's manifest moved`);
|
|
76
|
-
}
|
|
77
|
-
const floor = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range.trim());
|
|
78
|
-
return floor ? `^${floor[1]}.${floor[2] ?? 0}.${floor[3] ?? 0}` : range;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
69
|
/**
|
|
82
70
|
* What a new app depends on.
|
|
83
71
|
*
|
|
84
72
|
* Every version is read off this package's own manifest: `@waniwani/kit` at the
|
|
85
|
-
* version of the CLI doing the scaffolding,
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* file in the folder that can be wrong the day it is
|
|
73
|
+
* version of the CLI doing the scaffolding, and the four peers at the floors
|
|
74
|
+
* this package declares, capped by `installable` so a floor does not install
|
|
75
|
+
* the next major on the day it lands. A scaffold that wrote its own numbers
|
|
76
|
+
* here would be the one file in the folder that can be wrong the day it is
|
|
77
|
+
* created.
|
|
78
|
+
*
|
|
79
|
+
* `@waniwani/sdk` is written out even though a required peer is auto-installed
|
|
80
|
+
* without it, because the app imports it directly — `flows/*.ts` calls
|
|
81
|
+
* `createFlow` — and a package you import belongs in your own manifest rather
|
|
82
|
+
* than arriving because something else asked for it.
|
|
89
83
|
*/
|
|
90
84
|
function dependencies() {
|
|
91
|
-
const peers = MANIFEST.peerDependencies ?? {};
|
|
92
85
|
return {
|
|
93
86
|
"@waniwani/kit": `^${MANIFEST.version}`,
|
|
94
|
-
"@waniwani/sdk":
|
|
95
|
-
react: installable("react"
|
|
96
|
-
"react-dom": installable("react-dom"
|
|
97
|
-
zod: installable("zod"
|
|
87
|
+
"@waniwani/sdk": installable("@waniwani/sdk"),
|
|
88
|
+
react: installable("react"),
|
|
89
|
+
"react-dom": installable("react-dom"),
|
|
90
|
+
zod: installable("zod"),
|
|
98
91
|
};
|
|
99
92
|
}
|
|
100
93
|
|
package/cli/peers.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The peer ranges this package declares, and what to do with them.
|
|
3
|
+
*
|
|
4
|
+
* `@waniwani/sdk`, react, react-dom and zod reach an app as peers rather than
|
|
5
|
+
* as this package's own dependencies, so the app holds one copy and this
|
|
6
|
+
* package states only the floor underneath it. Three callers need that floor
|
|
7
|
+
* and each needs it differently: `init.mjs` writes an installable range into a
|
|
8
|
+
* new app, `validate.mjs` checks the range an existing app already wrote, and
|
|
9
|
+
* `codegen.mjs` fills one in when neither the app nor the template declared
|
|
10
|
+
* one. One module so the parsing exists once.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately not a semver dependency. What appears in a real app's manifest
|
|
13
|
+
* is an exact version, a caret, a tilde or a `>=` floor, and comparing those
|
|
14
|
+
* against a floor is the whole job. Anything this cannot parse is reported as
|
|
15
|
+
* unknown rather than guessed at — see `compare`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const MANIFEST = JSON.parse(
|
|
23
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A peer range this package declares, read back out so it is stated once.
|
|
28
|
+
*
|
|
29
|
+
* Missing throws. A floor that silently defaulted would let every check below
|
|
30
|
+
* pass vacuously, which is worse than the rename that removed it.
|
|
31
|
+
*/
|
|
32
|
+
export function peerRange(name) {
|
|
33
|
+
const range = MANIFEST.peerDependencies?.[name];
|
|
34
|
+
if (!range) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`@waniwani/kit declares no peerDependencies.${name}, and its own tooling reads that floor — ` +
|
|
37
|
+
"add it back to packages/kit/package.json, or drop the entry that reads it",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return range;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A peer range turned into something an app can depend on.
|
|
45
|
+
*
|
|
46
|
+
* A peer range is a floor, `>=19`, and a floor in an app's dependencies
|
|
47
|
+
* installs the next major on the day it lands. Cap it. Anything already ranged,
|
|
48
|
+
* `^4`, passes through as it is.
|
|
49
|
+
*
|
|
50
|
+
* The prerelease tail is part of the pattern because a floor can carry one, and
|
|
51
|
+
* `>=0.19.9-beta.0` falling through uncapped would put the very floor this
|
|
52
|
+
* exists to cap into a new app's manifest. `^0.19.9-beta.0` keeps the
|
|
53
|
+
* prerelease reachable and still stops at `0.20.0`.
|
|
54
|
+
*/
|
|
55
|
+
export function installable(name) {
|
|
56
|
+
const range = peerRange(name);
|
|
57
|
+
const floor = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(-[0-9A-Za-z.-]+)?$/.exec(range.trim());
|
|
58
|
+
return floor ? `^${floor[1]}.${floor[2] ?? 0}.${floor[3] ?? 0}${floor[4] ?? ""}` : range;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `1.2.3-beta.0` → `{ parts: [1,2,3], prerelease: "beta.0" }` */
|
|
62
|
+
function parseVersion(input) {
|
|
63
|
+
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/.exec(input.trim());
|
|
64
|
+
if (!match) return null;
|
|
65
|
+
return {
|
|
66
|
+
parts: [Number(match[1]), Number(match[2] ?? 0), Number(match[3] ?? 0)],
|
|
67
|
+
prerelease: match[4],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Ordering on major.minor.patch, with a prerelease sorting below its release. */
|
|
72
|
+
function order(a, b) {
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
if (a.parts[i] !== b.parts[i]) return a.parts[i] < b.parts[i] ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
if (a.prerelease && !b.prerelease) return -1;
|
|
77
|
+
if (!a.prerelease && b.prerelease) return 1;
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The window a dependency spec opens, as `{ low, high }`, where `high` is
|
|
83
|
+
* exclusive and `null` means unbounded.
|
|
84
|
+
*
|
|
85
|
+
* Caret follows semver's 0.x rule, which is the one that matters for the SDK:
|
|
86
|
+
* `^0.19.5` stops at `0.20.0`, so an SDK minor is a breaking change.
|
|
87
|
+
*/
|
|
88
|
+
function window(spec) {
|
|
89
|
+
const trimmed = spec.trim();
|
|
90
|
+
if (trimmed === "*" || trimmed === "" || trimmed === "latest") {
|
|
91
|
+
return { low: parseVersion("0.0.0"), high: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ranged = /^(\^|~|>=)\s*(.+)$/.exec(trimmed);
|
|
95
|
+
if (!ranged) {
|
|
96
|
+
const exact = parseVersion(trimmed.replace(/^=\s*/, ""));
|
|
97
|
+
return exact ? { low: exact, high: exact, exact: true } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const low = parseVersion(ranged[2]);
|
|
101
|
+
if (!low) return null;
|
|
102
|
+
if (ranged[1] === ">=") return { low, high: null };
|
|
103
|
+
|
|
104
|
+
const [major, minor] = low.parts;
|
|
105
|
+
// `~1.2.3` caps at the next minor. `^1.2.3` caps at the next major, except
|
|
106
|
+
// under 0.x where the minor is the compatibility boundary.
|
|
107
|
+
const high =
|
|
108
|
+
ranged[1] === "~" || major === 0
|
|
109
|
+
? { parts: [major, minor + 1, 0], prerelease: undefined }
|
|
110
|
+
: { parts: [major + 1, 0, 0], prerelease: undefined };
|
|
111
|
+
return { low, high };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* How a dependency spec sits against a peer floor.
|
|
116
|
+
*
|
|
117
|
+
* - `"ok"` — every version the spec allows is at or above the floor.
|
|
118
|
+
* - `"reachable"` — the spec allows the floor, and also allows something below
|
|
119
|
+
* it. A fresh install lands above the floor and a lockfile written earlier
|
|
120
|
+
* can hold the tree below it, so this is a warning rather than an error.
|
|
121
|
+
* - `"below"` — no version the spec allows reaches the floor. This one cannot
|
|
122
|
+
* resolve to a working tree.
|
|
123
|
+
* - `"prerelease"` — a prerelease, at or above the floor by number, which npm's
|
|
124
|
+
* semver rules still exclude from a range carrying no prerelease of its own.
|
|
125
|
+
* Both npm and bun warn on it, so reporting it as `ok` would have this check
|
|
126
|
+
* disagreeing with the tool that actually resolves the tree.
|
|
127
|
+
* - `"unknown"` — an expression this module does not parse (a union, a git
|
|
128
|
+
* URL, `workspace:*`). Reported as-is rather than assumed to be either.
|
|
129
|
+
*/
|
|
130
|
+
export function compare(spec, name) {
|
|
131
|
+
const range = peerRange(name);
|
|
132
|
+
const floorMatch = /^>=\s*(.+)$/.exec(range.trim());
|
|
133
|
+
const floor = parseVersion(floorMatch ? floorMatch[1] : range.replace(/^[\^~=]\s*/, ""));
|
|
134
|
+
const allowed = spec == null ? null : window(spec);
|
|
135
|
+
if (!floor || !allowed) return "unknown";
|
|
136
|
+
|
|
137
|
+
// A prerelease is opt-in under semver: `0.19.9-beta.0` does not satisfy
|
|
138
|
+
// `>=0.19.8`, because the range names no prerelease at that version. The
|
|
139
|
+
// floor carrying one of its own is someone pinning a prerelease on purpose,
|
|
140
|
+
// and then the plain comparison is what they asked for.
|
|
141
|
+
if (allowed.low.prerelease && !floor.prerelease) return "prerelease";
|
|
142
|
+
if (order(allowed.low, floor) >= 0) return "ok";
|
|
143
|
+
if (allowed.high === null || order(allowed.high, floor) > 0) return "reachable";
|
|
144
|
+
return "below";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The floor itself, for a message that has to name it. */
|
|
148
|
+
export function floorOf(name) {
|
|
149
|
+
return peerRange(name);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The lowest version a spec allows, normalised, or null if unparseable.
|
|
154
|
+
*
|
|
155
|
+
* `scripts/bump-deps.mjs` asks the question this answers, and it is the mirror
|
|
156
|
+
* of `compare`: that one checks a spec against this package's floor, while a
|
|
157
|
+
* floor bump needs to know whether the *template's* floor has risen above it.
|
|
158
|
+
*/
|
|
159
|
+
export function floorVersion(spec) {
|
|
160
|
+
const allowed = spec == null ? null : window(spec);
|
|
161
|
+
return allowed?.low ? allowed.low.parts.join(".") : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Ordering on two version strings, for a caller with no window to compare. */
|
|
165
|
+
export function compareVersions(a, b) {
|
|
166
|
+
const left = parseVersion(a);
|
|
167
|
+
const right = parseVersion(b);
|
|
168
|
+
if (!left || !right) return null;
|
|
169
|
+
return order(left, right);
|
|
170
|
+
}
|
package/cli/template.mjs
CHANGED
|
@@ -19,13 +19,35 @@ import { homedir, tmpdir } from "node:os";
|
|
|
19
19
|
import { join, resolve } from "node:path";
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* A commit, not a branch.
|
|
23
|
+
*
|
|
24
|
+
* A published version of this package is frozen, and what it generates has to
|
|
25
|
+
* be frozen with it. While the default was `beta`, the ref was re-resolved on
|
|
26
|
+
* the customer's machine at every command, so a push to that branch changed the
|
|
27
|
+
* output of every installed copy, and the assertions that catch a layout move
|
|
28
|
+
* (`REQUIRED` and `assertSeam` in `./codegen.mjs`) fired in a customer's
|
|
29
|
+
* terminal. Pinning a commit moves that failure into this repo's CI, where
|
|
30
|
+
* `scripts/template-contract.mjs` builds a real app against the pin before a
|
|
31
|
+
* release goes out.
|
|
32
|
+
*
|
|
33
|
+
* Bumping it is a one-line diff, and `scripts/bump-deps.mjs` proposes it. The
|
|
34
|
+
* commit is on the template's `beta` branch: the generator is written against
|
|
35
|
+
* that branch's layout (`vite.config.ts`, `src/server.ts`, `src/views/`), and
|
|
36
|
+
* `main` is still the older `server/` + `web/` + `api/` split, which it cannot
|
|
37
|
+
* absorb. An annotated tag can replace the SHA here whenever the template grows
|
|
38
|
+
* one, with no change to the resolver.
|
|
39
|
+
*
|
|
40
|
+
* This commit is `beta`'s head, and it reads `search` and `tracking` off
|
|
41
|
+
* `src/waniwani.ts` — the two fields `generateServerApp` emits from the app's
|
|
42
|
+
* `defineApp({ ... })`. That pairing is the reason to bump the two together:
|
|
43
|
+
* moving the pin here without the generator emitting those fields compiles to
|
|
44
|
+
* TS2339, and the contract is what catches it.
|
|
45
|
+
*
|
|
46
|
+
* Working on the template itself does not need a release: pass `--template` or
|
|
47
|
+
* set `WANIWANI_TEMPLATE` to a branch ref or a local checkout.
|
|
27
48
|
*/
|
|
28
|
-
export const DEFAULT_TEMPLATE =
|
|
49
|
+
export const DEFAULT_TEMPLATE =
|
|
50
|
+
"github:WaniWani-AI/mcp-distribution-template#c0d00e72a3733a5f42389731fe6bbaf7e0e07863";
|
|
29
51
|
|
|
30
52
|
const CACHE_ROOT = join(homedir(), ".cache", "waniwani", "templates");
|
|
31
53
|
|
|
@@ -36,11 +58,21 @@ function parseGithub(source) {
|
|
|
36
58
|
return { owner: match[1], repo: match[2], ref: match[3] ?? "main" };
|
|
37
59
|
}
|
|
38
60
|
|
|
61
|
+
/** A full commit SHA, which is already the thing a ref has to be resolved to. */
|
|
62
|
+
function isSha(ref) {
|
|
63
|
+
return /^[0-9a-f]{40}$/i.test(ref);
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
/**
|
|
40
67
|
* Resolve a ref to a commit SHA, so a cache entry is content-addressed and two
|
|
41
68
|
* builds of the same ref cannot silently differ.
|
|
42
69
|
*/
|
|
43
70
|
async function resolveSha({ owner, repo, ref }) {
|
|
71
|
+
// The pinned default is a commit, and asking the API to resolve a commit to
|
|
72
|
+
// itself is a round trip that can rate-limit, fail, or go down. A cached
|
|
73
|
+
// pin then needs no network at all, which is the point of pinning.
|
|
74
|
+
if (isSha(ref)) return ref.toLowerCase();
|
|
75
|
+
|
|
44
76
|
let response;
|
|
45
77
|
try {
|
|
46
78
|
response = await fetch(`https://api.github.com/repos/${owner}/${repo}/commits/${ref}`, {
|
|
@@ -148,5 +180,11 @@ export async function resolveTemplate(source = DEFAULT_TEMPLATE) {
|
|
|
148
180
|
export function describeTemplate(template) {
|
|
149
181
|
if (template.local) return `${template.dir} (local)`;
|
|
150
182
|
const state = template.offline ? "offline, cached" : template.cached ? "cached" : "downloaded";
|
|
151
|
-
|
|
183
|
+
// A pinned source already carries the commit, so printing the source verbatim
|
|
184
|
+
// would repeat all 40 characters of it next to the short form. Collapse to
|
|
185
|
+
// the repo, and say that the commit came from a pin rather than a branch.
|
|
186
|
+
const github = parseGithub(template.source);
|
|
187
|
+
const pinned = github && isSha(github.ref);
|
|
188
|
+
const origin = pinned ? `github:${github.owner}/${github.repo}` : template.source;
|
|
189
|
+
return `${origin} @ ${template.sha?.slice(0, 7)} (${pinned ? `pinned, ${state}` : state})`;
|
|
152
190
|
}
|
package/cli/validate.mjs
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
|
-
import { relative } from "node:path";
|
|
10
|
+
import { join, relative } from "node:path";
|
|
11
11
|
import { loadAppEnv } from "./env.mjs";
|
|
12
|
+
import { compare, floorOf, installable } from "./peers.mjs";
|
|
12
13
|
|
|
13
14
|
const NAME_RE = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
|
|
14
15
|
|
|
@@ -311,6 +312,68 @@ async function load(file, where, report) {
|
|
|
311
312
|
}
|
|
312
313
|
}
|
|
313
314
|
|
|
315
|
+
/**
|
|
316
|
+
* The SDK version an app asked for, against the floor this package declares.
|
|
317
|
+
*
|
|
318
|
+
* `@waniwani/sdk` is a required peer (see the manifest's `//sdk` note), so the
|
|
319
|
+
* app owns the version and this is the one place that says what the runtime and
|
|
320
|
+
* the pinned template need underneath it. It reads two manifests off disk and
|
|
321
|
+
* fetches nothing, which is why it runs in `check` rather than waiting for the
|
|
322
|
+
* dependency merge in `codegen.mjs` — a version that cannot work should not
|
|
323
|
+
* need a template download to be told so.
|
|
324
|
+
*
|
|
325
|
+
* Undeclared is not an error. npm and bun both install a required peer, and
|
|
326
|
+
* `codegen.mjs` writes one into the generated project, so an app that never
|
|
327
|
+
* mentions the SDK still gets a working copy.
|
|
328
|
+
*/
|
|
329
|
+
function checkPeers(app, report) {
|
|
330
|
+
let manifest;
|
|
331
|
+
try {
|
|
332
|
+
manifest = JSON.parse(readFileSync(join(app.root, "package.json"), "utf-8"));
|
|
333
|
+
} catch {
|
|
334
|
+
// No manifest, or an unparseable one. Both are `init`'s business, and
|
|
335
|
+
// neither is improved by a second error about a dependency inside it.
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const name = "@waniwani/sdk";
|
|
340
|
+
const spec = manifest.dependencies?.[name] ?? manifest.devDependencies?.[name];
|
|
341
|
+
if (spec == null) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const floor = floorOf(name);
|
|
346
|
+
const suggestion = installable(name);
|
|
347
|
+
switch (compare(spec, name)) {
|
|
348
|
+
case "below":
|
|
349
|
+
report.error(
|
|
350
|
+
"package.json",
|
|
351
|
+
`${name} ${spec} cannot reach ${floor}, which this kit needs`,
|
|
352
|
+
`no version that range allows will work: below the floor the SDK declares a @modelcontextprotocol/ext-apps peer that conflicts with the framework's, and npm refuses the tree. Set ${name} to ${suggestion}.`,
|
|
353
|
+
);
|
|
354
|
+
break;
|
|
355
|
+
case "prerelease":
|
|
356
|
+
report.warn(
|
|
357
|
+
"package.json",
|
|
358
|
+
`${name} ${spec} is a prerelease, and ${floor} does not accept one`,
|
|
359
|
+
`npm and bun both exclude a prerelease from a range that names none, so the install warns and the tree may not be what this spec says. Deliberate is fine; ${suggestion} is the released floor.`,
|
|
360
|
+
);
|
|
361
|
+
break;
|
|
362
|
+
case "reachable":
|
|
363
|
+
report.warn(
|
|
364
|
+
"package.json",
|
|
365
|
+
`${name} ${spec} also allows versions below ${floor}`,
|
|
366
|
+
`a fresh install resolves above the floor, and a lockfile written before it moved can hold this tree below it. ${suggestion} says the floor out loud.`,
|
|
367
|
+
);
|
|
368
|
+
break;
|
|
369
|
+
default:
|
|
370
|
+
// "ok", and "unknown" for an expression this cannot parse — a
|
|
371
|
+
// workspace protocol or a git URL, where the version is not in the
|
|
372
|
+
// string and guessing at it would be a false alarm either way.
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
314
377
|
export async function validateApp(app) {
|
|
315
378
|
// The check imports every server-safe module for real, and a module that
|
|
316
379
|
// builds a client at import time reads the environment while doing it. An app
|
|
@@ -319,6 +382,7 @@ export async function validateApp(app) {
|
|
|
319
382
|
loadAppEnv(app.root);
|
|
320
383
|
const report = new Report(app.root);
|
|
321
384
|
checkStructure(app, report);
|
|
385
|
+
checkPeers(app, report);
|
|
322
386
|
// Importing broken modules produces noise on top of structural errors.
|
|
323
387
|
if (report.ok) {
|
|
324
388
|
await checkModules(app, report);
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,100 @@ export type ToolHints = {
|
|
|
35
35
|
/** Calling twice with the same input has the same effect as calling once. */
|
|
36
36
|
idempotent?: boolean;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* What an app may set on the template's `search` tool.
|
|
40
|
+
*
|
|
41
|
+
* Declared here and again in the template's `src/search/index.ts`, on purpose:
|
|
42
|
+
* the template does not depend on this package, so there is no type to share.
|
|
43
|
+
* The two meet at the generated `waniwani.ts` and nowhere else, which makes this
|
|
44
|
+
* a mirror that has to be kept in step by hand. A field added on one side and
|
|
45
|
+
* not the other is silently dropped rather than reported.
|
|
46
|
+
*/
|
|
47
|
+
export type SearchOptions = {
|
|
48
|
+
/**
|
|
49
|
+
* Whether the template registers the tool at all.
|
|
50
|
+
*
|
|
51
|
+
* `false` is the only way an app can decline it, because it cannot unregister
|
|
52
|
+
* what the template has already registered. Worth using: a deployment with no
|
|
53
|
+
* corpus behind it, or one holding another market's documents, is better off
|
|
54
|
+
* without the tool than with one answering confidently out of the wrong file.
|
|
55
|
+
*/
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
/** Passages to ask for, 1-20. Unset leaves the SDK's default of 5. */
|
|
58
|
+
topK?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Similarity floor, 0-1, under which a passage is dropped rather than ranked
|
|
61
|
+
* last. Unset leaves the SDK's default of 0.3.
|
|
62
|
+
*/
|
|
63
|
+
minScore?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Exact-match filter on chunk metadata: a passage must carry all of these
|
|
66
|
+
* pairs to come back. With the corpus tagged at ingest time, this is a gate in
|
|
67
|
+
* code rather than a line of prompt.
|
|
68
|
+
*/
|
|
69
|
+
metadata?: Record<string, string>;
|
|
70
|
+
/** Give up on a slow search and answer as though nothing matched. */
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
/** Name the source document on each passage. */
|
|
73
|
+
includeSources?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Framing prepended to the answer text. Retrieved passages are third-party
|
|
76
|
+
* text on its way into a prompt; this is where an app says they are reference
|
|
77
|
+
* material rather than instructions.
|
|
78
|
+
*/
|
|
79
|
+
preamble?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Status text the host shows while the call is in flight, and once it has
|
|
82
|
+
* returned. Configurable because the defaults are English and this string is
|
|
83
|
+
* one of the few a user actually reads.
|
|
84
|
+
*/
|
|
85
|
+
invoking?: string;
|
|
86
|
+
invoked?: string;
|
|
87
|
+
};
|
|
88
|
+
/** The event categories the tracking backend recognises. */
|
|
89
|
+
export type ToolType = "pricing" | "product_info" | "availability" | "support" | "other";
|
|
90
|
+
/**
|
|
91
|
+
* Tracking options, forwarded whole to the SDK's `withWaniwani()`.
|
|
92
|
+
*
|
|
93
|
+
* A mirror of that function's options for the same reason as `SearchOptions`
|
|
94
|
+
* above, narrowed to what an app declares rather than constructs: the SDK also
|
|
95
|
+
* accepts a `client` instance and an `onError` callback, and neither belongs in a
|
|
96
|
+
* config file.
|
|
97
|
+
*
|
|
98
|
+
* `flushAfterToolCall` is the one that matters on serverless. An invocation
|
|
99
|
+
* frozen between tool calls takes any unsent event batch with it, and this is
|
|
100
|
+
* the only way an app can ask for the flush.
|
|
101
|
+
*/
|
|
102
|
+
export type TrackingOptions = {
|
|
103
|
+
/** One category for every tool, or a function mapping tool name to category. */
|
|
104
|
+
toolType?: ToolType | ((toolName: string) => ToolType | undefined);
|
|
105
|
+
/** Merged into every tracked event. */
|
|
106
|
+
metadata?: Record<string, unknown>;
|
|
107
|
+
/** Flush the tracking transport after each tool call. */
|
|
108
|
+
flushAfterToolCall?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Put widget tracking config in each tool response's `_meta.waniwani`, so a
|
|
111
|
+
* widget in the browser can send its own events.
|
|
112
|
+
*
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
injectWidgetToken?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Field names to strip from location `_meta` before events are sent. Pass
|
|
118
|
+
* `["latitude", "longitude"]` to drop coordinates and keep the rest.
|
|
119
|
+
*
|
|
120
|
+
* @default []
|
|
121
|
+
*/
|
|
122
|
+
stripLocationFields?: readonly string[];
|
|
123
|
+
/**
|
|
124
|
+
* Replace flow state fields marked with `redacted()` before they are tracked.
|
|
125
|
+
* Wire it to an env var to keep real values in development and redact in
|
|
126
|
+
* production.
|
|
127
|
+
*
|
|
128
|
+
* @default false
|
|
129
|
+
*/
|
|
130
|
+
applyFieldRedactions?: boolean;
|
|
131
|
+
};
|
|
38
132
|
export type AppConfig = {
|
|
39
133
|
/** MCP server name, e.g. `oney-split-payment`. */
|
|
40
134
|
name: string;
|
|
@@ -47,6 +141,14 @@ export type AppConfig = {
|
|
|
47
141
|
* call. Tone, guardrails, what this app is for.
|
|
48
142
|
*/
|
|
49
143
|
instructions?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Tune, or decline, the `search` tool the template ships. Reaches the
|
|
146
|
+
* template through the generated `waniwani.ts`; a template that ships no such
|
|
147
|
+
* tool ignores it.
|
|
148
|
+
*/
|
|
149
|
+
search?: SearchOptions;
|
|
150
|
+
/** Tracking behaviour for every tool call this app serves. */
|
|
151
|
+
tracking?: TrackingOptions;
|
|
50
152
|
};
|
|
51
153
|
export declare function defineApp(config: AppConfig): AppConfig;
|
|
52
154
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,4EAA4E;AAC5E,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,WAAW,CAAC;AAElC,+CAA+C;AAC/C,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7D;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAIF,MAAM,MAAM,SAAS,GAAG;IACvB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAoJH,MAAM,UAAU,SAAS,CAAC,MAAiB;IAC1C,OAAO,MAAM,CAAC;AACf,CAAC;AAwBD,MAAM,UAAU,UAAU,CACzB,GAAyB;IAEzB,OAAO,GAAG,CAAC;AACZ,CAAC;AAuCD,MAAM,UAAU,YAAY,CAAkB,GAAwB;IACrE,OAAO,GAAG,CAAC;AACZ,CAAC;AA4CD,MAAM,UAAU,cAAc,CAAC,GAAuB;IACrD,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waniwani/kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Build an MCP app as a folder: tools, widgets and flows, with one CLI and one shared server runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,9 +22,6 @@
|
|
|
22
22
|
"default": "./dist/web.js"
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
|
-
"//files": "`src` ships alongside `dist` on purpose: `waniwani eject` vendors the runtime as readable TypeScript, and it reads it out of the installed package.",
|
|
26
|
-
"//dependencies": "tsx, typescript and the @types packages are here rather than in devDependencies because the underlying framework shells out to tsc and tsx by bare name and resolves types from the app repo tree, while declaring none of them. An app repo owns no build config, so this package is the only thing that can put them there. nodemon is the fourth of that set and arrives on its own, as a peer of it.",
|
|
27
|
-
"//express": "express and cors back the api/ convention: the runtime mounts each endpoint with a JSON body parser and CORS, and an app's handlers are typed against express. Both are already skybridge's own dependencies at these ranges, so declaring them here adds no second copy — it stops an app from depending on a transitive hoist.",
|
|
28
25
|
"files": [
|
|
29
26
|
"dist",
|
|
30
27
|
"src",
|
|
@@ -48,20 +45,20 @@
|
|
|
48
45
|
"@types/react": "^19.2.14",
|
|
49
46
|
"@types/react-dom": "^19.2.3",
|
|
50
47
|
"@vitejs/plugin-react": "^6.0.3",
|
|
51
|
-
"@waniwani/sdk": "^0.19.5",
|
|
52
48
|
"cors": "^2.8.6",
|
|
53
49
|
"dotenv": "^17.4.1",
|
|
54
50
|
"express": "^5.2.1",
|
|
55
|
-
"skybridge": "
|
|
51
|
+
"skybridge": "1.4.0",
|
|
56
52
|
"tailwindcss": "^4.3.3",
|
|
57
53
|
"tsx": "^4.20.6",
|
|
58
54
|
"typescript": "^6.0.2",
|
|
59
55
|
"vite": "^8.1.5"
|
|
60
56
|
},
|
|
61
57
|
"devDependencies": {
|
|
62
|
-
"@skybridge/devtools": "
|
|
58
|
+
"@skybridge/devtools": "1.4.0"
|
|
63
59
|
},
|
|
64
60
|
"peerDependencies": {
|
|
61
|
+
"@waniwani/sdk": ">=0.19.9-beta.0",
|
|
65
62
|
"react": ">=19",
|
|
66
63
|
"react-dom": ">=19",
|
|
67
64
|
"zod": "^4"
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,103 @@ export type ToolHints = {
|
|
|
42
42
|
|
|
43
43
|
// ---------------------------------------------------------------- app config
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* What an app may set on the template's `search` tool.
|
|
47
|
+
*
|
|
48
|
+
* Declared here and again in the template's `src/search/index.ts`, on purpose:
|
|
49
|
+
* the template does not depend on this package, so there is no type to share.
|
|
50
|
+
* The two meet at the generated `waniwani.ts` and nowhere else, which makes this
|
|
51
|
+
* a mirror that has to be kept in step by hand. A field added on one side and
|
|
52
|
+
* not the other is silently dropped rather than reported.
|
|
53
|
+
*/
|
|
54
|
+
export type SearchOptions = {
|
|
55
|
+
/**
|
|
56
|
+
* Whether the template registers the tool at all.
|
|
57
|
+
*
|
|
58
|
+
* `false` is the only way an app can decline it, because it cannot unregister
|
|
59
|
+
* what the template has already registered. Worth using: a deployment with no
|
|
60
|
+
* corpus behind it, or one holding another market's documents, is better off
|
|
61
|
+
* without the tool than with one answering confidently out of the wrong file.
|
|
62
|
+
*/
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
/** Passages to ask for, 1-20. Unset leaves the SDK's default of 5. */
|
|
65
|
+
topK?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Similarity floor, 0-1, under which a passage is dropped rather than ranked
|
|
68
|
+
* last. Unset leaves the SDK's default of 0.3.
|
|
69
|
+
*/
|
|
70
|
+
minScore?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Exact-match filter on chunk metadata: a passage must carry all of these
|
|
73
|
+
* pairs to come back. With the corpus tagged at ingest time, this is a gate in
|
|
74
|
+
* code rather than a line of prompt.
|
|
75
|
+
*/
|
|
76
|
+
metadata?: Record<string, string>;
|
|
77
|
+
/** Give up on a slow search and answer as though nothing matched. */
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
/** Name the source document on each passage. */
|
|
80
|
+
includeSources?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Framing prepended to the answer text. Retrieved passages are third-party
|
|
83
|
+
* text on its way into a prompt; this is where an app says they are reference
|
|
84
|
+
* material rather than instructions.
|
|
85
|
+
*/
|
|
86
|
+
preamble?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Status text the host shows while the call is in flight, and once it has
|
|
89
|
+
* returned. Configurable because the defaults are English and this string is
|
|
90
|
+
* one of the few a user actually reads.
|
|
91
|
+
*/
|
|
92
|
+
invoking?: string;
|
|
93
|
+
invoked?: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The event categories the tracking backend recognises. */
|
|
97
|
+
export type ToolType = "pricing" | "product_info" | "availability" | "support" | "other";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Tracking options, forwarded whole to the SDK's `withWaniwani()`.
|
|
101
|
+
*
|
|
102
|
+
* A mirror of that function's options for the same reason as `SearchOptions`
|
|
103
|
+
* above, narrowed to what an app declares rather than constructs: the SDK also
|
|
104
|
+
* accepts a `client` instance and an `onError` callback, and neither belongs in a
|
|
105
|
+
* config file.
|
|
106
|
+
*
|
|
107
|
+
* `flushAfterToolCall` is the one that matters on serverless. An invocation
|
|
108
|
+
* frozen between tool calls takes any unsent event batch with it, and this is
|
|
109
|
+
* the only way an app can ask for the flush.
|
|
110
|
+
*/
|
|
111
|
+
export type TrackingOptions = {
|
|
112
|
+
/** One category for every tool, or a function mapping tool name to category. */
|
|
113
|
+
toolType?: ToolType | ((toolName: string) => ToolType | undefined);
|
|
114
|
+
/** Merged into every tracked event. */
|
|
115
|
+
metadata?: Record<string, unknown>;
|
|
116
|
+
/** Flush the tracking transport after each tool call. */
|
|
117
|
+
flushAfterToolCall?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Put widget tracking config in each tool response's `_meta.waniwani`, so a
|
|
120
|
+
* widget in the browser can send its own events.
|
|
121
|
+
*
|
|
122
|
+
* @default true
|
|
123
|
+
*/
|
|
124
|
+
injectWidgetToken?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Field names to strip from location `_meta` before events are sent. Pass
|
|
127
|
+
* `["latitude", "longitude"]` to drop coordinates and keep the rest.
|
|
128
|
+
*
|
|
129
|
+
* @default []
|
|
130
|
+
*/
|
|
131
|
+
stripLocationFields?: readonly string[];
|
|
132
|
+
/**
|
|
133
|
+
* Replace flow state fields marked with `redacted()` before they are tracked.
|
|
134
|
+
* Wire it to an env var to keep real values in development and redact in
|
|
135
|
+
* production.
|
|
136
|
+
*
|
|
137
|
+
* @default false
|
|
138
|
+
*/
|
|
139
|
+
applyFieldRedactions?: boolean;
|
|
140
|
+
};
|
|
141
|
+
|
|
45
142
|
export type AppConfig = {
|
|
46
143
|
/** MCP server name, e.g. `oney-split-payment`. */
|
|
47
144
|
name: string;
|
|
@@ -54,6 +151,14 @@ export type AppConfig = {
|
|
|
54
151
|
* call. Tone, guardrails, what this app is for.
|
|
55
152
|
*/
|
|
56
153
|
instructions?: string;
|
|
154
|
+
/**
|
|
155
|
+
* Tune, or decline, the `search` tool the template ships. Reaches the
|
|
156
|
+
* template through the generated `waniwani.ts`; a template that ships no such
|
|
157
|
+
* tool ignores it.
|
|
158
|
+
*/
|
|
159
|
+
search?: SearchOptions;
|
|
160
|
+
/** Tracking behaviour for every tool call this app serves. */
|
|
161
|
+
tracking?: TrackingOptions;
|
|
57
162
|
};
|
|
58
163
|
|
|
59
164
|
export function defineApp(config: AppConfig): AppConfig {
|