@waniwani/kit 0.1.4 → 0.1.6-beta.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/README.md CHANGED
@@ -529,6 +529,54 @@ flowchart LR
529
529
  project carrying a `vercel.json`, which is what lets `vercel deploy` inside it
530
530
  work with no special support.
531
531
 
532
+ ### Deploying is a git push
533
+
534
+ `waniwani build` writes a Vercel Build Output tree inside `.waniwani/`: the
535
+ bundled function, the static assets, the routing config. A git-connected project
536
+ builds that tree itself on push, and the first build writes the config it needs
537
+ into the app repo:
538
+
539
+ ```json
540
+ // vercel.json, generated once, yours to edit afterwards
541
+ {
542
+ "framework": null,
543
+ "buildCommand": "waniwani build && rm -rf .vercel/output && cp -R .waniwani/.vercel/output .vercel/output",
544
+ "routes": [{ "src": "/api(/.*)?", "dest": "/mcp" }]
545
+ }
546
+ ```
547
+
548
+ Each line answers something Vercel would otherwise get wrong.
549
+
550
+ `framework: null` stops the project's preset from hunting for a dependency the
551
+ repo does not have, which is what produces `No Next.js version detected` on a
552
+ repo holding no framework at all.
553
+
554
+ The `buildCommand` moves the tree from `.waniwani/`, which is gitignored and
555
+ absent from the clone, up to the one path where Vercel adopts the Build Output
556
+ API and serves the function as built.
557
+
558
+ The `routes` entry exists because Vercel reserves a root `api/` directory: it
559
+ compiles every file under one into a serverless function of its own, and an
560
+ endpoint module is not a Vercel handler. That entry is emitted ahead of Vercel's
561
+ filesystem layer, so `/api/*` reaches the server the kit built and the functions
562
+ Vercel made are never routed to. Deleting the directory during the build is not
563
+ an alternative, since the file list is read before the build command runs:
564
+
565
+ ```
566
+ Error: File not found: /vercel/path0/api/cal/book.ts
567
+ ```
568
+
569
+ Deploying without a build works too, once `build` has run:
570
+
571
+ ```bash
572
+ cd .waniwani && vercel deploy --prebuilt
573
+ ```
574
+
575
+ Environment variables live on the platform for both, since `.env` is read from
576
+ disk and a hosted build has no such file. A project that sets its variables for
577
+ production alone gets previews with none, which for an app whose flow reads
578
+ `WANIWANI_API_KEY` at import time means a function that fails to boot.
579
+
532
580
  ### Secrets live in the app's .env
533
581
 
534
582
  `.env` and `.env.local` sit next to `waniwani.config.ts`, and every command reads
package/cli/codegen.mjs CHANGED
@@ -44,9 +44,34 @@ import { fileURLToPath } from "node:url";
44
44
 
45
45
  const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
46
46
  const RUNTIME_SRC = join(PACKAGE_ROOT, "src");
47
- const PACKAGE_VERSION = JSON.parse(
48
- readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"),
49
- ).version;
47
+ /** This package's own manifest, which is where every version below comes from. */
48
+ const MANIFEST = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
49
+ const PACKAGE_VERSION = MANIFEST.version;
50
+
51
+ /**
52
+ * A version this package declares, read back out so it is stated once.
53
+ *
54
+ * Every version the generator forces on an app is a version the generator was
55
+ * built and verified against, which makes this manifest the only honest source
56
+ * for it. Writing the same range a second time as a literal down in `PINS` gave
57
+ * one fact two homes, and a bump could update either one alone: the manifest
58
+ * carried `skybridge@^1.3.5` while the pin forced `1.4.0`, and they agreed only
59
+ * because that is what the lockfile happened to resolve.
60
+ *
61
+ * Missing throws rather than defaults. `undefined` here would land in a
62
+ * generated `package.json` as a dependency with no version and fail at install
63
+ * time in someone else's project, a long way from the rename that caused it.
64
+ */
65
+ function declared(name, field = "dependencies") {
66
+ const version = MANIFEST[field]?.[name];
67
+ if (!version) {
68
+ throw new Error(
69
+ `@waniwani/kit declares no ${field}.${name}, and the generator pins apps to it — ` +
70
+ "add it back to packages/kit/package.json or drop it from PINS",
71
+ );
72
+ }
73
+ return version;
74
+ }
50
75
 
