@yichus/mcp 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // src/server.ts
2
- import { LocalRelayMcpServer } from "@mcp-b/webmcp-local-relay";
3
- import { z } from "zod";
2
+ import { z as z2 } from "zod";
4
3
 
5
4
  // src/calculators.ts
6
5
  var DEFAULT_BASE_URL = "https://yich.us";
@@ -17,6 +16,12 @@ var CALCULATORS = [
17
16
  description: "How much water does one rainfall event collect, and how much overflows the tank?",
18
17
  path: "demos/simulation/rainwater.html"
19
18
  },
19
+ {
20
+ slug: "rainwater-history",
21
+ title: "Rainwater through a dry spell",
22
+ description: "Run NOAA weather observations through an inspectable daily tank model. Change capacity and demand, inspect captured data, and share a saved report.",
23
+ path: "demos/simulation/rainwater-history.html"
24
+ },
20
25
  {
21
26
  slug: "battery-runtime",
22
27
  title: "Battery runtime",
@@ -166,44 +171,272 @@ function openUrlInBrowser(rawUrl) {
166
171
  command = "xdg-open";
167
172
  args = [validated];
168
173
  }
169
- spawn(command, args, { detached: true, stdio: "ignore" }).unref();
170
- return { command, args };
174
+ return new Promise((resolve) => {
175
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
176
+ child.once("spawn", () => {
177
+ child.unref();
178
+ resolve({ command, args, opened: true });
179
+ });
180
+ child.on("error", (error) => resolve({ command, args, opened: false, error: error.message }));
181
+ });
182
+ }
183
+
184
+ // src/relay.ts
185
+ import {
186
+ LocalRelayMcpServer,
187
+ RelayBridgeServer,
188
+ RelayRegistry,
189
+ buildPublicToolName
190
+ } from "@mcp-b/webmcp-local-relay";
191
+ var REMEMBERED_TABS = 256;
192
+ var FirstTabNamingRegistry = class extends RelayRegistry {
193
+ firstSeen = /* @__PURE__ */ new Map();
194
+ tabByConnection = /* @__PURE__ */ new Map();
195
+ seen = 0;
196
+ upsertSource(connectionId, hello) {
197
+ super.upsertSource(connectionId, hello);
198
+ this.tabByConnection.set(connectionId, hello.tabId);
199
+ this.remember(hello.tabId);
200
+ }
201
+ removeConnection(connectionId) {
202
+ super.removeConnection(connectionId);
203
+ this.tabByConnection.delete(connectionId);
204
+ }
205
+ listTools() {
206
+ return this.named().map(({ tool }) => tool).sort((a, b) => a.name.localeCompare(b.name));
207
+ }
208
+ resolveInvocation(options) {
209
+ const entry = this.named().find(({ tool }) => tool.name === options.toolName);
210
+ if (!entry) return null;
211
+ const resolved = super.resolveInvocation({ ...options, toolName: entry.relayName });
212
+ return resolved && { ...resolved, publicToolName: entry.tool.name };
213
+ }
214
+ // Each relay tool with the name an agent sees: the plain name for the
215
+ // copy published by the first-seen tab among those publishing it.
216
+ named() {
217
+ const tools = super.listTools();
218
+ const owners = /* @__PURE__ */ new Map();
219
+ for (const tool of tools) {
220
+ for (const source of tool.sources) {
221
+ const order = this.order(source.tabId);
222
+ owners.set(tool.originalName, Math.min(owners.get(tool.originalName) ?? order, order));
223
+ }
224
+ }
225
+ return tools.map((tool) => {
226
+ const owned = tool.sources.some((source) => this.order(source.tabId) === owners.get(tool.originalName));
227
+ const name = owned ? buildPublicToolName({ originalToolName: tool.originalName }) : tool.name;
228
+ return { relayName: tool.name, tool: { ...tool, name } };
229
+ });
230
+ }
231
+ order(tabId) {
232
+ return this.firstSeen.get(tabId) ?? Number.POSITIVE_INFINITY;
233
+ }
234
+ remember(tabId) {
235
+ if (this.firstSeen.has(tabId)) return;
236
+ this.firstSeen.set(tabId, this.seen++);
237
+ const open = new Set(this.tabByConnection.values());
238
+ for (const tab of this.firstSeen.keys()) {
239
+ if (this.firstSeen.size - open.size <= REMEMBERED_TABS) break;
240
+ if (!open.has(tab)) this.firstSeen.delete(tab);
241
+ }
242
+ }
243
+ };
244
+ var TOOL_LIST_CHANGED = "notifications/tools/list_changed";
245
+ var isToolListChanged = (message) => "method" in message && message.method === TOOL_LIST_CHANGED && !("id" in message);
246
+ var ToolListChangedCoalescer = class {
247
+ constructor(inner, windowMs) {
248
+ this.inner = inner;
249
+ this.windowMs = windowMs;
250
+ inner.onmessage = (message, extra) => this.onmessage?.(message, extra);
251
+ inner.onerror = (error) => this.onerror?.(error);
252
+ inner.onclose = () => {
253
+ this.drop();
254
+ this.onclose?.();
255
+ };
256
+ }
257
+ inner;
258
+ windowMs;
259
+ onclose;
260
+ onerror;
261
+ onmessage;
262
+ pending = null;
263
+ get sessionId() {
264
+ return this.inner.sessionId;
265
+ }
266
+ setProtocolVersion(version) {
267
+ this.inner.setProtocolVersion?.(version);
268
+ }
269
+ start() {
270
+ return this.inner.start();
271
+ }
272
+ async send(message, options) {
273
+ if (!isToolListChanged(message)) return this.inner.send(message, options);
274
+ if (this.pending) return;
275
+ this.pending = setTimeout(() => {
276
+ this.pending = null;
277
+ this.inner.send(message, options).catch((error) => this.onerror?.(error));
278
+ }, this.windowMs);
279
+ this.pending.unref?.();
280
+ }
281
+ close() {
282
+ this.drop();
283
+ return this.inner.close();
284
+ }
285
+ drop() {
286
+ if (this.pending) clearTimeout(this.pending);
287
+ this.pending = null;
288
+ }
289
+ };
290
+ var CoalescingRelayServer = class extends LocalRelayMcpServer {
291
+ constructor(options, windowMs) {
292
+ super(options);
293
+ this.windowMs = windowMs;
294
+ }
295
+ windowMs;
296
+ connect(transport) {
297
+ return super.connect(new ToolListChangedCoalescer(transport, this.windowMs));
298
+ }
299
+ };
300
+ var PageAnswerBridge = class extends RelayBridgeServer {
301
+ pageContent = /* @__PURE__ */ new WeakMap();
302
+ async invokeTool(...call) {
303
+ const result = await super.invokeTool(...call);
304
+ this.pageContent.set(result, result.content);
305
+ return result;
306
+ }
307
+ /** The content a page answered with, for an answer this bridge carried. */
308
+ pageAnswer(result) {
309
+ return this.pageContent.get(result);
310
+ }
311
+ };
312
+
313
+ // src/saveCalculator.ts
314
+ import { mkdir, writeFile } from "fs/promises";
315
+ import { isAbsolute, join } from "path";
316
+ import { z } from "zod";
317
+ var saveCalculatorInput = z.object({
318
+ exportToolName: z.string().regex(/^yichus_example_export(?:_[a-zA-Z0-9_-]+)?$/).describe("Public export tool name from webmcp_list_tools, matched to the intended tab."),
319
+ exampleId: z.string().min(1),
320
+ expectedRevision: z.number().int().nonnegative(),
321
+ outputDirectory: z.string().refine(isAbsolute, "Use an absolute output directory.").describe("A new directory under an existing parent. Existing directories are refused; files are never overwritten.")
322
+ });
323
+ var chunkSchema = z.object({
324
+ exampleId: z.string(),
325
+ revision: z.number().int(),
326
+ exportId: z.string().min(1),
327
+ assetBaseUrl: z.url(),
328
+ sources: z.array(z.object({ fileName: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.bosatsu$/), source: z.string().max(1e6) })).min(1).max(16),
329
+ html: z.object({
330
+ fileName: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.html$/),
331
+ content: z.string().min(1).max(32768),
332
+ offset: z.number().int().nonnegative(),
333
+ totalLength: z.number().int().positive().max(32 * 1024 * 1024),
334
+ nextOffset: z.number().int().nonnegative().nullable()
335
+ })
336
+ });
337
+ async function saveCalculator(rawArgs, invoke) {
338
+ const args = saveCalculatorInput.parse(rawArgs);
339
+ const content = [];
340
+ let first;
341
+ let offset = 0;
342
+ for (; ; ) {
343
+ const response = z.object({ isError: z.boolean().optional(), structuredContent: z.unknown().optional() }).parse(await invoke(args.exportToolName, {
344
+ exampleId: args.exampleId,
345
+ expectedRevision: args.expectedRevision,
346
+ offset,
347
+ ...first ? { expectedExportId: first.exportId } : {}
348
+ }));
349
+ if (response.isError) throw new Error("Browser export failed. Read the current example feedback before retrying.");
350
+ const chunk = chunkSchema.parse(response.structuredContent);
351
+ if (chunk.exampleId !== args.exampleId || chunk.revision !== args.expectedRevision || chunk.html.offset !== offset) throw new Error("Export does not match the requested example, revision, or offset.");
352
+ if (first && (chunk.exportId !== first.exportId || chunk.assetBaseUrl !== first.assetBaseUrl || chunk.html.fileName !== first.html.fileName || chunk.html.totalLength !== first.html.totalLength || JSON.stringify(chunk.sources) !== JSON.stringify(first.sources))) throw new Error("Export changed during transfer. Nothing was saved; start again.");
353
+ first ??= chunk;
354
+ const end = offset + chunk.html.content.length;
355
+ if (end > chunk.html.totalLength || (chunk.html.nextOffset === null ? end !== chunk.html.totalLength : chunk.html.nextOffset !== end || end >= chunk.html.totalLength)) throw new Error("Incomplete or inconsistent export chunk. Nothing was saved.");
356
+ content.push(chunk.html.content);
357
+ if (chunk.html.nextOffset === null) break;
358
+ offset = chunk.html.nextOffset;
359
+ }
360
+ const files = [...first.sources.map((s) => ({ fileName: s.fileName, content: s.source })), { fileName: first.html.fileName, content: content.join("") }];
361
+ if (new Set(files.map((f) => f.fileName.toLowerCase())).size !== files.length) throw new Error("Export file names must be unique.");
362
+ await mkdir(args.outputDirectory);
363
+ for (const file of files) await writeFile(join(args.outputDirectory, file.fileName), file.content, { encoding: "utf8", flag: "wx" });
364
+ return {
365
+ ok: true,
366
+ directory: args.outputDirectory,
367
+ revision: first.revision,
368
+ exportId: first.exportId,
369
+ assetBaseUrl: first.assetBaseUrl,
370
+ files: files.map((f) => ({ fileName: f.fileName, path: join(args.outputDirectory, f.fileName), bytes: Buffer.byteLength(f.content) })),
371
+ htmlPath: join(args.outputDirectory, first.html.fileName),
372
+ note: "The HTML starts from configured defaults and loads supporting assets from the site. It requires network access. No program was executed."
373
+ };
171
374
  }
172
375
 
173
376
  // src/server.ts
174
377
  var PACKAGE_NAME = "yichus-mcp";
