@wervt/nuxt 0.2.8 → 0.2.10

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.
Files changed (29) hide show
  1. package/README.md +1 -0
  2. package/dist/dev.js +1 -1
  3. package/dist/module.d.ts +11 -0
  4. package/dist/module.js +85 -4
  5. package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.js +1 -1
  6. package/dist/runtime/app/plugins/update.client.d.ts +4 -0
  7. package/dist/runtime/app/plugins/update.client.js +78 -0
  8. package/dist/runtime/app/plugins/webmcp.client.d.ts +4 -0
  9. package/dist/runtime/app/plugins/webmcp.client.js +50 -0
  10. package/dist/runtime/app/useWervtUpdate.d.ts +13 -0
  11. package/dist/runtime/app/useWervtUpdate.js +21 -0
  12. package/dist/runtime/server/boot.d.ts +1 -1
  13. package/dist/runtime/server/index.d.ts +3 -2
  14. package/dist/runtime/server/index.js +2 -1
  15. package/dist/runtime/server/task-handlers.d.ts +1 -1
  16. package/dist/runtime/server/task-run-nitro2.d.ts +1 -1
  17. package/dist/runtime/server/task-run-nitro3.d.ts +1 -1
  18. package/dist/runtime/server/tool-handlers.d.ts +32 -0
  19. package/dist/runtime/server/tool-handlers.js +70 -0
  20. package/dist/runtime/server/tool-list.d.ts +2 -0
  21. package/dist/runtime/server/tool-list.js +5 -0
  22. package/dist/runtime/server/tool-run.d.ts +2 -0
  23. package/dist/runtime/server/tool-run.js +5 -0
  24. package/dist/runtime/server/tools.d.ts +15 -0
  25. package/dist/runtime/server/tools.js +7 -0
  26. package/dist/runtime/server/validate.d.ts +5 -3
  27. package/dist/runtime/server/validate.js +1 -1
  28. package/package.json +3 -2
  29. /package/dist/node_modules/.pnpm/{h3@2.0.1_crossws@0.4.12_srvx@1.0.5__ocache@0.3.0 → h3@2.0.2_crossws@0.4.15_srvx@1.0.5__ocache@0.3.0}/node_modules/h3/dist/response.js +0 -0
