@wervt/nuxt 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/module.d.ts +11 -0
- package/dist/module.js +73 -4
- package/dist/runtime/app/plugins/update.client.d.ts +4 -0
- package/dist/runtime/app/plugins/update.client.js +78 -0
- package/dist/runtime/app/plugins/webmcp.client.d.ts +4 -0
- package/dist/runtime/app/plugins/webmcp.client.js +50 -0
- package/dist/runtime/app/useWervtUpdate.d.ts +13 -0
- package/dist/runtime/app/useWervtUpdate.js +21 -0
- package/dist/runtime/server/index.d.ts +3 -2
- package/dist/runtime/server/index.js +2 -1
- package/dist/runtime/server/tool-handlers.d.ts +32 -0
- package/dist/runtime/server/tool-handlers.js +70 -0
- package/dist/runtime/server/tool-list.d.ts +2 -0
- package/dist/runtime/server/tool-list.js +5 -0
- package/dist/runtime/server/tool-run.d.ts +2 -0
- package/dist/runtime/server/tool-run.js +5 -0
- package/dist/runtime/server/tools.d.ts +15 -0
- package/dist/runtime/server/tools.js +7 -0
- package/dist/runtime/server/validate.d.ts +4 -2
- package/dist/runtime/server/validate.js +1 -1
- package/package.json +2 -1
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/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,8 @@
|
|
|
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 } from "nuxt/kit";
|
|
3
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { builtinModules, createRequire } from "node:module";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import evlog from "evlog/nuxt";
|
|
7
7
|
//#region src/module.ts
|
|
8
8
|
const CONTENT_DATABASE = {
|
|
@@ -61,7 +61,7 @@ var module_default = defineNuxtModule({
|
|
|
61
61
|
inline: [
|
|
62
62
|
...options.externals?.inline ?? [],
|
|
63
63
|
resolver.resolve("./runtime"),
|
|
64
|
-
/(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@electric-sql\/client|evlog)(\/|$)/
|
|
64
|
+
/(^|\/node_modules\/)(drizzle-orm|postgres|valibot|@valibot\/to-json-schema|@electric-sql\/client|evlog)(\/|$)/
|
|
65
65
|
]
|
|
66
66
|
};
|
|
67
67
|
const exclude = options.esbuild?.options?.exclude ?? /node_modules/;
|
|
@@ -161,6 +161,32 @@ var module_default = defineNuxtModule({
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
}
|
|
164
|
+
const toolsDir = join(nuxt.options.serverDir, "tools");
|
|
165
|
+
const tools = options.webmcp === false ? [] : await findTools(toolsDir);
|
|
166
|
+
if (tools.length > 0) {
|
|
167
|
+
addServerTemplate({
|
|
168
|
+
filename: "#wervt/tools",
|
|
169
|
+
getContents: () => `export const tools = {\n${tools.map(({ name, path }) => ` ${JSON.stringify(name)}: () => import(${JSON.stringify(path)}),`).join("\n")}\n}\n`
|
|
170
|
+
});
|
|
171
|
+
addServerHandler({
|
|
172
|
+
route: "/_wervt/tools",
|
|
173
|
+
method: "get",
|
|
174
|
+
handler: resolver.resolve("./runtime/server/tool-list")
|
|
175
|
+
});
|
|
176
|
+
addServerHandler({
|
|
177
|
+
route: "/_wervt/tools/**",
|
|
178
|
+
method: "post",
|
|
179
|
+
handler: resolver.resolve("./runtime/server/tool-run")
|
|
180
|
+
});
|
|
181
|
+
addPlugin({
|
|
182
|
+
src: resolver.resolve("./runtime/app/plugins/webmcp.client"),
|
|
183
|
+
mode: "client"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (nuxt.options.dev && options.webmcp !== false) nuxt.hook("builder:watch", async (event, path) => {
|
|
187
|
+
const file = relative(toolsDir, resolve(nuxt.options.srcDir, path));
|
|
188
|
+
if ((event === "add" || event === "unlink") && !file.startsWith("..")) await nuxt.callHook("restart");
|
|
189
|
+
});
|
|
164
190
|
if (nuxt.options.dev && !nuxt.options._prepare) {
|
|
165
191
|
const app = await resolveAppName(nuxt.options.rootDir, options.app);
|
|
166
192
|
await setupDev(nuxt, app, options.dev ?? {}, { storage: useAblage });
|
|
@@ -178,6 +204,19 @@ var module_default = defineNuxtModule({
|
|
|
178
204
|
src: resolver.resolve("./runtime/app/plugins/user.client"),
|
|
179
205
|
mode: "client"
|
|
180
206
|
});
|
|
207
|
+
const updates = options.updates ?? "banner";
|
|
208
|
+
if (updates && !nuxt.options.dev) {
|
|
209
|
+
nuxt.options.experimental.checkOutdatedBuildInterval = false;
|
|
210
|
+
const config = nuxt.options.runtimeConfig.public;
|
|
211
|
+
config.wervt = {
|
|
212
|
+
...config.wervt,
|
|
213
|
+
updates
|
|
214
|
+
};
|
|
215
|
+
addPlugin({
|
|
216
|
+
src: resolver.resolve("./runtime/app/plugins/update.client"),
|
|
217
|
+
mode: "client"
|
|
218
|
+
});
|
|
219
|
+
}
|
|
181
220
|
nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
|
|
182
221
|
addImports([
|
|
183
222
|
{
|
|
@@ -195,10 +234,40 @@ var module_default = defineNuxtModule({
|
|
|
195
234
|
{
|
|
196
235
|
name: "wervtLogoutUrl",
|
|
197
236
|
from: resolver.resolve("./runtime/app/useWervtUser")
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "useWervtUpdate",
|
|
240
|
+
from: resolver.resolve("./runtime/app/useWervtUpdate")
|
|
198
241
|
}
|
|
199
242
|
]);
|
|
200
243
|
}
|
|
201
244
|
});
|
|
245
|
+
const TOOL_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
246
|
+
".ts",
|
|
247
|
+
".js",
|
|
248
|
+
".mts",
|
|
249
|
+
".mjs"
|
|
250
|
+
]);
|
|
251
|
+
const TOOL_NAME_RE = /^[\w.-]{1,128}$/;
|
|
252
|
+
/** The tools in `server/tools/`: `todos/add.ts` is `todos.add`, like Nitro names tasks. */
|
|
253
|
+
async function findTools(dir) {
|
|
254
|
+
const files = await readdir(dir, { recursive: true }).catch(() => []);
|
|
255
|
+
const tools = [];
|
|
256
|
+
for (const file of files.toSorted((a, b) => a.localeCompare(b))) {
|
|
257
|
+
const ext = extname(file);
|
|
258
|
+
if (!TOOL_EXTENSIONS.has(ext) || file.endsWith(`.d${ext}`)) continue;
|
|
259
|
+
const name = file.slice(0, -ext.length).split(sep).join(".");
|
|
260
|
+
if (!TOOL_NAME_RE.test(name)) {
|
|
261
|
+
console.warn(`[wervt] Skipping server/tools/${file}: tool names can only use A-Z, 0-9, _ and -.`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
tools.push({
|
|
265
|
+
name,
|
|
266
|
+
path: join(dir, file)
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return tools;
|
|
270
|
+
}
|
|
202
271
|
/** evlog's Nitro 2 plugin and error handler, as evlog/nuxt resolves them. */
|
|
203
272
|
const EVLOG_NITRO2_RE = /([\\/]evlog[\\/]dist[\\/]nitro)[\\/](plugin|errorHandler)(\.mjs)?$/;
|
|
204
273
|
const BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
|
|
@@ -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,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,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
|
-
|
|
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 };
|
|
@@ -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
|
+
error: string;
|
|
15
|
+
issues?: undefined;
|
|
16
|
+
result?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
error: string;
|
|
19
|
+
issues: {
|
|
20
|
+
readonly root?: [string, ...string[]] | undefined;
|
|
21
|
+
readonly nested?: {
|
|
22
|
+
readonly [x: string]: [string, ...string[]] | undefined;
|
|
23
|
+
} | undefined;
|
|
24
|
+
readonly other?: [string, ...string[]] | undefined;
|
|
25
|
+
};
|
|
26
|
+
result?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
result: {} | null;
|
|
29
|
+
error?: undefined;
|
|
30
|
+
issues?: undefined;
|
|
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,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
|
|
@@ -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. */
|
|
@@ -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.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Nuxt module for apps hosted on wervt: database, live data and runtime setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drizzle",
|
|
@@ -39,6 +39,7 @@
|
|
|
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
44
|
"evlog": "^2.30.0",
|
|
44
45
|
"postgres": "^3.4.9",
|