51
76
  /**
52
77
  * The template comes across whole, minus an explicit list.
@@ -160,17 +185,27 @@ const SEAM = { file: "src/server.ts", symbol: "registerApp" };
160
185
  *
161
186
  * Each entry carries its reason, and the CLI reports what it changed.
162
187
  */
163
- /** Forced to an exact version: the generated code is built against these. */
188
+ /**
189
+ * Forced to what this package declares: the generated code is built against
190
+ * these, and `declared()` is what keeps the two statements of that one fact
191
+ * from drifting apart.
192
+ */
164
193
  const PINS = {
165
194
  dependencies: {
166
195
  skybridge: {
167
- version: "1.4.0",
196
+ version: declared("skybridge"),
168
197
  why: "the template's range floats within 1.x; the runtime is built and verified against this one",
169
198
  },
170
- "@waniwani/sdk": { version: "^0.19.5", why: "flows and tracking need the current SDK" },
199
+ "@waniwani/sdk": {
200
+ version: declared("@waniwani/sdk"),
201
+ why: "flows and tracking need the current SDK",
202
+ },
171
203
  },
172
204
  devDependencies: {
173
- "@skybridge/devtools": { version: "1.4.0", why: "must match the framework" },
205
+ "@skybridge/devtools": {
206
+ version: declared("@skybridge/devtools", "devDependencies"),
207
+ why: "must match the framework",
208
+ },
174
209
  },
175
210
  };
176
211
 
@@ -186,6 +221,36 @@ const ENSURED = {
186
221
  },
187
222
  };
188
223
 