175
- var PACKAGE_VERSION = "0.1.0";
378
+ var PACKAGE_VERSION = "0.1.1";
176
379
  function createYichusMcpServer(options = {}) {
177
380
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://yich.us");
178
381
  const bridgeOverrides = options.bridgeOverrides ?? {};
179
- const server = new LocalRelayMcpServer({
382
+ const bridge = new PageAnswerBridge({
383
+ host: bridgeOverrides.host,
384
+ port: bridgeOverrides.port,
385
+ label: bridgeOverrides.label ?? "Yichus",
386
+ // The pair every page hints the widget (web/webmcp-autocomplete.js,
387
+ // the generated calculators); the widget rejects any other hello.
388
+ workspace: bridgeOverrides.workspace ?? "yichus",
389
+ relayId: bridgeOverrides.relayId ?? "yichus",
390
+ allowedOrigins: [baseUrl],
391
+ // Cold browser compilation can outlast the relay's 25-second default.
392
+ // Keep a deadline so an unresponsive tab still fails explicitly.
393
+ invokeTimeoutMs: 12e4
394
+ }, new FirstTabNamingRegistry());
395
+ const server = new CoalescingRelayServer({
180
396
  serverName: PACKAGE_NAME,
181
397
  serverVersion: PACKAGE_VERSION,
182
- bridgeOptions: {
183
- host: bridgeOverrides.host,
184
- port: bridgeOverrides.port,
185
- label: bridgeOverrides.label ?? "Yichus",
186
- // The pair every page hints the widget (web/webmcp-autocomplete.js,
187
- // the generated calculators); the widget rejects any other hello.
188
- workspace: bridgeOverrides.workspace ?? "yichus",
189
- relayId: bridgeOverrides.relayId ?? "yichus",
190
- allowedOrigins: [baseUrl]
191
- }
192
- });
398
+ bridge
399
+ }, TOOL_LIST_CHANGED_WINDOW_MS);
193
400
  registerYichusTools(server, { baseUrl, disableOpener: options.disableOpener ?? false });
401
+ answerRefusalsAsThePageWroteThem(server, bridge);
194
402
  return server;
195
403
  }