package/README.md CHANGED
@@ -19,6 +19,7 @@ export default defineNuxtConfig({
19
19
  - `proxyShape()` (server) and `useShape()` (app): live rows from Postgres via Electric.
20
20
  - `useWervtUser()`: the signed-in wervt user, on the server and in the app.
21
21
  - `useValidatedBody()` / `useValidatedQuery()` / `useValidatedParams()`: valibot validation.
22
+ - `defineTool()` (server): tools in `server/tools/` that browser agents call through WebMCP.
22
23
  - `nuxt dev` starts against the local wervt stack (Postgres, Electric) and a dev user.
23
24
 
24
25
  Deploy with [`@wervt/cli`](https://www.npmjs.com/package/@wervt/cli): `wervt deploy`.
package/dist/dev.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createStorage } from "./storage.js";
2
2
  import { appDatabaseUrl, dbRole, provisionDatabase, storageCredentials } from "./provision.js";
3
3
  import { useLogger } from "nuxt/kit";
4
+ import { existsSync, watch } from "node:fs";
4
5
  import { readFile } from "node:fs/promises";
5
6
  import { join } from "node:path";
6
- import { existsSync, watch } from "node:fs";
7
7
  //#region src/dev.ts
8
8
  const DEFAULTS = {
9
9
  adminUrl: "postgres://postgres:postgres@localhost:5432/wervt",
package/dist/module.d.ts CHANGED
@@ -7,6 +7,17 @@ export interface ModuleOptions {
7
7
  dev?: DevOptions;
8
8
  /** Time zone of `nitro.scheduledTasks`, e.g. `Europe/Berlin`. Defaults to UTC. */
9
9
  timezone?: string;
10
+ /**
11
+ * When a new release is live while the app is open: `'banner'` (default) shows a small
12
+ * "A new version is available · Reload" banner, `'manual'` only sets `useWervtUpdate()` for
13
+ * the app's own banner, `false` leaves it to Nuxt (reload on the next navigation, hourly).
14
+ */
15
+ updates?: 'banner' | 'manual' | false;
16
+ /**
17
+ * Registers the tools in `server/tools/` (`defineTool`) with browser agents through WebMCP.
18
+ * Defaults to true; `false` leaves them out of the build.
19
+ */
20
+ webmcp?: boolean;
10
21
  }
11
22
  /** `server/wervt-tasks.json` in the build: the app's tasks and schedules, for the control plane. */
12
23
  export interface TasksManifest {
package/dist/module.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { resolveAppName, setupDev } from "./dev.js";
2
- import { addImports, addPlugin, addServerHandler, addServerPlugin, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule } from "nuxt/kit";
3
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { addImports, addPlugin, addServerHandler, addServerPlugin, addServerTemplate, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule, useLogger } from "nuxt/kit";
3
+ import { existsSync } from "node:fs";
4
+ import { mkdir, readdir, writeFile } from "node:fs/promises";
4
5
  import { builtinModules, createRequire } from "node:module";
5
- import { dirname, join } from "node:path";
6
+ import { dirname, extname, join, relative, resolve, sep } from "node:path";
6
7
  import evlog from "evlog/nuxt";
7
8
  //#region src/module.ts
8
9
  const CONTENT_DATABASE = {
@@ -61,7 +62,7 @@ var module_default = defineNuxtModule({
61
62
  inline: [
62
63
  ...options.externals?.inline ?? [],
63
64
  resolver.resolve("./runtime"),
64
- /(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@electric-sql\/client|evlog)(\/|$)/
65
+ /(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
65
66
  ]
66
67
  };
67
68
  const exclude = options.esbuild?.options?.exclude ?? /node_modules/;
@@ -161,6 +162,35 @@ var module_default = defineNuxtModule({
161
162
  });
162
163
  });
163
164
  }
165
+ const toolsDir = join(nuxt.options.serverDir, "tools");
166
+ const tools = options.webmcp === false ? [] : await findTools(toolsDir);
167
+ if (tools.length > 0) {
168
+ addServerTemplate({
169
+ filename: "#wervt/tools",
170
+ getContents: () => `export const tools = {\n${tools.map(({ name, path }) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(path)}),`).join("\n")}\n}\n`
171
+ });
172
+ addServerHandler({
173
+ route: "/_wervt/tools",
174
+ method: "get",
175
+ handler: resolver.resolve("./runtime/server/tool-list")
176
+ });
177
+ addServerHandler({
178
+ route: "/_wervt/tools/**",
179
+ method: "post",
180
+ handler: resolver.resolve("./runtime/server/tool-run")
181
+ });
182
+ addPlugin({
183
+ src: resolver.resolve("./runtime/app/plugins/webmcp.client"),
184
+ mode: "client"
185
+ });
186
+ }
187
+ if (nuxt.options.dev && options.webmcp !== false) nuxt.hook("builder:watch", async (event, path) => {
188
+ const file = relative(toolsDir, resolve(nuxt.options.srcDir, path));
189
+ if ((event === "add" || event === "unlink") && !file.startsWith("..")) await nuxt.callHook("restart");
190
+ });
191
+ if (nuxt.options.dev && nitroMajor >= 3) {
192
+ if (!hasPackage(nuxt.options.rootDir, "vue")) logger.warn("`vue` is not a dependency of this app, so `nuxt dev` fails with \"module is not defined\". Run `pnpm add vue`.");
193
+ }
164
194
  if (nuxt.options.dev && !nuxt.options._prepare) {
165
195
  const app = await resolveAppName(nuxt.options.rootDir, options.app);
166
196
  await setupDev(nuxt, app, options.dev ?? {}, { storage: useAblage });
@@ -178,6 +208,19 @@ var module_default = defineNuxtModule({
178
208
  src: resolver.resolve("./runtime/app/plugins/user.client"),
179
209
  mode: "client"
180
210
  });
211
+ const updates = options.updates ?? "banner";
212
+ if (updates && !nuxt.options.dev) {
213
+ nuxt.options.experimental.checkOutdatedBuildInterval = false;
214
+ const config = nuxt.options.runtimeConfig.public;
215
+ config.wervt = {
216
+ ...config.wervt,
217
+ updates
218
+ };
219
+ addPlugin({
220
+ src: resolver.resolve("./runtime/app/plugins/update.client"),
221
+ mode: "client"
222
+ });
223
+ }
181
224
  nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
182
225
  addImports([
183
226
  {
@@ -195,10 +238,48 @@ var module_default = defineNuxtModule({
195
238
  {
196
239
  name: "wervtLogoutUrl",
197
240
  from: resolver.resolve("./runtime/app/useWervtUser")
241
+ },
242
+ {
243
+ name: "useWervtUpdate",
244
+ from: resolver.resolve("./runtime/app/useWervtUpdate")
198
245
  }
199
246
  ]);
200
247
  }
201
248
  });
249
+ const TOOL_EXTENSIONS = /* @__PURE__ */ new Set([
250
+ ".ts",
251
+ ".js",
252
+ ".mts",
253
+ ".mjs"
254
+ ]);
255
+ const TOOL_NAME_RE = /^[\w.-]{1,128}$/;
256
+ /** The tools in `server/tools/`: `todos/add.ts` is `todos.add`, like Nitro names tasks. */
257
+ async function findTools(dir) {
258
+ const files = await readdir(dir, { recursive: true }).catch(() => []);
259
+ const tools = [];
260
+ for (const file of files.toSorted((a, b) => a.localeCompare(b))) {
261
+ const ext = extname(file);
262
+ if (!TOOL_EXTENSIONS.has(ext) || file.endsWith(`.d${ext}`)) continue;
263
+ const name = file.slice(0, -ext.length).split(sep).join(".");
264
+ if (!TOOL_NAME_RE.test(name)) {
265
+ logger.warn(`Skipping server/tools/${file}: tool names can only use A-Z, 0-9, _ and -.`);
266
+ continue;
267
+ }
268
+ tools.push({
269
+ name,
270
+ path: join(dir, file)
271
+ });
272
+ }
273
+ return tools;
274
+ }
275
+ const logger = useLogger("wervt");
276
+ /** Whether `name` is in a `node_modules` folder Node would look in from `dir`. */
277
+ function hasPackage(dir, name) {
278
+ for (let current = dir;; current = dirname(current)) {
279
+ if (existsSync(join(current, "node_modules", name, "package.json"))) return true;
280
+ if (dirname(current) === current) return false;
281
+ }
282
+ }
202
283
  /** evlog's Nitro 2 plugin and error handler, as evlog/nuxt resolves them. */
203
284
  const EVLOG_NITRO2_RE = /([\\/]evlog[\\/]dist[\\/]nitro)[\\/](plugin|errorHandler)(\.mjs)?$/;
204
285
  const BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
@@ -1,4 +1,4 @@
1
- import { HTTPError } from "../../../../../../h3@2.0.1_crossws@0.4.12_srvx@1.0.5__ocache@0.3.0/node_modules/h3/dist/response.js";
1
+ import { HTTPError } from "../../../../../../h3@2.0.2_crossws@0.4.15_srvx@1.0.5__ocache@0.3.0/node_modules/h3/dist/response.js";
2
2
  import { tasks } from "#nitro/virtual/tasks";
3
3
  //#region ../../node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.mjs
4
4
  const __runningTasks__ = {};
@@ -0,0 +1,4 @@
1
+ //#region src/runtime/app/plugins/update.client.d.ts
2
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,78 @@
1
+ import { useUpdateAvailable, useWervtUpdate } from "../useWervtUpdate.js";
2
+ import { defineNuxtPlugin, getAppManifest, onNuxtReady, useRuntimeConfig } from "#app";
3
+ //#region src/runtime/app/plugins/update.client.ts
4
+ const INTERVAL_MS = 6e4;
5
+ /** Focus and visibility changes come in bursts; one check covers them. */
6
+ const MIN_GAP_MS = 1e4;
7
+ var update_client_default = defineNuxtPlugin({
8
+ name: "wervt:update",
9
+ setup(nuxtApp) {
10
+ const config = useRuntimeConfig();
11
+ const mode = config.public.wervt?.updates;
12
+ const available = useUpdateAvailable();
13
+ const url = `${(config.app.cdnURL || config.app.baseURL).replace(/\/$/, "")}/${config.app.buildAssetsDir.replace(/^\/|\/$/g, "")}/builds/latest.json`;
14
+ let last = 0;
15
+ async function check() {
16
+ if (available.value || document.hidden || Date.now() - last < MIN_GAP_MS) return;
17
+ last = Date.now();
18
+ try {
19
+ const current = await getAppManifest();
20
+ const res = await fetch(`${url}?${last}`, {
21
+ cache: "no-store",
22
+ signal: AbortSignal.timeout(1e4)
23
+ });
24
+ if (!res.ok) return;
25
+ const latest = await res.json();
26
+ if (!latest.id || latest.id === current.id) return;
27
+ available.value = true;
28
+ await nuxtApp.callHook("app:manifest:update", latest);
29
+ if (mode === "banner") showBanner(useWervtUpdate().reload);
30
+ } catch {}
31
+ }
32
+ onNuxtReady(() => {
33
+ setInterval(check, INTERVAL_MS);
34
+ document.addEventListener("visibilitychange", check);
35
+ window.addEventListener("focus", check);
36
+ });
37
+ }
38
+ });
39
+ /** A small pill at the bottom, plain DOM so it works with any styling (or none). */
40
+ function showBanner(reload) {
41
+ if (document.getElementById("wervt-update")) return;
42
+ const bar = document.createElement("div");
43
+ bar.id = "wervt-update";
44
+ bar.setAttribute("role", "status");
45
+ bar.style.cssText = [
46
+ "position:fixed",
47
+ "left:50%",
48
+ "bottom:max(16px, env(safe-area-inset-bottom))",
49
+ "transform:translateX(-50%)",
50
+ "z-index:2147483647",
51
+ "display:flex",
52
+ "align-items:center",
53
+ "gap:8px",
54
+ "padding:6px 6px 6px 16px",
55
+ "border-radius:999px",
56
+ "background:#18181b",
57
+ "color:#fafafa",
58
+ "font:500 14px/1.4 system-ui, sans-serif",
59
+ "box-shadow:0 4px 16px rgb(0 0 0 / 0.25)",
60
+ "white-space:nowrap"
61
+ ].join(";");
62
+ const text = document.createElement("span");
63
+ text.textContent = "A new version is available";
64
+ const button = (label, style, onClick) => {
65
+ const el = document.createElement("button");
66
+ el.type = "button";
67
+ el.textContent = label;
68
+ el.style.cssText = `border:0;border-radius:999px;padding:6px 12px;font:inherit;cursor:pointer;${style}`;
69
+ el.addEventListener("click", onClick);
70
+ return el;
71
+ };
72
+ const close = button("×", "background:transparent;color:inherit;padding:6px 10px", () => bar.remove());
73
+ close.setAttribute("aria-label", "Dismiss");
74
+ bar.append(text, button("Reload", "background:#fafafa;color:#18181b", reload), close);
75
+ document.body.append(bar);
76
+ }
77
+ //#endregion
78
+ export { update_client_default as default };
@@ -0,0 +1,4 @@
1
+ //#region src/runtime/app/plugins/webmcp.client.d.ts
2
+ declare const _default: import("#app").Plugin<Record<string, unknown>> & import("#app").ObjectPlugin<Record<string, unknown>>;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,50 @@
1
+ import { defineNuxtPlugin, onNuxtReady, useRuntimeConfig } from "#app";
2
+ //#region src/runtime/app/plugins/webmcp.client.ts
3
+ function pageModelContext() {
4
+ const context = document.modelContext ?? navigator.modelContext;
5
+ return typeof context?.registerTool === "function" ? context : void 0;
6
+ }
7
+ var webmcp_client_default = defineNuxtPlugin({
8
+ name: "wervt:webmcp",
9
+ setup() {
10
+ const context = pageModelContext();
11
+ if (!context) return;
12
+ const base = useRuntimeConfig().app.baseURL.replace(/\/$/, "");
13
+ async function execute(name, input) {
14
+ const res = await fetch(`${base}/_wervt/tools/${encodeURIComponent(name)}`, {
15
+ method: "POST",
16
+ headers: { "content-type": "application/json" },
17
+ body: JSON.stringify(input ?? {})
18
+ });
19
+ const body = await res.json().catch(() => void 0);
20
+ if (res.ok) return body?.result ?? null;
21
+ const issues = body?.issues ?? body?.data;
22
+ const message = (typeof body?.error === "string" ? body.error : body?.message) ?? `${res.status} ${res.statusText}`;
23
+ throw new Error(issues ? `${message} ${JSON.stringify(issues)}` : message);
24
+ }
25
+ onNuxtReady(async () => {
26
+ let tools;
27
+ try {
28
+ const res = await fetch(`${base}/_wervt/tools`);
29
+ if (!res.ok) return;
30
+ tools = await res.json();
31
+ } catch {
32
+ return;
33
+ }
34
+ for (const tool of tools) try {
35
+ const result = context.registerTool({
36
+ name: tool.name,
37
+ description: tool.description,
38
+ inputSchema: tool.inputSchema,
39
+ ...tool.readOnly ? { annotations: { readOnlyHint: true } } : {},
40
+ execute: (input) => execute(tool.name, input)
41
+ });
42
+ if (result instanceof Promise) result.catch((e) => console.warn(`wervt: could not register tool ${tool.name}`, e));
43
+ } catch (e) {
44
+ console.warn(`wervt: could not register tool ${tool.name}`, e);
45
+ }
46
+ });
47
+ }
48
+ });
49
+ //#endregion
50
+ export { webmcp_client_default as default };
@@ -0,0 +1,13 @@
1
+ //#region src/runtime/app/useWervtUpdate.d.ts
2
+ /** State behind `useWervtUpdate()`, set by the update check (`plugins/update.client`). */
3
+ export declare const useUpdateAvailable: () => import("vue").Ref<boolean, boolean>;
4
+ /**
5
+ * Whether a newer release of this app is live, and a reload into it. @wervt/nuxt checks every
6
+ * minute and when the tab comes back to the foreground. With `wervt: { updates: 'manual' }`
7
+ * the app shows its own banner from this instead of the built-in one.
8
+ */
9
+ export declare function useWervtUpdate(): {
10
+ available: Readonly<import("vue").Ref<boolean, boolean>>;
11
+ reload: () => void;
12
+ };
13
+ //#endregion
@@ -0,0 +1,21 @@
1
+ import { readonly } from "vue";
2
+ import { reloadNuxtApp, useState } from "#app";
3
+ //#region src/runtime/app/useWervtUpdate.ts
4
+ /** State behind `useWervtUpdate()`, set by the update check (`plugins/update.client`). */
5
+ const useUpdateAvailable = () => useState("wervt-update", () => false);
6
+ /**
7
+ * Whether a newer release of this app is live, and a reload into it. @wervt/nuxt checks every
8
+ * minute and when the tab comes back to the foreground. With `wervt: { updates: 'manual' }`
9
+ * the app shows its own banner from this instead of the built-in one.
10
+ */
11
+ function useWervtUpdate() {
12
+ return {
13
+ available: readonly(useUpdateAvailable()),
14
+ reload: () => reloadNuxtApp({
15
+ force: true,
16
+ persistState: true
17
+ })
18
+ };
19
+ }
20
+ //#endregion
21
+ export { useUpdateAvailable, useWervtUpdate };
@@ -3,8 +3,8 @@ declare const _default: import("nuxt/server").EventHandler<{
3
3
  error: string;
4
4
  ok?: undefined;
5
5
  } | {
6
- ok: boolean;
7
6
  error?: undefined;
7
+ ok: boolean;
8
8
  }>;
9
9
  //#endregion
10
10
  export { _default as default };
@@ -3,6 +3,7 @@ import { useDb } from "./db.js";
3
3
  import { createEvlogError, log, useLogger } from "./logger.js";
4
4
  import { ShapeOptions, proxyShape } from "./shape.js";
5
5
  import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
6
- import { useWervtUser } from "./user.js";
7
6
  import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
8
- export { type SafeResult, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type WervtUser, createEvlogError, defineTask, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
7
+ import { Tool, defineTool } from "./tools.js";
8
+ import { useWervtUser } from "./user.js";
9
+ export { type SafeResult, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type Tool, type WervtUser, createEvlogError, defineTask, defineTool, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
@@ -2,6 +2,7 @@ import { useDb } from "./db.js";
2
2
  import { createEvlogError, log, useLogger } from "./logger.js";
3
3
  import { proxyShape } from "./shape.js";
4
4
  import { defineTask } from "./tasks.js";
5
+ import { defineTool } from "./tools.js";
5
6
  import { useWervtUser } from "./user.js";
6
7
  import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
7
- export { createEvlogError, defineTask, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
8
+ export { createEvlogError, defineTask, defineTool, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
@@ -15,8 +15,8 @@ export declare function taskRunHandler(runTask: RunTask): import("nuxt/server").
15
15
  error: string;
16
16
  result?: undefined;
17
17
  } | {
18
- result: {} | null;
19
18
  error?: undefined;
19
+ result: {} | null;
20
20
  }>>;
21
21
  /** `GET /_wervt/tasks`: the tasks with the descriptions from `defineTask({ meta })`. */
22
22
  export declare function taskListHandler(tasks: Registry): import("nuxt/server").EventHandler<Promise<{
@@ -3,8 +3,8 @@ declare const _default: import("nuxt/server").EventHandler<Promise<{
3
3
  error: string;
4
4
  result?: undefined;
5
5
  } | {
6
- result: {} | null;
7
6
  error?: undefined;
7
+ result: {} | null;
8
8
  }>>;
9
9
  //#endregion
10
10
  export { _default as default };
@@ -3,8 +3,8 @@ declare const _default: import("nuxt/server").EventHandler<Promise<{
3
3
  error: string;
4
4
  result?: undefined;
5
5
  } | {
6
- result: {} | null;
7
6
  error?: undefined;
7
+ result: {} | null;
8
8
  }>>;
9
9
  //#endregion
10
10
  export { _default as default };
@@ -0,0 +1,32 @@
1
+ //#region src/runtime/server/tool-handlers.d.ts
2
+ /** `GET /_wervt/tools`: name, description and JSON Schema of each tool, for the client plugin. */
3
+ export declare const listTools: import("nuxt/server").EventHandler<Promise<{
4
+ name: string;
5
+ description: string;
6
+ inputSchema: import("@valibot/to-json-schema").JsonSchema | {
7
+ type: string;
8
+ properties: {};
9
+ };
10
+ readOnly: boolean;
11
+ }[]>>;
12
+ /** `POST /_wervt/tools/:name` with the arguments as JSON body: runs the tool's handler. */
13
+ export declare const runTool: import("nuxt/server").EventHandler<Promise<{
14
+ result?: undefined;
15
+ error: string;
16
+ issues?: undefined;
17
+ } | {
18
+ result?: undefined;
19
+ error: string;
20
+ issues: {
21
+ readonly root?: [string, ...string[]];
22
+ readonly nested?: {
23
+ readonly [x: string]: [string, ...string[]] | undefined;
24
+ } | undefined;
25
+ readonly other?: [string, ...string[]];
26
+ };
27
+ } | {
28
+ error?: undefined;
29
+ issues?: undefined;
30
+ result: {} | null;
31
+ }>>;
32
+ //#endregion
@@ -0,0 +1,70 @@
1
+ import { useLogger } from "./logger.js";
2
+ import { safeValidate, toSchema } from "./validate.js";
3
+ import { defineEventHandler } from "nuxt/server";
4
+ import { toJsonSchema } from "@valibot/to-json-schema";
5
+ import { tools } from "#wervt/tools";
6
+ //#region src/runtime/server/tool-handlers.ts
7
+ async function load(name) {
8
+ return (await (Object.hasOwn(tools, name) ? tools[name] : void 0)?.())?.default;
9
+ }
10
+ function inputSchema(tool) {
11
+ if (!tool.input) return {
12
+ type: "object",
13
+ properties: {}
14
+ };
15
+ const schema = toJsonSchema(toSchema(tool.input), {
16
+ typeMode: "input",
17
+ errorMode: "ignore"
18
+ });
19
+ delete schema.$schema;
20
+ return schema;
21
+ }
22
+ /** `GET /_wervt/tools`: name, description and JSON Schema of each tool, for the client plugin. */
23
+ const listTools = defineEventHandler(async (event) => {
24
+ event.res.headers.set("cache-control", "no-store");
25
+ return (await Promise.all(Object.keys(tools).map(async (name) => {
26
+ const tool = await load(name);
27
+ if (!tool) return void 0;
28
+ return {
29
+ name,
30
+ description: tool.description,
31
+ inputSchema: inputSchema(tool),
32
+ readOnly: tool.readOnly === true
33
+ };
34
+ }))).filter((tool) => tool !== void 0);
35
+ });
36
+ function crossSite(event) {
37
+ const site = event.req.headers.get("sec-fetch-site");
38
+ return site !== null && site !== "same-origin";
39
+ }
40
+ /** `POST /_wervt/tools/:name` with the arguments as JSON body: runs the tool's handler. */
41
+ const runTool = defineEventHandler(async (event) => {
42
+ event.res.headers.set("cache-control", "no-store");
43
+ if (crossSite(event)) {
44
+ event.res.status = 403;
45
+ return { error: "Tools can only be called from the app itself." };
46
+ }
47
+ const name = decodeURIComponent(new URL(event.req.url).pathname.split("/").pop() ?? "");
48
+ const tool = await load(name);
49
+ if (!tool) {
50
+ event.res.status = 404;
51
+ return { error: `No tool named ${name}.` };
52
+ }
53
+ useLogger(event).set({ tool: name });
54
+ const body = await event.req.json().catch(() => void 0);
55
+ let input;
56
+ if (tool.input) {
57
+ const result = await safeValidate(tool.input, body ?? {});
58
+ if (!result.success) {
59
+ event.res.status = 400;
60
+ return {
61
+ error: "Invalid arguments.",
62
+ issues: result.issues
63
+ };
64
+ }
65
+ input = result.data;
66
+ }
67
+ return { result: await tool.handler(event, input) ?? null };
68
+ });
69
+ //#endregion
70
+ export { listTools, runTool };
@@ -0,0 +1,2 @@
1
+ import { listTools } from "./tool-handlers.js";
2
+ export { listTools as default };
@@ -0,0 +1,5 @@
1
+ import { listTools } from "./tool-handlers.js";
2
+ //#region src/runtime/server/tool-list.ts
3
+ var tool_list_default = listTools;
4
+ //#endregion
5
+ export { tool_list_default as default };
@@ -0,0 +1,2 @@
1
+ import { runTool } from "./tool-handlers.js";
2
+ export { runTool as default };
@@ -0,0 +1,5 @@
1
+ import { runTool } from "./tool-handlers.js";
2
+ //#region src/runtime/server/tool-run.ts
3
+ var tool_run_default = runTool;
4
+ //#endregion
5
+ export { tool_run_default as default };
@@ -0,0 +1,15 @@
1
+ import { Input, Output } from "./validate.js";
2
+ import { RequestEvent } from "nuxt/server";
3
+ //#region src/runtime/server/tools.d.ts
4
+ export interface Tool<I extends Input | undefined = Input | undefined, R = unknown> {
5
+ /** What the tool does and when an agent should call it. Agents read this to pick a tool. */
6
+ description: string;
7
+ /** valibot schema or `{ key: schema }` shape of the arguments. Omit it for no arguments. */
8
+ input?: I;
9
+ /** Only reads, changes nothing. Agents may call such tools without asking the user. */
10
+ readOnly?: boolean;
11
+ handler(event: RequestEvent, input: I extends Input ? Output<I> : undefined): R | Promise<R>;
12
+ }
13
+ /** A tool in `server/tools/` (`server/tools/todos/add.ts` is `todos.add`). */
14
+ export declare function defineTool<R, I extends Input | undefined = undefined>(tool: Tool<I, R>): Tool<I, R>;
15
+ //#endregion
@@ -0,0 +1,7 @@
1
+ //#region src/runtime/server/tools.ts
2
+ /** A tool in `server/tools/` (`server/tools/todos/add.ts` is `todos.add`). */
3
+ function defineTool(tool) {
4
+ return tool;
5
+ }
6
+ //#endregion
7
+ export { defineTool };
@@ -3,8 +3,8 @@ import * as v from "valibot";
3
3
  //#region src/runtime/server/validate.d.ts
4
4
  type Schema = v.GenericSchema | v.GenericSchemaAsync;
5
5
  type Shape = v.ObjectEntriesAsync;
6
- type Input = Schema | Shape;
7
- type Output<T extends Input> = T extends Schema ? v.InferOutput<T> : T extends Shape ? v.InferOutput<v.ObjectSchemaAsync<T, undefined>> : never;
6
+ export type Input = Schema | Shape;
7
+ export type Output<T extends Input> = T extends Schema ? v.InferOutput<T> : T extends Shape ? v.InferOutput<v.ObjectSchemaAsync<T, undefined>> : never;
8
8
  export type SafeResult<T extends Input> = {
9
9
  success: true;
10
10
  data: Output<T>;
@@ -14,6 +14,8 @@ export type SafeResult<T extends Input> = {
14
14
  data?: undefined;
15
15
  issues: v.FlatErrors<undefined>;
16
16
  };
17
+ export declare function toSchema(input: Input): Schema;
18
+ export declare function safeValidate<T extends Input>(input: T, data: unknown): Promise<SafeResult<T>>;
17
19
  /** Parsed JSON body. Throws a 400 with flattened issues when invalid. */
18
20
  export declare const useValidatedBody: <T extends Input>(event: RequestEvent, schema: T) => Promise<Output<T>>;
19
21
  /** Query parameters (strings; use `vh.*` to coerce). Throws a 400 when invalid. */
@@ -27,7 +29,7 @@ export declare const useSafeValidatedParams: <T extends Input>(event: RequestEve
27
29
  /** Schemas for values that arrive as strings (query, route params, form fields). */
28
30
  export declare const vh: {
29
31
  /** `'true'` / `'false'` → boolean. */
30
- boolAsString: v.SchemaWithPipe<readonly [v.PicklistSchema<["true", "false"], undefined>, v.TransformAction<"true" | "false", boolean>]>;
32
+ boolAsString: v.SchemaWithPipe<readonly [v.PicklistSchema<["true", "false"], undefined>, v.TransformAction<"false" | "true", boolean>]>;
31
33
  /** HTML checkbox: `'on'` or missing → boolean. */
32
34
  checkboxAsString: v.SchemaWithPipe<readonly [v.OptionalSchema<v.LiteralSchema<"on", undefined>, undefined>, v.TransformAction<"on" | undefined, boolean>]>;
33
35
  /** Integer string → number. */
@@ -48,4 +48,4 @@ const vh = {
48
48
  id: v.pipe(v.string(), v.digits(), v.transform(Number), v.safeInteger(), v.minValue(1))
49
49
  };
50
50
  //#endregion
51
- export { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh };
51
+ export { safeValidate, toSchema, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wervt/nuxt",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Nuxt module for apps hosted on wervt: database, live data and runtime setup.",
5
5
  "keywords": [
6
6
  "drizzle",
@@ -39,8 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@electric-sql/client": "^1.5.28",
42
+ "@valibot/to-json-schema": "^1.8.0",
42
43
  "aws4fetch": "^1.0.20",
43
- "evlog": "^2.30.0",
44
+ "evlog": "^2.30.1",
44
45
  "postgres": "^3.4.9",
45
46
  "valibot": "^1.5.0"
46
47
  },