224
+ /**
225
+ * What the vendored runtime needs declared, for the eject layout only.
226
+ *
227
+ * A build reaches the runtime through `@waniwani/kit`, so express, cors and
228
+ * their types arrive as that package's own dependencies — which is why it
229
+ * declares them (see its `//dependencies` and `//express` notes). Ejecting drops
230
+ * the package and copies `src/` in as source, and the imports come with it: the
231
+ * vendored tree imports `express` and `cors` by name, and `tsc` needs their
232
+ * types. Nothing was putting either back, so an ejected project installed and
233
+ * then failed to compile on ten TS7006/TS7016 errors, with express and cors
234
+ * present in `node_modules` only as a transitive hoist out of the framework.
235
+ *
236
+ * Only the two the runtime imports and the app does not already get: `skybridge`
237
+ * and `zod` are the other bare specifiers under `src/`, and both are declared
238
+ * for every layout already.
239
+ */
240
+ const VENDORED = {
241
+ dependencies: {
242
+ express: { version: declared("express"), why: "the vendored runtime imports express" },
243
+ cors: { version: declared("cors"), why: "the vendored runtime mounts CORS per endpoint" },
244
+ },
245
+ devDependencies: {
246
+ "@types/express": {
247
+ version: declared("@types/express"),
248
+ why: "the vendored runtime is typed against express",
249
+ },
250
+ "@types/cors": { version: declared("@types/cors"), why: "same, for cors" },
251
+ },
252
+ };
253
+
189
254
  /**
190
255
  * Scripts the generated layout needs, added only when the template has no
191
256
  * script by that name. The template's own scripts are left untouched.
@@ -625,6 +690,13 @@ export const app = {
625
690
  title: config.title,
626
691
  version: config.version ?? ${JSON.stringify(version ?? "0.0.0")},
627
692
  instructions: config.instructions,
693
+ // Forwarded whole, for the template to read if it has anything to read them
694
+ // with: \`search\` tunes the search tool a template ships, \`tracking\` reaches
695
+ // the SDK's withWaniwani(). A template that uses neither ignores both, so
696
+ // emitting them unconditionally keeps one generator working across templates
697
+ // that read them and templates that do not.
698
+ search: config.search,
699
+ tracking: config.tracking,
628
700
  };
629
701
 
630
702
  export async function registerApp(server: McpServer): Promise<void> {
@@ -779,6 +851,16 @@ function generatePackageJson(app, appPackageJson, template, layout) {
779
851
  overrides.push({ name, to: version, why });
780
852
  }
781
853
 
854
+ // Same rule as ENSURED — an app or template declaring its own keeps it —
855
+ // but only where the runtime arrives as source rather than as a package.
856
+ if (layout.vendored) {
857
+ for (const [name, { version, why }] of Object.entries(VENDORED[kind] ?? {})) {
858
+ if (merged[name]) continue;
859
+ merged[name] = version;
860
+ overrides.push({ name, to: version, why });
861
+ }
862
+ }
863
+
782
864
  return merged;
783
865
  };
784
866
 
@@ -875,6 +957,51 @@ function readProvenance(root) {
875
957
  }
876
958
  }
877
959
 
960
+ /**
961
+ * What a git-connected Vercel project needs at the app root, written there when
962
+ * the app has none.
963
+ *
964
+ * The build output lands in `.waniwani/`, which is gitignored and absent from
965
+ * the clone, so a hosted build has to run the kit itself and move the tree to
966
+ * the one path where Vercel adopts the Build Output API. Every line here is
967
+ * about this kit's own layout, which is why the file is generated rather than
968
+ * taken from the template: the template knows nothing about `waniwani build` or
969
+ * `.waniwani/`.
970
+ *
971
+ * The `routes` entry is the part that is not obvious. Vercel reserves a root
972
+ * `api/` directory and compiles every file under it into a serverless function
973
+ * of its own, which for an app folder means one broken function per endpoint
974
+ * (`defineEndpoint({ ... })` is an object, not a Vercel handler) sitting in the
975
+ * filesystem layer ahead of the server that actually serves them. A legacy
976
+ * `routes` entry is emitted before that layer, so `/api/*` reaches the kit's
977
+ * function and Vercel's own are never routed to. There is no way to stop it
978
+ * building them: it reads the file list before the build command runs, so a
979
+ * build that deletes the directory fails with `File not found`, and
980
+ * `outputDirectory` does not suppress it either.
981
+ */
982
+ const VERCEL_JSON = {
983
+ $schema: "https://openapi.vercel.sh/vercel.json",
984
+ // Otherwise the project's framework preset decides, and a preset looking for a
985
+ // dependency an app folder does not have fails the build outright.
986
+ framework: null,
987
+ buildCommand:
988
+ "waniwani build && rm -rf .vercel/output && cp -R .waniwani/.vercel/output .vercel/output",
989
+ // Ahead of Vercel's filesystem layer, which is where its own api/ functions sit.
990
+ routes: [{ src: "/api(/.*)?", dest: "/mcp" }],
991
+ };
992
+
993
+ /**
994
+ * @returns true when the file was written, for the CLI to report
995
+ */
996
+ function ensureVercelJson(appRoot) {
997
+ const file = join(appRoot, "vercel.json");
998
+ // An app that has edited its own deploy config keeps it. Overwriting would
999
+ // throw away a `maxDuration`, a region, or a cron someone needed.
1000
+ if (existsSync(file)) return false;
1001
+ writeFileSync(file, `${JSON.stringify(VERCEL_JSON, null, 2)}\n`);
1002
+ return true;
1003
+ }
1004
+
878
1005
  /** Keep `.waniwani/` out of the app repo, the way `.next/` is kept out. */