196
- function asInternal(server) {
197
- return server;
198
- }
404
+ var TOOL_LIST_CHANGED_WINDOW_MS = 100;
405
+ var isFunction = (field) => typeof field === "function";
406
+ var StaticToolFields = z2.object({
407
+ mcpServer: z2.object({
408
+ registerTool: z2.custom(isFunction),
409
+ // The MCP SDK's record of each registered tool; the relay's
410
+ // webmcp_call_tool is wrapped there (answerRefusalsAsThePageWroteThem).
411
+ _registeredTools: z2.object({ webmcp_call_tool: z2.object({ handler: z2.custom(isFunction) }) })
412
+ }),
413
+ staticToolData: z2.instanceof(Map)
414
+ });
415
+ var StaticToolHost = z2.custom(
416
+ (value) => StaticToolFields.safeParse(value).success,
417
+ { error: "@mcp-b/webmcp-local-relay no longer has mcpServer.registerTool, a registered webmcp_call_tool handler and staticToolData" }
418
+ );
199
419
  function registerYichusTools(server, ctx) {
200
- const internal = asInternal(server);
201
- registerStatic(internal, {
420
+ const host = StaticToolHost.parse(server);
421
+ registerStatic(host, {
422
+ name: "yichus_save_calculator",
423
+ description: "Save a browser-compiled example and its Bosatsu sources to a NEW local directory. Pass the public yichus_example_export tool name for the intended tab and its current source revision. Transfers bounded chunks internally and returns file paths, not generated HTML. Does not execute the program. Requires the browser tab to stay open.",
424
+ annotations: { readOnlyHint: false, destructiveHint: false },
425
+ input: saveCalculatorInput,
426
+ handler: async (args) => {
427
+ try {
428
+ const payload = await saveCalculator(args, (name, params) => server.bridge.invokeTool(name, params));
429
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload };
430
+ } catch (error) {
431
+ return { isError: true, content: [{ type: "text", text: String(error) }] };
432
+ }
433
+ }
434
+ });
435
+ registerStatic(host, {
202
436
  name: "yichus_list_calculators",
203
437
  description: "List the curated Yichus calculator demos and the playground editor available on the configured Yichus origin. Returns a slug, title, description, and absolute URL for each.",
204
438
  annotations: { readOnlyHint: true, idempotentHint: true },
205
- inputSchemaJson: { type: "object", properties: {}, additionalProperties: false },
206
- inputSchemaZod: {},
439
+ input: z2.object({}),
207
440
  handler: async () => {
208
441
  const entries = listCalculators().map((calc) => ({
209
442
  slug: calc.slug,
@@ -218,24 +451,16 @@ function registerYichusTools(server, ctx) {
218
451
  };
219
452
  }
220
453
  });
221
- registerStatic(internal, {
454
+ registerStatic(host, {
222
455
  name: "yichus_open_calculator",
223
456
  description: "Open a curated Yichus calculator (or the playground) in the user's default browser with WebMCP enabled (?webmcp=1). After opening, the relay tools below will see the page's tools (yichus_list_inputs, yichus_set_inputs, yichus_list_outputs, yichus_explain_output for calculators; yichus_playground_* for the playground). Use yichus_list_calculators to discover slugs.",
224
457
  annotations: { openWorldHint: true },
225
- inputSchemaJson: {
226
- type: "object",
227
- properties: {
228
- slug: {
229
- type: "string",
230
- description: 'Calculator slug from yichus_list_calculators (for example "loan", "investment", "tax", "playground").'
231
- }
232
- },
233
- required: ["slug"],
234
- additionalProperties: false
235
- },
236
- inputSchemaZod: { slug: z.string() },
237
- handler: async (rawArgs) => {
238
- const args = z.object({ slug: z.string() }).parse(rawArgs);
458
+ input: z2.object({
459
+ slug: z2.string().describe(
460
+ 'Calculator slug from yichus_list_calculators (for example "loan", "investment", "tax", "playground").'
461
+ )
462
+ }),
463
+ handler: async (args) => {
239
464
  const calc = findCalculator(args.slug);
240
465
  if (!calc) {
241
466
  const known = listCalculators().map((c) => c.slug).join(", ");
@@ -250,10 +475,7 @@ function registerYichusTools(server, ctx) {
250
475
  };
251
476
  }
252
477
  const url = withWebMcpFlag(calculatorUrl(calc, ctx.baseUrl));
253
- let opener = null;
254
- if (!ctx.disableOpener) {
255
- opener = openUrlInBrowser(url);
256
- }
478
+ const attempt = ctx.disableOpener ? null : await openUrlInBrowser(url);
257
479
  const payload = {
258
480
  calculator: {
259
481
  slug: calc.slug,
@@ -261,35 +483,39 @@ function registerYichusTools(server, ctx) {
261
483
  description: calc.description
262
484
  },
263
485
  url,
264
- opened: !ctx.disableOpener,
265
- opener
486
+ opened: attempt?.opened ?? false,
487
+ opener: attempt && { command: attempt.command, args: attempt.args },
488
+ ...attempt?.error === void 0 ? {} : { openerError: attempt.error }
266
489
  };
267
490
  return {
268
- content: [
269
- {
270
- type: "text",
271
- text: ctx.disableOpener ? `Would open ${calc.title} at ${url} (browser opener disabled).` : `Opening ${calc.title} at ${url}.`
272
- }
273
- ],
491
+ content: [{ type: "text", text: openAnswer(calc, url, attempt) }],
274
492
  structuredContent: payload
275
493
  };
276
494
  }
277
495
  });
278
496
  }
279
- function registerStatic(internal, spec) {
280
- const config = {
281
- description: spec.description,
282
- inputSchema: spec.inputSchemaZod
497
+ function openAnswer(calc, url, attempt) {
498
+ if (attempt === null) return `Would open ${calc.title} at ${url} (browser opener disabled).`;
499
+ if (attempt.opened) return `Opening ${calc.title} at ${url}.`;
500
+ return `Could not open a browser on this machine (${attempt.error}). Open this link in a browser yourself to load ${calc.title}: ${url}`;
501
+ }
502
+ function answerRefusalsAsThePageWroteThem(server, bridge) {
503
+ const callTool = StaticToolHost.parse(server).mcpServer._registeredTools.webmcp_call_tool;
504
+ const relayHandler = callTool.handler;
505
+ callTool.handler = async (...call) => {
506
+ const result = await relayHandler(...call);
507
+ const page = bridge.pageAnswer(result);
508
+ return result.isError === true && page !== void 0 ? { ...result, content: page } : result;
283
509
  };
284
- if (spec.annotations && Object.keys(spec.annotations).length > 0) {
285
- config.annotations = spec.annotations;
286
- }
287
- internal.mcpServer.registerTool(spec.name, config, spec.handler);
288
- internal.staticToolData.set(spec.name, {
289
- name: spec.name,
290
- description: spec.description,
291
- inputSchema: spec.inputSchemaJson,
292
- ...spec.annotations && Object.keys(spec.annotations).length > 0 ? { annotations: spec.annotations } : {}
510
+ }
511
+ function registerStatic(host, spec) {
512
+ const { name, description, annotations, input, handler } = spec;
513
+ host.mcpServer.registerTool(name, { description, inputSchema: input.shape, annotations }, handler);
514
+ host.staticToolData.set(name, {
515
+ name,
516
+ description,
517
+ inputSchema: z2.toJSONSchema(input, { unrepresentable: "any" }),
518
+ annotations
293
519
  });
294
520
  }
295
521
  function normalizeBaseUrl(raw) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/calculators.ts","../src/openUrl.ts"],"sourcesContent":["import { LocalRelayMcpServer } from '@mcp-b/webmcp-local-relay';\nimport { z } from 'zod';\nimport {\n CALCULATORS,\n PLAYGROUND,\n calculatorUrl,\n findCalculator,\n listCalculators,\n withWebMcpFlag,\n type Calculator,\n} from './calculators.js';\nimport { openUrlInBrowser } from './openUrl.js';\n\nconst PACKAGE_NAME = 'yichus-mcp';\n// Bumped via packages/yichus-mcp/package.json. Kept inline so the bin works\n// without reading package.json from disk at startup.\nconst PACKAGE_VERSION = '0.1.0';\n\nexport interface YichusMcpServerOptions {\n /**\n * Origin of the deployed Yichus instance. The relay rejects browser\n * connections from any other origin and `yichus_open_calculator` builds\n * URLs against this base.\n *\n * @defaultValue `https://yich.us`\n */\n baseUrl?: string;\n\n /**\n * Override the WebMCP local-relay options that get passed straight through\n * to the bundled `LocalRelayMcpServer`. Use this to change the bind port,\n * label, workspace name, or relay id.\n *\n * The page-side embed (`web/webmcp-autocomplete.js`, `ensureRelayEmbed`,\n * and the generated calculator pages) hints the widget a workspace and a\n * relay id, and the widget refuses any relay whose discovery hello\n * differs from either. Every shipped page hints `yichus` for both, so the\n * defaults here are the one agreement; override both sides together.\n */\n bridgeOverrides?: {\n host?: string;\n port?: number;\n label?: string;\n workspace?: string;\n relayId?: string;\n };\n\n /**\n * If `true`, the `yichus_open_calculator` tool returns the URL it would\n * have opened without actually launching a browser process. Used by tests\n * and by anyone running the server in a headless environment.\n *\n * @defaultValue `false`\n */\n disableOpener?: boolean;\n}\n\n/**\n * Construct a fully-wired Yichus MCP server.\n *\n * The returned `LocalRelayMcpServer` already has every default WebMCP relay\n * tool plus the two yichus-curated tools (`yichus_list_calculators` and\n * `yichus_open_calculator`) registered. Call `start()` then `startStdio()`\n * (or `connect(transport)`) to bring it online.\n */\nexport function createYichusMcpServer(options: YichusMcpServerOptions = {}): LocalRelayMcpServer {\n const baseUrl = normalizeBaseUrl(options.baseUrl ?? 'https://yich.us');\n const bridgeOverrides = options.bridgeOverrides ?? {};\n\n const server = new LocalRelayMcpServer({\n serverName: PACKAGE_NAME,\n serverVersion: PACKAGE_VERSION,\n bridgeOptions: {\n host: bridgeOverrides.host,\n port: bridgeOverrides.port,\n label: bridgeOverrides.label ?? 'Yichus',\n // The pair every page hints the widget (web/webmcp-autocomplete.js,\n // the generated calculators); the widget rejects any other hello.\n workspace: bridgeOverrides.workspace ?? 'yichus',\n relayId: bridgeOverrides.relayId ?? 'yichus',\n allowedOrigins: [baseUrl],\n },\n });\n\n registerYichusTools(server, { baseUrl, disableOpener: options.disableOpener ?? false });\n return server;\n}\n\ninterface RelayInternals {\n mcpServer: {\n registerTool: (\n name: string,\n config: Record<string, unknown>,\n handler: (args: unknown) => Promise<unknown> | unknown\n ) => unknown;\n };\n staticToolData: Map<\n string,\n {\n name: string;\n description?: string;\n inputSchema: object;\n annotations?: object;\n }\n >;\n}\n\n/**\n * `@mcp-b/webmcp-local-relay@2.2.1` marks `mcpServer` and `staticToolData`\n * as TypeScript-private but the JavaScript fields are public. We pin the\n * relay version exactly so an upstream rename here is a deliberate audit\n * step rather than a silent breakage.\n */\nfunction asInternal(server: LocalRelayMcpServer): RelayInternals {\n return server as unknown as RelayInternals;\n}\n\nfunction registerYichusTools(\n server: LocalRelayMcpServer,\n ctx: { baseUrl: string; disableOpener: boolean }\n): void {\n const internal = asInternal(server);\n\n registerStatic(internal, {\n name: 'yichus_list_calculators',\n description:\n 'List the curated Yichus calculator demos and the playground editor available on the configured Yichus origin. Returns a slug, title, description, and absolute URL for each.',\n annotations: { readOnlyHint: true, idempotentHint: true },\n inputSchemaJson: { type: 'object', properties: {}, additionalProperties: false },\n inputSchemaZod: {} as z.ZodRawShape,\n handler: async () => {\n const entries = listCalculators().map((calc) => ({\n slug: calc.slug,\n title: calc.title,\n description: calc.description,\n url: calculatorUrl(calc, ctx.baseUrl),\n }));\n const payload = { baseUrl: ctx.baseUrl, calculators: entries };\n return {\n content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],\n structuredContent: payload,\n };\n },\n });\n\n registerStatic(internal, {\n name: 'yichus_open_calculator',\n description:\n \"Open a curated Yichus calculator (or the playground) in the user's default browser with WebMCP enabled (?webmcp=1). After opening, the relay tools below will see the page's tools (yichus_list_inputs, yichus_set_inputs, yichus_list_outputs, yichus_explain_output for calculators; yichus_playground_* for the playground). Use yichus_list_calculators to discover slugs.\",\n annotations: { openWorldHint: true },\n inputSchemaJson: {\n type: 'object',\n properties: {\n slug: {\n type: 'string',\n description:\n 'Calculator slug from yichus_list_calculators (for example \"loan\", \"investment\", \"tax\", \"playground\").',\n },\n },\n required: ['slug'],\n additionalProperties: false,\n },\n inputSchemaZod: { slug: z.string() },\n handler: async (rawArgs) => {\n const args = z.object({ slug: z.string() }).parse(rawArgs);\n const calc: Calculator | undefined = findCalculator(args.slug);\n if (!calc) {\n const known = listCalculators().map((c) => c.slug).join(', ');\n return {\n content: [\n {\n type: 'text',\n text: `Unknown calculator slug \"${args.slug}\". Known slugs: ${known}.`,\n },\n ],\n isError: true,\n };\n }\n const url = withWebMcpFlag(calculatorUrl(calc, ctx.baseUrl));\n let opener: { command: string; args: string[] } | null = null;\n if (!ctx.disableOpener) {\n opener = openUrlInBrowser(url);\n }\n const payload = {\n calculator: {\n slug: calc.slug,\n title: calc.title,\n description: calc.description,\n },\n url,\n opened: !ctx.disableOpener,\n opener,\n };\n return {\n content: [\n {\n type: 'text',\n text: ctx.disableOpener\n ? `Would open ${calc.title} at ${url} (browser opener disabled).`\n : `Opening ${calc.title} at ${url}.`,\n },\n ],\n structuredContent: payload,\n };\n },\n });\n}\n\ninterface StaticToolSpec {\n name: string;\n description: string;\n annotations?: Record<string, unknown>;\n inputSchemaJson: object;\n inputSchemaZod: z.ZodRawShape;\n handler: (args: unknown) => Promise<unknown> | unknown;\n}\n\nfunction registerStatic(internal: RelayInternals, spec: StaticToolSpec): void {\n const config: Record<string, unknown> = {\n description: spec.description,\n inputSchema: spec.inputSchemaZod,\n };\n if (spec.annotations && Object.keys(spec.annotations).length > 0) {\n config.annotations = spec.annotations;\n }\n internal.mcpServer.registerTool(spec.name, config, spec.handler);\n internal.staticToolData.set(spec.name, {\n name: spec.name,\n description: spec.description,\n inputSchema: spec.inputSchemaJson,\n ...(spec.annotations && Object.keys(spec.annotations).length > 0\n ? { annotations: spec.annotations }\n : {}),\n });\n}\n\nfunction normalizeBaseUrl(raw: string): string {\n // `URL` validates the input and re-serializes to the canonical form.\n return new URL(raw).origin;\n}\n\nexport const _testing = {\n CALCULATORS,\n PLAYGROUND,\n asInternal,\n};\n","/**\n * Curated catalogue of Yichus simulation demos.\n *\n * GENERATED from deploy/manifest.json by `yichus deploy --emit-calculators`.\n * Do not edit the CALCULATORS array by hand: the manifest row that deploys\n * a calculator is also its catalog entry, so the two cannot drift.\n */\n\nexport interface Calculator {\n /** Stable short identifier used by `yichus_open_calculator`. */\n readonly slug: string;\n /** Human-readable title that matches the page `<title>`. */\n readonly title: string;\n /** One-line description suitable for an agent to choose between calculators. */\n readonly description: string;\n /** Path under the deploy base URL (no leading slash). */\n readonly path: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://yich.us';\n\nexport const CALCULATORS: readonly Calculator[] = [\n {\n slug: 'event-budget',\n title: 'Event budget',\n description: 'Ticket income, event costs, and the amount left over. Change attendance or adapt the cost rules.',\n path: 'demos/simulation/event-budget.html',\n },\n {\n slug: 'rainwater',\n title: 'Rainwater tank',\n description: 'How much water does one rainfall event collect, and how much overflows the tank?',\n path: 'demos/simulation/rainwater.html',\n },\n {\n slug: 'battery-runtime',\n title: 'Battery runtime',\n description: 'Estimate runtime from a battery energy budget, a constant load, and a usable-capacity assumption.',\n path: 'demos/simulation/battery-runtime.html',\n },\n {\n slug: 'recipe-scale',\n title: 'Scale a recipe',\n description: 'Scale ingredient weights to a different number of servings. Replace the ingredients with your own.',\n path: 'demos/simulation/recipe-scale.html',\n },\n {\n slug: 'download-time',\n title: 'Download time',\n description: 'Convert file size and connection speed into transfer time, allowing for a chosen efficiency.',\n path: 'demos/simulation/download-time.html',\n },\n {\n slug: 'loan',\n title: 'Loan Calculator',\n description: 'Fixed monthly payment and total interest for a loan repaid in equal monthly installments.',\n path: 'demos/simulation/loan-calculator.html',\n },\n {\n slug: 'investment',\n title: 'Investment Growth Calculator',\n description: 'Compound growth of a recurring investment under configurable return assumptions.',\n path: 'demos/simulation/investment.html',\n },\n {\n slug: 'tax',\n title: 'Tax Calculator',\n description: 'Flat-rate example: subtract a deduction and apply one percentage. No tax brackets or jurisdiction-specific rules.',\n path: 'demos/simulation/tax-calculator.html',\n },\n {\n slug: 'carbon',\n title: 'Carbon Footprint Calculator',\n description: 'Illustrative annual CO2 totals from driving, flights, electricity, and gas, using fixed emission factors.',\n path: 'demos/simulation/carbon-footprint.html',\n },\n {\n slug: 'rate',\n title: 'Rate Calculator',\n description: 'Apply a basis-point rate to an amount, then subtract a percentage fee. Inspect both helper calculations in Why.',\n path: 'demos/simulation/rate-calculator.html',\n },\n {\n slug: 'compound-fold',\n title: 'CompoundFold',\n description: 'Compound a starting amount over several periods, increasing the rate by 0.1 percentage points each period.',\n path: 'demos/simulation/compound-fold.html',\n },\n {\n slug: 'closure-capture',\n title: 'ClosureCapture',\n description: 'Scale two amounts with the same input rate captured by a shared inner function.',\n path: 'demos/simulation/closure-capture.html',\n },\n {\n slug: 'lambda-factory',\n title: 'Fee pipeline',\n description: 'Apply processing, platform, and regulatory fees in sequence, with each helper capturing its chosen rate.',\n path: 'demos/simulation/lambda-factory-selector.html',\n },\n {\n slug: 'portfolio',\n title: 'Three growth rates',\n description: 'Split an amount into three equal parts and apply a chosen growth rate to each. This example does not estimate risk.',\n path: 'demos/simulation/portfolio-analysis.html',\n },\n {\n slug: 'strategy-portfolio',\n title: 'Choosing a growth multiplier',\n description: 'Choose between three illustrative growth multipliers. This is a branching example, not an investment-risk model.',\n path: 'demos/simulation/strategy-portfolio.html',\n },\n {\n slug: 'tiered-policy',\n title: 'Claim payout rules',\n description: 'A toy claim payout: choose an incident severity and policy tier, then inspect the adjustments and deduction.',\n path: 'demos/simulation/tiered-policy-engine.html',\n },\n {\n slug: 'what-if-path-switch',\n title: 'WhatIfPathSwitch',\n description: 'Switches computation paths via assumption toggles (\"what-if\" branches).',\n path: 'demos/simulation/what-if-path-switch.html',\n },\n {\n slug: 'deadweight_loss',\n title: 'Deadweight Loss Calculator',\n description: 'Equilibrium, surplus, and deadweight loss of a per-unit tax on a linear market.',\n path: 'demos/simulation/deadweight-loss.html',\n },\n {\n slug: 'trend_projection',\n title: 'Vintage Forecasts',\n description: 'An illustrative series with synthetic forecast vintages. Compare compounding assumptions; these are model-generated values, not agency data.',\n path: 'demos/simulation/trend-projection.html',\n },\n {\n slug: 'carbon_abatement',\n title: 'Carbon Abatement Path',\n description: 'A rising marginal abatement cost path as a sampled curve, with derived cheapest-step and tranche-cost marks.',\n path: 'demos/simulation/carbon-abatement.html',\n },\n] as const;\n\nexport const PLAYGROUND: Calculator = {\n slug: 'playground',\n title: 'Yichus Bosatsu Playground',\n description: 'In-browser editor for Bosatsu simulations with live compile and Why explainer.',\n path: 'editor.html',\n} as const;\n\n/** Look up a curated calculator by its slug, including the playground. */\nexport function findCalculator(slug: string): Calculator | undefined {\n if (slug === PLAYGROUND.slug) return PLAYGROUND;\n return CALCULATORS.find((calc) => calc.slug === slug);\n}\n\n/** Returns every entry exposed by `yichus_list_calculators`. */\nexport function listCalculators(): readonly Calculator[] {\n return [...CALCULATORS, PLAYGROUND];\n}\n\n/** Build the full URL for a calculator at the given base. */\nexport function calculatorUrl(calc: Calculator, baseUrl = DEFAULT_BASE_URL): string {\n const base = new URL(baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);\n return new URL(calc.path, base).toString();\n}\n\n/**\n * Force `?webmcp=1` on a URL so the page loads the WebMCP relay embed.\n *\n * Pages generated by `SimulationGenerator` and `web/editor.html` only inject\n * the relay embed when this flag (or `localStorage.yichus_webmcp = \"on\"`) is\n * present, so an agent that opens a calculator must set it.\n */\nexport function withWebMcpFlag(rawUrl: string): string {\n const url = new URL(rawUrl);\n url.searchParams.set('webmcp', '1');\n return url.toString();\n}\n","import { spawn } from 'node:child_process';\n\n/**\n * Opens a URL in the user's default browser.\n *\n * Uses `URL.href` so shell metacharacters in the original string are escaped\n * before they reach the platform opener. Mirrors the approach the upstream\n * `webmcp-local-relay` `webmcp_open_page` tool uses.\n */\nexport function openUrlInBrowser(rawUrl: string): { command: string; args: string[] } {\n const validated = new URL(rawUrl).href;\n const platform = process.platform;\n\n let command: string;\n let args: string[];\n if (platform === 'darwin') {\n command = 'open';\n args = [validated];\n } else if (platform === 'win32') {\n command = 'powershell';\n args = ['-NoProfile', '-NonInteractive', '-Command', `Start-Process '${validated.replace(/'/g, \"''\")}'`];\n } else {\n command = 'xdg-open';\n args = [validated];\n }\n\n spawn(command, args, { detached: true, stdio: 'ignore' }).unref();\n return { command, args };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server.ts","../src/calculators.ts","../src/openUrl.ts","../src/relay.ts","../src/saveCalculator.ts"],"sourcesContent":["import { LocalRelayMcpServer } from '@mcp-b/webmcp-local-relay';\nimport { z } from 'zod';\nimport {\n CALCULATORS,\n PLAYGROUND,\n calculatorUrl,\n findCalculator,\n listCalculators,\n withWebMcpFlag,\n type Calculator,\n} from './calculators.js';\nimport { openUrlInBrowser, type OpenAttempt } from './openUrl.js';\nimport { CoalescingRelayServer, FirstTabNamingRegistry, PageAnswerBridge } from './relay.js';\nimport {saveCalculator, saveCalculatorInput} from './saveCalculator.js';\n\nconst PACKAGE_NAME = 'yichus-mcp';\n// Bumped via packages/yichus-mcp/package.json. Kept inline so the bin works\n// without reading package.json from disk at startup.\nexport const PACKAGE_VERSION = '0.1.1';\n\nexport interface YichusMcpServerOptions {\n /**\n * Origin of the deployed Yichus instance. The relay rejects browser\n * connections from any other origin and `yichus_open_calculator` builds\n * URLs against this base.\n *\n * @defaultValue `https://yich.us`\n */\n baseUrl?: string;\n\n /**\n * Override the WebMCP local-relay options that get passed straight through\n * to the bundled `LocalRelayMcpServer`. Use this to change the bind port,\n * label, workspace name, or relay id.\n *\n * The page-side embed (`web/webmcp-autocomplete.js`, `ensureRelayEmbed`,\n * and the generated calculator pages) hints the widget a workspace and a\n * relay id, and the widget refuses any relay whose discovery hello\n * differs from either. Every shipped page hints `yichus` for both, so the\n * defaults here are the one agreement; override both sides together.\n */\n bridgeOverrides?: {\n host?: string;\n port?: number;\n label?: string;\n workspace?: string;\n relayId?: string;\n };\n\n /**\n * If `true`, the `yichus_open_calculator` tool returns the URL it would\n * have opened without actually launching a browser process. Used by tests\n * and by anyone running the server in a headless environment.\n *\n * @defaultValue `false`\n */\n disableOpener?: boolean;\n}\n\n/**\n * Construct a fully-wired Yichus MCP server.\n *\n * The returned `LocalRelayMcpServer` already has every default WebMCP relay\n * tool plus Yichus calculator discovery, opening, and saving tools\n * registered. Call `start()` then `startStdio()`\n * (or `connect(transport)`) to bring it online.\n */\nexport function createYichusMcpServer(options: YichusMcpServerOptions = {}): LocalRelayMcpServer {\n const baseUrl = normalizeBaseUrl(options.baseUrl ?? 'https://yich.us');\n const bridgeOverrides = options.bridgeOverrides ?? {};\n\n const bridge = new PageAnswerBridge({\n host: bridgeOverrides.host,\n port: bridgeOverrides.port,\n label: bridgeOverrides.label ?? 'Yichus',\n // The pair every page hints the widget (web/webmcp-autocomplete.js,\n // the generated calculators); the widget rejects any other hello.\n workspace: bridgeOverrides.workspace ?? 'yichus',\n relayId: bridgeOverrides.relayId ?? 'yichus',\n allowedOrigins: [baseUrl],\n // Cold browser compilation can outlast the relay's 25-second default.\n // Keep a deadline so an unresponsive tab still fails explicitly.\n invokeTimeoutMs: 120000,\n }, new FirstTabNamingRegistry());\n const server = new CoalescingRelayServer({\n serverName: PACKAGE_NAME,\n serverVersion: PACKAGE_VERSION,\n bridge,\n }, TOOL_LIST_CHANGED_WINDOW_MS);\n\n registerYichusTools(server, { baseUrl, disableOpener: options.disableOpener ?? false });\n answerRefusalsAsThePageWroteThem(server, bridge);\n return server;\n}\n\n/**\n * How long one tools/list_changed notice waits for the rest of a tab's\n * registration before it goes out.\n */\nexport const TOOL_LIST_CHANGED_WINDOW_MS = 100;\n\n/**\n * Where a tool sits beside the relay's own. `@mcp-b/webmcp-local-relay`\n * has no public way to add one (its `registerStaticTool` is private), so\n * the two fields that method writes are read here, from the exactly pinned\n * version, and checked when the server is made: a release that renames\n * them fails construction instead of an agent's call.\n */\ninterface StaticToolHost {\n mcpServer: {\n _registeredTools: { webmcp_call_tool: { handler: RelayToolHandler } };\n registerTool: (\n name: string,\n config: Record<string, unknown>,\n handler: (args: never) => Promise<unknown> | unknown\n ) => unknown;\n };\n staticToolData: Map<string, { name: string; description?: string; inputSchema: object; annotations?: object }>;\n}\n\ntype RelayToolHandler = (...call: unknown[]) => Promise<unknown>;\nconst isFunction = (field: unknown) => typeof field === 'function';\n\nconst StaticToolFields = z.object({\n mcpServer: z.object({\n registerTool: z.custom(isFunction),\n // The MCP SDK's record of each registered tool; the relay's\n // webmcp_call_tool is wrapped there (answerRefusalsAsThePageWroteThem).\n _registeredTools: z.object({ webmcp_call_tool: z.object({ handler: z.custom(isFunction) }) }),\n }),\n staticToolData: z.instanceof(Map),\n});\nconst StaticToolHost = z.custom<StaticToolHost>(\n (value) => StaticToolFields.safeParse(value).success,\n { error: '@mcp-b/webmcp-local-relay no longer has mcpServer.registerTool, a registered webmcp_call_tool handler and staticToolData' },\n);\n\nfunction registerYichusTools(\n server: LocalRelayMcpServer,\n ctx: { baseUrl: string; disableOpener: boolean }\n): void {\n const host = StaticToolHost.parse(server);\n\n registerStatic(host, {\n name:'yichus_save_calculator',\n description:'Save a browser-compiled example and its Bosatsu sources to a NEW local directory. Pass the public yichus_example_export tool name for the intended tab and its current source revision. Transfers bounded chunks internally and returns file paths, not generated HTML. Does not execute the program. Requires the browser tab to stay open.',\n annotations:{readOnlyHint:false,destructiveHint:false},\n input:saveCalculatorInput,\n handler:async args => {\n try {\n const payload = await saveCalculator(args,(name,params) => server.bridge.invokeTool(name,params));\n return {content:[{type:'text',text:JSON.stringify(payload)}],structuredContent:payload};\n } catch (error) {\n return {isError:true,content:[{type:'text',text:String(error)}]};\n }\n },\n });\n\n registerStatic(host, {\n name: 'yichus_list_calculators',\n description:\n 'List the curated Yichus calculator demos and the playground editor available on the configured Yichus origin. Returns a slug, title, description, and absolute URL for each.',\n annotations: { readOnlyHint: true, idempotentHint: true },\n input: z.object({}),\n handler: async () => {\n const entries = listCalculators().map((calc) => ({\n slug: calc.slug,\n title: calc.title,\n description: calc.description,\n url: calculatorUrl(calc, ctx.baseUrl),\n }));\n const payload = { baseUrl: ctx.baseUrl, calculators: entries };\n return {\n content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],\n structuredContent: payload,\n };\n },\n });\n\n registerStatic(host, {\n name: 'yichus_open_calculator',\n description:\n \"Open a curated Yichus calculator (or the playground) in the user's default browser with WebMCP enabled (?webmcp=1). After opening, the relay tools below will see the page's tools (yichus_list_inputs, yichus_set_inputs, yichus_list_outputs, yichus_explain_output for calculators; yichus_playground_* for the playground). Use yichus_list_calculators to discover slugs.\",\n annotations: { openWorldHint: true },\n input: z.object({\n slug: z.string().describe(\n 'Calculator slug from yichus_list_calculators (for example \"loan\", \"investment\", \"tax\", \"playground\").'\n ),\n }),\n handler: async (args) => {\n const calc: Calculator | undefined = findCalculator(args.slug);\n if (!calc) {\n const known = listCalculators().map((c) => c.slug).join(', ');\n return {\n content: [\n {\n type: 'text',\n text: `Unknown calculator slug \"${args.slug}\". Known slugs: ${known}.`,\n },\n ],\n isError: true,\n };\n }\n const url = withWebMcpFlag(calculatorUrl(calc, ctx.baseUrl));\n const attempt: OpenAttempt | null = ctx.disableOpener ? null : await openUrlInBrowser(url);\n const payload = {\n calculator: {\n slug: calc.slug,\n title: calc.title,\n description: calc.description,\n },\n url,\n opened: attempt?.opened ?? false,\n opener: attempt && { command: attempt.command, args: attempt.args },\n ...(attempt?.error === undefined ? {} : { openerError: attempt.error }),\n };\n return {\n content: [{ type: 'text', text: openAnswer(calc, url, attempt) }],\n structuredContent: payload,\n };\n },\n });\n}\n\n/** The sentence an agent relays: what happened, and the link to open by hand when no browser was opened. */\nfunction openAnswer(calc: Calculator, url: string, attempt: OpenAttempt | null): string {\n if (attempt === null) return `Would open ${calc.title} at ${url} (browser opener disabled).`;\n if (attempt.opened) return `Opening ${calc.title} at ${url}.`;\n return `Could not open a browser on this machine (${attempt.error}). ` +\n `Open this link in a browser yourself to load ${calc.title}: ${url}`;\n}\n\n/**\n * Hands a page's refusal to the agent as the page wrote it.\n *\n * The relay's `webmcp_call_tool` adds its tool catalog to every page\n * answer. On a result that worked that is a reminder of what else can be\n * called; on a refusal (\"Loan Principal ($) (principal): 5000000 is above\n * the maximum 1000000 ...\") it buried the one sentence the agent had to\n * relay. The bridge kept the content each page sent, keyed by the answer\n * the relay extends; a refusal is answered with that content alone. The\n * relay's own errors (an unknown tool name, a page that never answered) are\n * not page answers and keep the catalog, which lists the names to use.\n */\nfunction answerRefusalsAsThePageWroteThem(server: LocalRelayMcpServer, bridge: PageAnswerBridge): void {\n const callTool = StaticToolHost.parse(server).mcpServer._registeredTools.webmcp_call_tool;\n const relayHandler = callTool.handler;\n callTool.handler = async (...call) => {\n const result = (await relayHandler(...call)) as { isError?: boolean };\n const page = bridge.pageAnswer(result);\n return result.isError === true && page !== undefined ? { ...result, content: page } : result;\n };\n}\n\ninterface StaticToolSpec<Input extends z.ZodObject> {\n name: string;\n description: string;\n annotations: Record<string, unknown>;\n input: Input;\n handler: (args: z.infer<Input>) => Promise<unknown> | unknown;\n}\n\n// The MCP SDK validates calls against the input's shape; agents read the\n// JSON Schema Zod derives from the same input.\nfunction registerStatic<Input extends z.ZodObject>(host: StaticToolHost, spec: StaticToolSpec<Input>): void {\n const { name, description, annotations, input, handler } = spec;\n host.mcpServer.registerTool(name, { description, inputSchema: input.shape, annotations }, handler);\n host.staticToolData.set(name, {\n name,\n description,\n inputSchema: z.toJSONSchema(input, { unrepresentable: 'any' }),\n annotations,\n });\n}\n\nfunction normalizeBaseUrl(raw: string): string {\n // `URL` validates the input and re-serializes to the canonical form.\n return new URL(raw).origin;\n}\n\nexport const _testing = {\n CALCULATORS,\n PLAYGROUND,\n};\n","/**\n * Curated catalogue of Yichus simulation demos.\n *\n * GENERATED from deploy/manifest.json by `yichus deploy --emit-calculators`.\n * Do not edit the CALCULATORS array by hand: the manifest row that deploys\n * a calculator is also its catalog entry, so the two cannot drift.\n */\n\nexport interface Calculator {\n /** Stable short identifier used by `yichus_open_calculator`. */\n readonly slug: string;\n /** Human-readable title that matches the page `<title>`. */\n readonly title: string;\n /** One-line description suitable for an agent to choose between calculators. */\n readonly description: string;\n /** Path under the deploy base URL (no leading slash). */\n readonly path: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://yich.us';\n\nexport const CALCULATORS: readonly Calculator[] = [\n {\n slug: 'event-budget',\n title: 'Event budget',\n description: 'Ticket income, event costs, and the amount left over. Change attendance or adapt the cost rules.',\n path: 'demos/simulation/event-budget.html',\n },\n {\n slug: 'rainwater',\n title: 'Rainwater tank',\n description: 'How much water does one rainfall event collect, and how much overflows the tank?',\n path: 'demos/simulation/rainwater.html',\n },\n {\n slug: 'rainwater-history',\n title: 'Rainwater through a dry spell',\n description: 'Run NOAA weather observations through an inspectable daily tank model. Change capacity and demand, inspect captured data, and share a saved report.',\n path: 'demos/simulation/rainwater-history.html',\n },\n {\n slug: 'battery-runtime',\n title: 'Battery runtime',\n description: 'Estimate runtime from a battery energy budget, a constant load, and a usable-capacity assumption.',\n path: 'demos/simulation/battery-runtime.html',\n },\n {\n slug: 'recipe-scale',\n title: 'Scale a recipe',\n description: 'Scale ingredient weights to a different number of servings. Replace the ingredients with your own.',\n path: 'demos/simulation/recipe-scale.html',\n },\n {\n slug: 'download-time',\n title: 'Download time',\n description: 'Convert file size and connection speed into transfer time, allowing for a chosen efficiency.',\n path: 'demos/simulation/download-time.html',\n },\n {\n slug: 'loan',\n title: 'Loan Calculator',\n description: 'Fixed monthly payment and total interest for a loan repaid in equal monthly installments.',\n path: 'demos/simulation/loan-calculator.html',\n },\n {\n slug: 'investment',\n title: 'Investment Growth Calculator',\n description: 'Compound growth of a recurring investment under configurable return assumptions.',\n path: 'demos/simulation/investment.html',\n },\n {\n slug: 'tax',\n title: 'Tax Calculator',\n description: 'Flat-rate example: subtract a deduction and apply one percentage. No tax brackets or jurisdiction-specific rules.',\n path: 'demos/simulation/tax-calculator.html',\n },\n {\n slug: 'carbon',\n title: 'Carbon Footprint Calculator',\n description: 'Illustrative annual CO2 totals from driving, flights, electricity, and gas, using fixed emission factors.',\n path: 'demos/simulation/carbon-footprint.html',\n },\n {\n slug: 'rate',\n title: 'Rate Calculator',\n description: 'Apply a basis-point rate to an amount, then subtract a percentage fee. Inspect both helper calculations in Why.',\n path: 'demos/simulation/rate-calculator.html',\n },\n {\n slug: 'compound-fold',\n title: 'CompoundFold',\n description: 'Compound a starting amount over several periods, increasing the rate by 0.1 percentage points each period.',\n path: 'demos/simulation/compound-fold.html',\n },\n {\n slug: 'closure-capture',\n title: 'ClosureCapture',\n description: 'Scale two amounts with the same input rate captured by a shared inner function.',\n path: 'demos/simulation/closure-capture.html',\n },\n {\n slug: 'lambda-factory',\n title: 'Fee pipeline',\n description: 'Apply processing, platform, and regulatory fees in sequence, with each helper capturing its chosen rate.',\n path: 'demos/simulation/lambda-factory-selector.html',\n },\n {\n slug: 'portfolio',\n title: 'Three growth rates',\n description: 'Split an amount into three equal parts and apply a chosen growth rate to each. This example does not estimate risk.',\n path: 'demos/simulation/portfolio-analysis.html',\n },\n {\n slug: 'strategy-portfolio',\n title: 'Choosing a growth multiplier',\n description: 'Choose between three illustrative growth multipliers. This is a branching example, not an investment-risk model.',\n path: 'demos/simulation/strategy-portfolio.html',\n },\n {\n slug: 'tiered-policy',\n title: 'Claim payout rules',\n description: 'A toy claim payout: choose an incident severity and policy tier, then inspect the adjustments and deduction.',\n path: 'demos/simulation/tiered-policy-engine.html',\n },\n {\n slug: 'what-if-path-switch',\n title: 'WhatIfPathSwitch',\n description: 'Switches computation paths via assumption toggles (\"what-if\" branches).',\n path: 'demos/simulation/what-if-path-switch.html',\n },\n {\n slug: 'deadweight_loss',\n title: 'Deadweight Loss Calculator',\n description: 'Equilibrium, surplus, and deadweight loss of a per-unit tax on a linear market.',\n path: 'demos/simulation/deadweight-loss.html',\n },\n {\n slug: 'trend_projection',\n title: 'Vintage Forecasts',\n description: 'An illustrative series with synthetic forecast vintages. Compare compounding assumptions; these are model-generated values, not agency data.',\n path: 'demos/simulation/trend-projection.html',\n },\n {\n slug: 'carbon_abatement',\n title: 'Carbon Abatement Path',\n description: 'A rising marginal abatement cost path as a sampled curve, with derived cheapest-step and tranche-cost marks.',\n path: 'demos/simulation/carbon-abatement.html',\n },\n] as const;\n\nexport const PLAYGROUND: Calculator = {\n slug: 'playground',\n title: 'Yichus Bosatsu Playground',\n description: 'In-browser editor for Bosatsu simulations with live compile and Why explainer.',\n path: 'editor.html',\n} as const;\n\n/** Look up a curated calculator by its slug, including the playground. */\nexport function findCalculator(slug: string): Calculator | undefined {\n if (slug === PLAYGROUND.slug) return PLAYGROUND;\n return CALCULATORS.find((calc) => calc.slug === slug);\n}\n\n/** Returns every entry exposed by `yichus_list_calculators`. */\nexport function listCalculators(): readonly Calculator[] {\n return [...CALCULATORS, PLAYGROUND];\n}\n\n/** Build the full URL for a calculator at the given base. */\nexport function calculatorUrl(calc: Calculator, baseUrl = DEFAULT_BASE_URL): string {\n const base = new URL(baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`);\n return new URL(calc.path, base).toString();\n}\n\n/**\n * Force `?webmcp=1` on a URL so the page loads the WebMCP relay embed.\n *\n * Pages generated by `SimulationGenerator` and `web/editor.html` only inject\n * the relay embed when this flag (or `localStorage.yichus_webmcp = \"on\"`) is\n * present, so an agent that opens a calculator must set it.\n */\nexport function withWebMcpFlag(rawUrl: string): string {\n const url = new URL(rawUrl);\n url.searchParams.set('webmcp', '1');\n return url.toString();\n}\n","import { spawn } from 'node:child_process';\n\n/** What happened when the relay asked the platform opener for a URL. */\nexport interface OpenAttempt {\n command: string;\n args: string[];\n /** True once the opener process started; false when it could not be started. */\n opened: boolean;\n /** Why the opener could not be started (for example `spawn xdg-open ENOENT`). */\n error?: string;\n}\n\n/**\n * Opens a URL in the user's default browser.\n *\n * Uses `URL.href` so shell metacharacters in the original string are escaped\n * before they reach the platform opener. Mirrors the approach the upstream\n * `webmcp-local-relay` `webmcp_open_page` tool uses.\n *\n * Resolves only once the opener has started or failed to start, so the\n * answer reports what happened. A machine with no opener (a container, a\n * headless box) answers `opened: false` instead of an unhandled `error`\n * event, which would end the relay and every tool with it.\n */\nexport function openUrlInBrowser(rawUrl: string): Promise<OpenAttempt> {\n const validated = new URL(rawUrl).href;\n const platform = process.platform;\n\n let command: string;\n let args: string[];\n if (platform === 'darwin') {\n command = 'open';\n args = [validated];\n } else if (platform === 'win32') {\n command = 'powershell';\n args = ['-NoProfile', '-NonInteractive', '-Command', `Start-Process '${validated.replace(/'/g, \"''\")}'`];\n } else {\n command = 'xdg-open';\n args = [validated];\n }\n\n return new Promise((resolve) => {\n const child = spawn(command, args, { detached: true, stdio: 'ignore' });\n child.once('spawn', () => {\n child.unref();\n resolve({ command, args, opened: true });\n });\n // Node emits a failed start as an 'error' event; with no listener it\n // is thrown and ends the process.\n child.on('error', (error) => resolve({ command, args, opened: false, error: error.message }));\n });\n}\n","import {\n LocalRelayMcpServer,\n RelayBridgeServer,\n RelayRegistry,\n buildPublicToolName,\n type AggregatedTool,\n type RelayCallToolResult,\n type ResolvedInvocation,\n} from '@mcp-b/webmcp-local-relay';\n\ntype Hello = Parameters<RelayRegistry['upsertSource']>[1];\ntype InvocationQuery = Parameters<RelayRegistry['resolveInvocation']>[0];\ntype Transport = Parameters<LocalRelayMcpServer['connect']>[0];\ntype Message = Parameters<Transport['send']>[0];\ntype SendOptions = Parameters<Transport['send']>[1];\n\n/**\n * How many tab ids the registry remembers once their tabs are gone. A tab\n * that comes back while remembered (a reload) keeps its place; one that\n * returns after this many others is ordered as a new tab.\n */\nexport const REMEMBERED_TABS = 256;\n\n/**\n * Keeps the name an agent is already using when a second tab publishes the\n * same tool.\n *\n * The relay's own registry renames every copy of a tool once two tabs\n * publish it (`yichus_page_feedback` becomes `yichus_page_feedback_494e`\n * and `yichus_page_feedback_769c`), so opening a second site page broke the\n * name the agent had just called on the first. Here the tab the relay saw\n * first keeps the plain name and only later tabs keep the relay's tab-id\n * suffix. \"First\" is by tab id, which the embed keeps in the tab's\n * sessionStorage, so a reload of the first tab keeps its name too; when\n * that tab closes, the next-oldest remaining tab takes the plain name.\n *\n * Only the registry's public methods are refined: the relay still names,\n * ranks and resolves every tool, and the names agents see and call are\n * mapped onto its names in both directions.\n */\nexport class FirstTabNamingRegistry extends RelayRegistry {\n private readonly firstSeen = new Map<string, number>();\n private readonly tabByConnection = new Map<string, string>();\n private seen = 0;\n\n override upsertSource(connectionId: string, hello: Hello): void {\n super.upsertSource(connectionId, hello);\n this.tabByConnection.set(connectionId, hello.tabId);\n this.remember(hello.tabId);\n }\n\n override removeConnection(connectionId: string): void {\n super.removeConnection(connectionId);\n this.tabByConnection.delete(connectionId);\n }\n\n override listTools(): AggregatedTool[] {\n return this.named()\n .map(({ tool }) => tool)\n .sort((a, b) => a.name.localeCompare(b.name));\n }\n\n override resolveInvocation(options: InvocationQuery): ResolvedInvocation | null {\n const entry = this.named().find(({ tool }) => tool.name === options.toolName);\n if (!entry) return null;\n const resolved = super.resolveInvocation({ ...options, toolName: entry.relayName });\n return resolved && { ...resolved, publicToolName: entry.tool.name };\n }\n\n // Each relay tool with the name an agent sees: the plain name for the\n // copy published by the first-seen tab among those publishing it.\n private named(): { relayName: string; tool: AggregatedTool }[] {\n const tools = super.listTools();\n const owners = new Map<string, number>();\n for (const tool of tools) {\n for (const source of tool.sources) {\n const order = this.order(source.tabId);\n owners.set(tool.originalName, Math.min(owners.get(tool.originalName) ?? order, order));\n }\n }\n return tools.map((tool) => {\n const owned = tool.sources.some((source) => this.order(source.tabId) === owners.get(tool.originalName));\n const name = owned ? buildPublicToolName({ originalToolName: tool.originalName }) : tool.name;\n return { relayName: tool.name, tool: { ...tool, name } };\n });\n }\n\n private order(tabId: string): number {\n return this.firstSeen.get(tabId) ?? Number.POSITIVE_INFINITY;\n }\n\n private remember(tabId: string): void {\n if (this.firstSeen.has(tabId)) return;\n this.firstSeen.set(tabId, this.seen++);\n const open = new Set(this.tabByConnection.values());\n for (const tab of this.firstSeen.keys()) {\n if (this.firstSeen.size - open.size <= REMEMBERED_TABS) break;\n if (!open.has(tab)) this.firstSeen.delete(tab);\n }\n }\n}\n\nconst TOOL_LIST_CHANGED = 'notifications/tools/list_changed';\n\nconst isToolListChanged = (message: Message): boolean =>\n 'method' in message && message.method === TOOL_LIST_CHANGED && !('id' in message);\n\n/**\n * A transport that sends one `notifications/tools/list_changed` per burst.\n *\n * The relay mirrors each page tool as its own MCP tool, and the MCP SDK\n * sends a list_changed notice on every `registerTool` and every `remove()`;\n * the relay then sends one more of its own after each sync. A tab\n * publishing 12 tools therefore cost the agent 13 notices, and a reload (12\n * removals, 12 registrations) 25 or more, each of which many hosts answer\n * by re-reading the whole tool list. Here the first notice of a burst opens\n * a window of `windowMs` that the rest join; when it ends a single notice\n * goes out, after every registration in it has landed, so the tools/list\n * the agent sends in reply sees all of them. Closing drops a pending\n * notice. Every other message passes straight through.\n */\nexport class ToolListChangedCoalescer implements Transport {\n onclose?: Transport['onclose'];\n onerror?: Transport['onerror'];\n onmessage?: Transport['onmessage'];\n private pending: ReturnType<typeof setTimeout> | null = null;\n\n constructor(private readonly inner: Transport, private readonly windowMs: number) {\n inner.onmessage = (message, extra) => this.onmessage?.(message, extra);\n inner.onerror = (error) => this.onerror?.(error);\n inner.onclose = () => {\n this.drop();\n this.onclose?.();\n };\n }\n\n get sessionId(): string | undefined {\n return this.inner.sessionId;\n }\n\n setProtocolVersion(version: string): void {\n this.inner.setProtocolVersion?.(version);\n }\n\n start(): Promise<void> {\n return this.inner.start();\n }\n\n async send(message: Message, options?: SendOptions): Promise<void> {\n if (!isToolListChanged(message)) return this.inner.send(message, options);\n if (this.pending) return;\n this.pending = setTimeout(() => {\n this.pending = null;\n this.inner.send(message, options).catch((error: Error) => this.onerror?.(error));\n }, this.windowMs);\n this.pending.unref?.();\n }\n\n close(): Promise<void> {\n this.drop();\n return this.inner.close();\n }\n\n private drop(): void {\n if (this.pending) clearTimeout(this.pending);\n this.pending = null;\n }\n}\n\n/** The relay's MCP server, with list_changed notices coalesced on any transport it connects to. */\nexport class CoalescingRelayServer extends LocalRelayMcpServer {\n constructor(\n options: ConstructorParameters<typeof LocalRelayMcpServer>[0],\n private readonly windowMs: number,\n ) {\n super(options);\n }\n\n override connect(transport: Transport): Promise<void> {\n return super.connect(new ToolListChangedCoalescer(transport, this.windowMs));\n }\n}\n\n/**\n * A bridge that remembers what each page answered.\n *\n * The relay's `webmcp_call_tool` appends its whole tool catalog to a page's\n * answer by replacing the answer's content, the page's own refusals\n * included: \"Count (count): 100 is above the maximum 10\" arrived followed by\n * a page of tool descriptions. Here the content the page sent is kept,\n * keyed by the answer object the relay goes on to extend, so a refusal can\n * be handed to the agent exactly as the page wrote it\n * (`answerRefusalsAsThePageWroteThem` in server.ts).\n */\nexport class PageAnswerBridge extends RelayBridgeServer {\n private readonly pageContent = new WeakMap<object, RelayCallToolResult['content']>();\n\n override async invokeTool(...call: Parameters<RelayBridgeServer['invokeTool']>): Promise<RelayCallToolResult> {\n const result = await super.invokeTool(...call);\n this.pageContent.set(result, result.content);\n return result;\n }\n\n /** The content a page answered with, for an answer this bridge carried. */\n pageAnswer(result: object): RelayCallToolResult['content'] | undefined {\n return this.pageContent.get(result);\n }\n}\n","import {mkdir, writeFile} from 'node:fs/promises';\nimport {isAbsolute, join} from 'node:path';\nimport {z} from 'zod';\n\nexport const saveCalculatorInput = z.object({\n exportToolName: z.string().regex(/^yichus_example_export(?:_[a-zA-Z0-9_-]+)?$/).describe('Public export tool name from webmcp_list_tools, matched to the intended tab.'),\n exampleId: z.string().min(1),\n expectedRevision: z.number().int().nonnegative(),\n outputDirectory: z.string().refine(isAbsolute, 'Use an absolute output directory.').describe('A new directory under an existing parent. Existing directories are refused; files are never overwritten.'),\n});\n\nconst chunkSchema = z.object({\n exampleId: z.string(), revision: z.number().int(), exportId: z.string().min(1), assetBaseUrl: z.url(),\n sources: z.array(z.object({fileName:z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.bosatsu$/), source:z.string().max(1000000)})).min(1).max(16),\n html: z.object({fileName:z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\\.html$/), content:z.string().min(1).max(32768),\n offset:z.number().int().nonnegative(), totalLength:z.number().int().positive().max(32*1024*1024), nextOffset:z.number().int().nonnegative().nullable()}),\n});\ntype Chunk = z.infer<typeof chunkSchema>;\ntype Invoke = (name:string, args:Record<string,unknown>) => Promise<unknown>;\n\n/** Transfer bytes through the relay without putting generated HTML in model context.\n * Validate the complete export before creating a directory. Chunks belong to one\n * compilation, and only its actual generated files are written (no execution).\n */\nexport async function saveCalculator(rawArgs:unknown, invoke:Invoke) {\n const args = saveCalculatorInput.parse(rawArgs);\n const content: string[] = [];\n let first: Chunk | undefined;\n let offset = 0;\n for (;;) {\n const response = z.object({isError:z.boolean().optional(),structuredContent:z.unknown().optional()}).parse(await invoke(args.exportToolName, {\n exampleId:args.exampleId, expectedRevision:args.expectedRevision, offset,\n ...(first ? {expectedExportId:first.exportId} : {}),\n }));\n if (response.isError) throw new Error('Browser export failed. Read the current example feedback before retrying.');\n const chunk = chunkSchema.parse(response.structuredContent);\n if (chunk.exampleId !== args.exampleId || chunk.revision !== args.expectedRevision || chunk.html.offset !== offset) throw new Error('Export does not match the requested example, revision, or offset.');\n if (first && (chunk.exportId !== first.exportId || chunk.assetBaseUrl !== first.assetBaseUrl || chunk.html.fileName !== first.html.fileName || chunk.html.totalLength !== first.html.totalLength || JSON.stringify(chunk.sources) !== JSON.stringify(first.sources))) throw new Error('Export changed during transfer. Nothing was saved; start again.');\n first ??= chunk;\n const end = offset + chunk.html.content.length;\n if (end > chunk.html.totalLength || (chunk.html.nextOffset === null ? end !== chunk.html.totalLength : chunk.html.nextOffset !== end || end >= chunk.html.totalLength)) throw new Error('Incomplete or inconsistent export chunk. Nothing was saved.');\n content.push(chunk.html.content);\n if (chunk.html.nextOffset === null) break;\n offset = chunk.html.nextOffset;\n }\n const files = [...first.sources.map(s => ({fileName:s.fileName,content:s.source})),{fileName:first.html.fileName,content:content.join('')}];\n if (new Set(files.map(f => f.fileName.toLowerCase())).size !== files.length) throw new Error('Export file names must be unique.');\n await mkdir(args.outputDirectory);\n for (const file of files) await writeFile(join(args.outputDirectory,file.fileName),file.content,{encoding:'utf8',flag:'wx'});\n return {ok:true,directory:args.outputDirectory,revision:first.revision,exportId:first.exportId,assetBaseUrl:first.assetBaseUrl,\n files:files.map(f => ({fileName:f.fileName,path:join(args.outputDirectory,f.fileName),bytes:Buffer.byteLength(f.content)})),\n htmlPath:join(args.outputDirectory,first.html.fileName),\n note:'The HTML starts from configured defaults and loads supporting assets from the site. It requires network access. No program was executed.',\n };\n}\n"],"mappings":";AACA,SAAS,KAAAA,UAAS;;;ACkBlB,IAAM,mBAAmB;AAElB,IAAM,cAAqC;AAAA,EAChD;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AACF;AAEO,IAAM,aAAyB;AAAA,EACpC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,MAAM;AACR;AAGO,SAAS,eAAe,MAAsC;AACnE,MAAI,SAAS,WAAW,KAAM,QAAO;AACrC,SAAO,YAAY,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI;AACtD;AAGO,SAAS,kBAAyC;AACvD,SAAO,CAAC,GAAG,aAAa,UAAU;AACpC;AAGO,SAAS,cAAc,MAAkB,UAAU,kBAA0B;AAClF,QAAM,OAAO,IAAI,IAAI,QAAQ,SAAS,GAAG,IAAI,UAAU,GAAG,OAAO,GAAG;AACpE,SAAO,IAAI,IAAI,KAAK,MAAM,IAAI,EAAE,SAAS;AAC3C;AASO,SAAS,eAAe,QAAwB;AACrD,QAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,MAAI,aAAa,IAAI,UAAU,GAAG;AAClC,SAAO,IAAI,SAAS;AACtB;;;ACzLA,SAAS,aAAa;AAwBf,SAAS,iBAAiB,QAAsC;AACrE,QAAM,YAAY,IAAI,IAAI,MAAM,EAAE;AAClC,QAAM,WAAW,QAAQ;AAEzB,MAAI;AACJ,MAAI;AACJ,MAAI,aAAa,UAAU;AACzB,cAAU;AACV,WAAO,CAAC,SAAS;AAAA,EACnB,WAAW,aAAa,SAAS;AAC/B,cAAU;AACV,WAAO,CAAC,cAAc,mBAAmB,YAAY,kBAAkB,UAAU,QAAQ,MAAM,IAAI,CAAC,GAAG;AAAA,EACzG,OAAO;AACL,cAAU;AACV,WAAO,CAAC,SAAS;AAAA,EACnB;AAEA,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,QAAQ,MAAM,SAAS,MAAM,EAAE,UAAU,MAAM,OAAO,SAAS,CAAC;AACtE,UAAM,KAAK,SAAS,MAAM;AACxB,YAAM,MAAM;AACZ,cAAQ,EAAE,SAAS,MAAM,QAAQ,KAAK,CAAC;AAAA,IACzC,CAAC;AAGD,UAAM,GAAG,SAAS,CAAC,UAAU,QAAQ,EAAE,SAAS,MAAM,QAAQ,OAAO,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EAC9F,CAAC;AACH;;;ACnDA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAaA,IAAM,kBAAkB;AAmBxB,IAAM,yBAAN,cAAqC,cAAc;AAAA,EACvC,YAAY,oBAAI,IAAoB;AAAA,EACpC,kBAAkB,oBAAI,IAAoB;AAAA,EACnD,OAAO;AAAA,EAEN,aAAa,cAAsB,OAAoB;AAC9D,UAAM,aAAa,cAAc,KAAK;AACtC,SAAK,gBAAgB,IAAI,cAAc,MAAM,KAAK;AAClD,SAAK,SAAS,MAAM,KAAK;AAAA,EAC3B;AAAA,EAES,iBAAiB,cAA4B;AACpD,UAAM,iBAAiB,YAAY;AACnC,SAAK,gBAAgB,OAAO,YAAY;AAAA,EAC1C;AAAA,EAES,YAA8B;AACrC,WAAO,KAAK,MAAM,EACf,IAAI,CAAC,EAAE,KAAK,MAAM,IAAI,EACtB,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAAA,EAChD;AAAA,EAES,kBAAkB,SAAqD;AAC9E,UAAM,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,MAAM,KAAK,SAAS,QAAQ,QAAQ;AAC5E,QAAI,CAAC,MAAO,QAAO;AACnB,UAAM,WAAW,MAAM,kBAAkB,EAAE,GAAG,SAAS,UAAU,MAAM,UAAU,CAAC;AAClF,WAAO,YAAY,EAAE,GAAG,UAAU,gBAAgB,MAAM,KAAK,KAAK;AAAA,EACpE;AAAA;AAAA;AAAA,EAIQ,QAAuD;AAC7D,UAAM,QAAQ,MAAM,UAAU;AAC9B,UAAM,SAAS,oBAAI,IAAoB;AACvC,eAAW,QAAQ,OAAO;AACxB,iBAAW,UAAU,KAAK,SAAS;AACjC,cAAM,QAAQ,KAAK,MAAM,OAAO,KAAK;AACrC,eAAO,IAAI,KAAK,cAAc,KAAK,IAAI,OAAO,IAAI,KAAK,YAAY,KAAK,OAAO,KAAK,CAAC;AAAA,MACvF;AAAA,IACF;AACA,WAAO,MAAM,IAAI,CAAC,SAAS;AACzB,YAAM,QAAQ,KAAK,QAAQ,KAAK,CAAC,WAAW,KAAK,MAAM,OAAO,KAAK,MAAM,OAAO,IAAI,KAAK,YAAY,CAAC;AACtG,YAAM,OAAO,QAAQ,oBAAoB,EAAE,kBAAkB,KAAK,aAAa,CAAC,IAAI,KAAK;AACzF,aAAO,EAAE,WAAW,KAAK,MAAM,MAAM,EAAE,GAAG,MAAM,KAAK,EAAE;AAAA,IACzD,CAAC;AAAA,EACH;AAAA,EAEQ,MAAM,OAAuB;AACnC,WAAO,KAAK,UAAU,IAAI,KAAK,KAAK,OAAO;AAAA,EAC7C;AAAA,EAEQ,SAAS,OAAqB;AACpC,QAAI,KAAK,UAAU,IAAI,KAAK,EAAG;AAC/B,SAAK,UAAU,IAAI,OAAO,KAAK,MAAM;AACrC,UAAM,OAAO,IAAI,IAAI,KAAK,gBAAgB,OAAO,CAAC;AAClD,eAAW,OAAO,KAAK,UAAU,KAAK,GAAG;AACvC,UAAI,KAAK,UAAU,OAAO,KAAK,QAAQ,gBAAiB;AACxD,UAAI,CAAC,KAAK,IAAI,GAAG,EAAG,MAAK,UAAU,OAAO,GAAG;AAAA,IAC/C;AAAA,EACF;AACF;AAEA,IAAM,oBAAoB;AAE1B,IAAM,oBAAoB,CAAC,YACzB,YAAY,WAAW,QAAQ,WAAW,qBAAqB,EAAE,QAAQ;AAgBpE,IAAM,2BAAN,MAAoD;AAAA,EAMzD,YAA6B,OAAmC,UAAkB;AAArD;AAAmC;AAC9D,UAAM,YAAY,CAAC,SAAS,UAAU,KAAK,YAAY,SAAS,KAAK;AACrE,UAAM,UAAU,CAAC,UAAU,KAAK,UAAU,KAAK;AAC/C,UAAM,UAAU,MAAM;AACpB,WAAK,KAAK;AACV,WAAK,UAAU;AAAA,IACjB;AAAA,EACF;AAAA,EAP6B;AAAA,EAAmC;AAAA,EALhE;AAAA,EACA;AAAA,EACA;AAAA,EACQ,UAAgD;AAAA,EAWxD,IAAI,YAAgC;AAClC,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEA,mBAAmB,SAAuB;AACxC,SAAK,MAAM,qBAAqB,OAAO;AAAA,EACzC;AAAA,EAEA,QAAuB;AACrB,WAAO,KAAK,MAAM,MAAM;AAAA,EAC1B;AAAA,EAEA,MAAM,KAAK,SAAkB,SAAsC;AACjE,QAAI,CAAC,kBAAkB,OAAO,EAAG,QAAO,KAAK,MAAM,KAAK,SAAS,OAAO;AACxE,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU,WAAW,MAAM;AAC9B,WAAK,UAAU;AACf,WAAK,MAAM,KAAK,SAAS,OAAO,EAAE,MAAM,CAAC,UAAiB,KAAK,UAAU,KAAK,CAAC;AAAA,IACjF,GAAG,KAAK,QAAQ;AAChB,SAAK,QAAQ,QAAQ;AAAA,EACvB;AAAA,EAEA,QAAuB;AACrB,SAAK,KAAK;AACV,WAAO,KAAK,MAAM,MAAM;AAAA,EAC1B;AAAA,EAEQ,OAAa;AACnB,QAAI,KAAK,QAAS,cAAa,KAAK,OAAO;AAC3C,SAAK,UAAU;AAAA,EACjB;AACF;AAGO,IAAM,wBAAN,cAAoC,oBAAoB;AAAA,EAC7D,YACE,SACiB,UACjB;AACA,UAAM,OAAO;AAFI;AAAA,EAGnB;AAAA,EAHmB;AAAA,EAKV,QAAQ,WAAqC;AACpD,WAAO,MAAM,QAAQ,IAAI,yBAAyB,WAAW,KAAK,QAAQ,CAAC;AAAA,EAC7E;AACF;AAaO,IAAM,mBAAN,cAA+B,kBAAkB;AAAA,EACrC,cAAc,oBAAI,QAAgD;AAAA,EAEnF,MAAe,cAAc,MAAiF;AAC5G,UAAM,SAAS,MAAM,MAAM,WAAW,GAAG,IAAI;AAC7C,SAAK,YAAY,IAAI,QAAQ,OAAO,OAAO;AAC3C,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,WAAW,QAA4D;AACrE,WAAO,KAAK,YAAY,IAAI,MAAM;AAAA,EACpC;AACF;;;AC/MA,SAAQ,OAAO,iBAAgB;AAC/B,SAAQ,YAAY,YAAW;AAC/B,SAAQ,SAAQ;AAET,IAAM,sBAAsB,EAAE,OAAO;AAAA,EAC1C,gBAAgB,EAAE,OAAO,EAAE,MAAM,6CAA6C,EAAE,SAAS,8EAA8E;AAAA,EACvK,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,kBAAkB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC/C,iBAAiB,EAAE,OAAO,EAAE,OAAO,YAAY,mCAAmC,EAAE,SAAS,0GAA0G;AACzM,CAAC;AAED,IAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,WAAW,EAAE,OAAO;AAAA,EAAG,UAAU,EAAE,OAAO,EAAE,IAAI;AAAA,EAAG,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAAG,cAAc,EAAE,IAAI;AAAA,EACpG,SAAS,EAAE,MAAM,EAAE,OAAO,EAAC,UAAS,EAAE,OAAO,EAAE,MAAM,uCAAuC,GAAG,QAAO,EAAE,OAAO,EAAE,IAAI,GAAO,EAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EAC9I,MAAM,EAAE,OAAO;AAAA,IAAC,UAAS,EAAE,OAAO,EAAE,MAAM,oCAAoC;AAAA,IAAG,SAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,KAAK;AAAA,IAClH,QAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IAAG,aAAY,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,KAAG,OAAK,IAAI;AAAA,IAAG,YAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS;AAAA,EAAC,CAAC;AAC3J,CAAC;AAQD,eAAsB,eAAe,SAAiB,QAAe;AACnE,QAAM,OAAO,oBAAoB,MAAM,OAAO;AAC9C,QAAM,UAAoB,CAAC;AAC3B,MAAI;AACJ,MAAI,SAAS;AACb,aAAS;AACP,UAAM,WAAW,EAAE,OAAO,EAAC,SAAQ,EAAE,QAAQ,EAAE,SAAS,GAAE,mBAAkB,EAAE,QAAQ,EAAE,SAAS,EAAC,CAAC,EAAE,MAAM,MAAM,OAAO,KAAK,gBAAgB;AAAA,MAC3I,WAAU,KAAK;AAAA,MAAW,kBAAiB,KAAK;AAAA,MAAkB;AAAA,MAClE,GAAI,QAAQ,EAAC,kBAAiB,MAAM,SAAQ,IAAI,CAAC;AAAA,IACnD,CAAC,CAAC;AACF,QAAI,SAAS,QAAS,OAAM,IAAI,MAAM,2EAA2E;AACjH,UAAM,QAAQ,YAAY,MAAM,SAAS,iBAAiB;AAC1D,QAAI,MAAM,cAAc,KAAK,aAAa,MAAM,aAAa,KAAK,oBAAoB,MAAM,KAAK,WAAW,OAAQ,OAAM,IAAI,MAAM,mEAAmE;AACvM,QAAI,UAAU,MAAM,aAAa,MAAM,YAAY,MAAM,iBAAiB,MAAM,gBAAgB,MAAM,KAAK,aAAa,MAAM,KAAK,YAAY,MAAM,KAAK,gBAAgB,MAAM,KAAK,eAAe,KAAK,UAAU,MAAM,OAAO,MAAM,KAAK,UAAU,MAAM,OAAO,GAAI,OAAM,IAAI,MAAM,iEAAiE;AACvV,cAAU;AACV,UAAM,MAAM,SAAS,MAAM,KAAK,QAAQ;AACxC,QAAI,MAAM,MAAM,KAAK,gBAAgB,MAAM,KAAK,eAAe,OAAO,QAAQ,MAAM,KAAK,cAAc,MAAM,KAAK,eAAe,OAAO,OAAO,MAAM,KAAK,aAAc,OAAM,IAAI,MAAM,6DAA6D;AACrP,YAAQ,KAAK,MAAM,KAAK,OAAO;AAC/B,QAAI,MAAM,KAAK,eAAe,KAAM;AACpC,aAAS,MAAM,KAAK;AAAA,EACtB;AACA,QAAM,QAAQ,CAAC,GAAG,MAAM,QAAQ,IAAI,QAAM,EAAC,UAAS,EAAE,UAAS,SAAQ,EAAE,OAAM,EAAE,GAAE,EAAC,UAAS,MAAM,KAAK,UAAS,SAAQ,QAAQ,KAAK,EAAE,EAAC,CAAC;AAC1I,MAAI,IAAI,IAAI,MAAM,IAAI,OAAK,EAAE,SAAS,YAAY,CAAC,CAAC,EAAE,SAAS,MAAM,OAAQ,OAAM,IAAI,MAAM,mCAAmC;AAChI,QAAM,MAAM,KAAK,eAAe;AAChC,aAAW,QAAQ,MAAO,OAAM,UAAU,KAAK,KAAK,iBAAgB,KAAK,QAAQ,GAAE,KAAK,SAAQ,EAAC,UAAS,QAAO,MAAK,KAAI,CAAC;AAC3H,SAAO;AAAA,IAAC,IAAG;AAAA,IAAK,WAAU,KAAK;AAAA,IAAgB,UAAS,MAAM;AAAA,IAAS,UAAS,MAAM;AAAA,IAAS,cAAa,MAAM;AAAA,IAChH,OAAM,MAAM,IAAI,QAAM,EAAC,UAAS,EAAE,UAAS,MAAK,KAAK,KAAK,iBAAgB,EAAE,QAAQ,GAAE,OAAM,OAAO,WAAW,EAAE,OAAO,EAAC,EAAE;AAAA,IAC1H,UAAS,KAAK,KAAK,iBAAgB,MAAM,KAAK,QAAQ;AAAA,IACtD,MAAK;AAAA,EACP;AACF;;;AJvCA,IAAM,eAAe;AAGd,IAAM,kBAAkB;AAiDxB,SAAS,sBAAsB,UAAkC,CAAC,GAAwB;AAC/F,QAAM,UAAU,iBAAiB,QAAQ,WAAW,iBAAiB;AACrE,QAAM,kBAAkB,QAAQ,mBAAmB,CAAC;AAEpD,QAAM,SAAS,IAAI,iBAAiB;AAAA,IAClC,MAAM,gBAAgB;AAAA,IACtB,MAAM,gBAAgB;AAAA,IACtB,OAAO,gBAAgB,SAAS;AAAA;AAAA;AAAA,IAGhC,WAAW,gBAAgB,aAAa;AAAA,IACxC,SAAS,gBAAgB,WAAW;AAAA,IACpC,gBAAgB,CAAC,OAAO;AAAA;AAAA;AAAA,IAGxB,iBAAiB;AAAA,EACnB,GAAG,IAAI,uBAAuB,CAAC;AAC/B,QAAM,SAAS,IAAI,sBAAsB;AAAA,IACvC,YAAY;AAAA,IACZ,eAAe;AAAA,IACf;AAAA,EACF,GAAG,2BAA2B;AAE9B,sBAAoB,QAAQ,EAAE,SAAS,eAAe,QAAQ,iBAAiB,MAAM,CAAC;AACtF,mCAAiC,QAAQ,MAAM;AAC/C,SAAO;AACT;AAMO,IAAM,8BAA8B;AAsB3C,IAAM,aAAa,CAAC,UAAmB,OAAO,UAAU;AAExD,IAAM,mBAAmBC,GAAE,OAAO;AAAA,EAChC,WAAWA,GAAE,OAAO;AAAA,IAClB,cAAcA,GAAE,OAAO,UAAU;AAAA;AAAA;AAAA,IAGjC,kBAAkBA,GAAE,OAAO,EAAE,kBAAkBA,GAAE,OAAO,EAAE,SAASA,GAAE,OAAO,UAAU,EAAE,CAAC,EAAE,CAAC;AAAA,EAC9F,CAAC;AAAA,EACD,gBAAgBA,GAAE,WAAW,GAAG;AAClC,CAAC;AACD,IAAM,iBAAiBA,GAAE;AAAA,EACvB,CAAC,UAAU,iBAAiB,UAAU,KAAK,EAAE;AAAA,EAC7C,EAAE,OAAO,2HAA2H;AACtI;AAEA,SAAS,oBACP,QACA,KACM;AACN,QAAM,OAAO,eAAe,MAAM,MAAM;AAExC,iBAAe,MAAM;AAAA,IACnB,MAAK;AAAA,IACL,aAAY;AAAA,IACZ,aAAY,EAAC,cAAa,OAAM,iBAAgB,MAAK;AAAA,IACrD,OAAM;AAAA,IACN,SAAQ,OAAM,SAAQ;AACpB,UAAI;AACF,cAAM,UAAU,MAAM,eAAe,MAAK,CAAC,MAAK,WAAW,OAAO,OAAO,WAAW,MAAK,MAAM,CAAC;AAChG,eAAO,EAAC,SAAQ,CAAC,EAAC,MAAK,QAAO,MAAK,KAAK,UAAU,OAAO,EAAC,CAAC,GAAE,mBAAkB,QAAO;AAAA,MACxF,SAAS,OAAO;AACd,eAAO,EAAC,SAAQ,MAAK,SAAQ,CAAC,EAAC,MAAK,QAAO,MAAK,OAAO,KAAK,EAAC,CAAC,EAAC;AAAA,MACjE;AAAA,IACF;AAAA,EACF,CAAC;AAED,iBAAe,MAAM;AAAA,IACnB,MAAM;AAAA,IACN,aACE;AAAA,IACF,aAAa,EAAE,cAAc,MAAM,gBAAgB,KAAK;AAAA,IACxD,OAAOA,GAAE,OAAO,CAAC,CAAC;AAAA,IAClB,SAAS,YAAY;AACnB,YAAM,UAAU,gBAAgB,EAAE,IAAI,CAAC,UAAU;AAAA,QAC/C,MAAM,KAAK;AAAA,QACX,OAAO,KAAK;AAAA,QACZ,aAAa,KAAK;AAAA,QAClB,KAAK,cAAc,MAAM,IAAI,OAAO;AAAA,MACtC,EAAE;AACF,YAAM,UAAU,EAAE,SAAS,IAAI,SAAS,aAAa,QAAQ;AAC7D,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE,CAAC;AAAA,QAClE,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF,CAAC;AAED,iBAAe,MAAM;AAAA,IACnB,MAAM;AAAA,IACN,aACE;AAAA,IACF,aAAa,EAAE,eAAe,KAAK;AAAA,IACnC,OAAOA,GAAE,OAAO;AAAA,MACd,MAAMA,GAAE,OAAO,EAAE;AAAA,QACf;AAAA,MACF;AAAA,IACF,CAAC;AAAA,IACD,SAAS,OAAO,SAAS;AACvB,YAAM,OAA+B,eAAe,KAAK,IAAI;AAC7D,UAAI,CAAC,MAAM;AACT,cAAM,QAAQ,gBAAgB,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI;AAC5D,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MAAM,4BAA4B,KAAK,IAAI,mBAAmB,KAAK;AAAA,YACrE;AAAA,UACF;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AACA,YAAM,MAAM,eAAe,cAAc,MAAM,IAAI,OAAO,CAAC;AAC3D,YAAM,UAA8B,IAAI,gBAAgB,OAAO,MAAM,iBAAiB,GAAG;AACzF,YAAM,UAAU;AAAA,QACd,YAAY;AAAA,UACV,MAAM,KAAK;AAAA,UACX,OAAO,KAAK;AAAA,UACZ,aAAa,KAAK;AAAA,QACpB;AAAA,QACA;AAAA,QACA,QAAQ,SAAS,UAAU;AAAA,QAC3B,QAAQ,WAAW,EAAE,SAAS,QAAQ,SAAS,MAAM,QAAQ,KAAK;AAAA,QAClE,GAAI,SAAS,UAAU,SAAY,CAAC,IAAI,EAAE,aAAa,QAAQ,MAAM;AAAA,MACvE;AACA,aAAO;AAAA,QACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,WAAW,MAAM,KAAK,OAAO,EAAE,CAAC;AAAA,QAChE,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAGA,SAAS,WAAW,MAAkB,KAAa,SAAqC;AACtF,MAAI,YAAY,KAAM,QAAO,cAAc,KAAK,KAAK,OAAO,GAAG;AAC/D,MAAI,QAAQ,OAAQ,QAAO,WAAW,KAAK,KAAK,OAAO,GAAG;AAC1D,SAAO,6CAA6C,QAAQ,KAAK,mDACf,KAAK,KAAK,KAAK,GAAG;AACtE;AAcA,SAAS,iCAAiC,QAA6B,QAAgC;AACrG,QAAM,WAAW,eAAe,MAAM,MAAM,EAAE,UAAU,iBAAiB;AACzE,QAAM,eAAe,SAAS;AAC9B,WAAS,UAAU,UAAU,SAAS;AACpC,UAAM,SAAU,MAAM,aAAa,GAAG,IAAI;AAC1C,UAAM,OAAO,OAAO,WAAW,MAAM;AACrC,WAAO,OAAO,YAAY,QAAQ,SAAS,SAAY,EAAE,GAAG,QAAQ,SAAS,KAAK,IAAI;AAAA,EACxF;AACF;AAYA,SAAS,eAA0C,MAAsB,MAAmC;AAC1G,QAAM,EAAE,MAAM,aAAa,aAAa,OAAO,QAAQ,IAAI;AAC3D,OAAK,UAAU,aAAa,MAAM,EAAE,aAAa,aAAa,MAAM,OAAO,YAAY,GAAG,OAAO;AACjG,OAAK,eAAe,IAAI,MAAM;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,aAAaA,GAAE,aAAa,OAAO,EAAE,iBAAiB,MAAM,CAAC;AAAA,IAC7D;AAAA,EACF,CAAC;AACH;AAEA,SAAS,iBAAiB,KAAqB;AAE7C,SAAO,IAAI,IAAI,GAAG,EAAE;AACtB;","names":["z","z"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yichus/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "MCP server that exposes the Yichus calculators and playground over WebMCP",
5
5
  "license": "MIT",
6
6
  "homepage": "https://yich.us",
@@ -25,19 +25,13 @@
25
25
  "dist",
26
26
  "README.md"
27
27
  ],
28
- "scripts": {
29
- "build": "tsup",
30
- "test": "vitest run",
31
- "test:watch": "vitest",
32
- "typecheck": "tsc --noEmit",
33
- "prepublishOnly": "npm run build && npm test"
34
- },
35
28
  "dependencies": {
36
29
  "@mcp-b/webmcp-local-relay": "2.2.1",
37
30
  "zod": "^4.3.6"
38
31
  },
39
32
  "devDependencies": {
40
33
  "@types/node": "^22.0.0",
34
+ "@vitest/coverage-v8": "2.1.9",
41
35
  "tsup": "^8.3.0",
42
36
  "typescript": "^5.6.0",
43
37
  "vitest": "^2.1.0"
@@ -55,5 +49,11 @@
55
49
  "yichus",
56
50
  "calculator",
57
51
  "bosatsu"
58
- ]
59
- }
52
+ ],
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "test": "vitest run --coverage",
56
+ "test:watch": "vitest",
57
+ "typecheck": "tsc --noEmit"
58
+ }
59
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Steven Klaiber-Noble
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.