879
1006
  function ignoreBuildOutput(appRoot) {
880
1007
  const file = join(appRoot, ".gitignore");
@@ -1037,6 +1164,17 @@ export function generate(app, { template, layout: layoutName = "build", outDir }
1037
1164
  sha: template.sha,
1038
1165
  local: template.local,
1039
1166
  manifest: manifest ? MANIFEST_FILE : undefined,
1167
+ // Which generator wrote this tree, and the versions it forced
1168
+ // while doing it. A deployed app misbehaving is the case this
1169
+ // serves: the tree itself then answers which template commit and
1170
+ // which SDK it was built from, without a guess from the app's
1171
+ // lockfile or from whatever the CLI happens to pin today.
1172
+ kit: PACKAGE_VERSION,
1173
+ pins: Object.fromEntries(
1174
+ Object.values(PINS).flatMap((group) =>
1175
+ Object.entries(group).map(([name, pin]) => [name, pin.version]),
1176
+ ),
1177
+ ),
1040
1178
  // What survived to the end, copied and generated alike. The
1041
1179
  // copy is the raw list minus whatever a generated file replaced,
1042
1180
  // and the generated half is here so that a build which stops
@@ -1055,11 +1193,23 @@ export function generate(app, { template, layout: layoutName = "build", outDir }
1055
1193
  )}\n`,
1056
1194
  );
1057
1195
 
1196
+ let vercelJson = false;
1058
1197
  if (layoutName === "build") {
1059
1198
  // A .gitignore inside the output would stop `vercel deploy` uploading
1060
1199
  // anything, so the ignore goes in the app repo instead.
1061
1200
  ignoreBuildOutput(app.root);
1201
+ // Same reasoning for the deploy config: what Vercel reads on a git build is
1202
+ // the app repo's root, not the output directory.
1203
+ vercelJson = ensureVercelJson(app.root);
1062
1204
  }
1063
1205
 
1064
- return { outDir: root, written, overrides, fromTemplate, moved, manifest: Boolean(manifest) };
1206
+ return {
1207
+ outDir: root,
1208
+ written,
1209
+ overrides,
1210
+ fromTemplate,
1211
+ moved,
1212
+ vercelJson,
1213
+ manifest: Boolean(manifest),
1214
+ };
1065
1215
  }
package/cli/index.mjs CHANGED
@@ -143,7 +143,12 @@ async function prepare(appRoot, flags, { quiet = false } = {}) {
143
143
  console.log(`${yellow("!")} ${dim("GitHub unreachable — using the cached template")}`);
144
144
  }
145
145
 
146
- const { outDir, overrides, fromTemplate, manifest } = generate(app, { template });
146
+ const { outDir, overrides, fromTemplate, manifest, vercelJson } = generate(app, { template });
147
+ // Written into the app's own repo rather than the output, so it is worth a
148
+ // line even outside debug: it is a tracked file that appeared.
149
+ if (!quiet && vercelJson) {
150
+ console.log(`${green("+")} ${bold("vercel.json")} ${dim("— deploy config for a git-connected project")}`);
151
+ }
147
152
  if (!quiet && DEBUG) {
148
153
  console.log(
149
154
  `${dim(`${fromTemplate.length} files copied`)} ${dim(
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
- * `beta`, not `main`. The generator is written against the template's current
23
- * layout — `vite.config.ts`, `src/server.ts`, `src/views/` — and `main` is still
24
- * the older `server/` + `web/` + `api/` split, which it cannot absorb. When beta
25
- * merges down, this goes back to a ref on `main` (a tag, ideally see the
26
- * README's known gaps).
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 = "github:WaniWani-AI/mcp-distribution-template#beta";
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
- return `${template.source} @ ${template.sha?.slice(0, 7)} (${state})`;
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/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
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
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;AA2CH,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"}
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.4",
3
+ "version": "0.1.6-beta.0",
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": {
@@ -25,6 +25,7 @@
25
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
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
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
+ "//skybridge": "skybridge and @skybridge/devtools are exact, and the two versions match each other. codegen.mjs forces both on every generated app by reading them back out of this file, so a range here would mean the kit was verified against whatever it resolved while apps were pinned to something else. Bumping them is scripts/bump-deps.mjs, and scripts/template-contract.mjs is what proves the bump.",
28
29
  "files": [
29
30
  "dist",
30
31
  "src",
@@ -48,18 +49,18 @@
48
49
  "@types/react": "^19.2.14",
49
50
  "@types/react-dom": "^19.2.3",
50
51
  "@vitejs/plugin-react": "^6.0.3",
51
- "@waniwani/sdk": "^0.19.5",
52
+ "@waniwani/sdk": "0.19.9-beta.0",
52
53
  "cors": "^2.8.6",
53
54
  "dotenv": "^17.4.1",
54
55
  "express": "^5.2.1",
55
- "skybridge": "^1.3.5",
56
+ "skybridge": "1.4.0",
56
57
  "tailwindcss": "^4.3.3",
57
58
  "tsx": "^4.20.6",
58
59
  "typescript": "^6.0.2",
59
60
  "vite": "^8.1.5"
60
61
  },
61
62
  "devDependencies": {
62
- "@skybridge/devtools": "^1.2.7"
63
+ "@skybridge/devtools": "1.4.0"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "react": ">=19",
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 {