@devframes/agentic 1.2.6 → 1.3.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/connect/index.mjs +18 -6
- package/dist/mcp/index.mjs +174 -11
- package/package.json +6 -6
package/dist/connect/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/server";
|
|
2
|
-
import { diagnostics, listLiveDevframeInstances, probeDevframeOrigin } from "devframe/internal";
|
|
2
|
+
import { LIST_CLIENTS_TOOL, diagnostics, listLiveDevframeInstances, probeDevframeOrigin } from "devframe/internal";
|
|
3
3
|
import { toAgentToolName } from "devframe/utils/agent-tool-name";
|
|
4
4
|
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
@@ -25,13 +25,15 @@ function buildInstanceRequestHeaders(url, token) {
|
|
|
25
25
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
26
26
|
return headers;
|
|
27
27
|
}
|
|
28
|
+
/** Wire name of the built-in tab-listing tool an instance exposes once a browser tab connects. */
|
|
29
|
+
const LIST_CLIENTS_NAME = toAgentToolName(LIST_CLIENTS_TOOL);
|
|
28
30
|
const INDEX_TOOL = toAgentToolName("devframe:connect:list-instances");
|
|
29
31
|
const CALL_TOOL = toAgentToolName("devframe:connect:call-tool");
|
|
30
32
|
const MCP_DISABLED_HINT = "This instance runs without an MCP route. Restart it with the --mcp flag to expose its tools, then list instances again.";
|
|
31
33
|
const GATEWAY_TOOLS = [{
|
|
32
34
|
name: INDEX_TOOL,
|
|
33
35
|
title: "Discover running devframes",
|
|
34
|
-
description: "Discover every running devframe dev server on this machine and list each one's MCP tools. Call this FIRST, before assuming which devtools are available; the result names the instance (id, project root, origin) and the port to pass to the call tool. Safe to call freely.",
|
|
36
|
+
description: "Discover every running devframe dev server on this machine and list each one's MCP tools, plus the browser tabs connected to it (`mcp.clients`). Call this FIRST, before assuming which devtools are available; the result names the instance (id, project root, origin) and the port to pass to the call tool. Safe to call freely.",
|
|
35
37
|
inputSchema: {
|
|
36
38
|
type: "object",
|
|
37
39
|
properties: {}
|
|
@@ -57,7 +59,7 @@ const GATEWAY_TOOLS = [{
|
|
|
57
59
|
},
|
|
58
60
|
args: {
|
|
59
61
|
type: "object",
|
|
60
|
-
description: "Arguments object for the tool. Omit for zero-argument tools."
|
|
62
|
+
description: "Arguments object for the tool. Omit for zero-argument tools. Tools forwarded from a browser tab accept `client_id` (from `mcp.clients` in list-instances) to target one tab; omitted, the most recently focused tab runs it."
|
|
61
63
|
}
|
|
62
64
|
},
|
|
63
65
|
required: ["port", "tool"],
|
|
@@ -125,7 +127,7 @@ async function index(options) {
|
|
|
125
127
|
try {
|
|
126
128
|
entry.mcp = {
|
|
127
129
|
url,
|
|
128
|
-
|
|
130
|
+
...await indexInstanceMcp(url, resolveAuthToken(options.authToken, record))
|
|
129
131
|
};
|
|
130
132
|
} catch (error) {
|
|
131
133
|
entry.mcp = {
|
|
@@ -163,8 +165,18 @@ async function probePort(port, base = "/", timeoutMs) {
|
|
|
163
165
|
startedAt: 0
|
|
164
166
|
};
|
|
165
167
|
}
|
|
166
|
-
async function
|
|
167
|
-
return withInstanceClient(url, token, async (client) =>
|
|
168
|
+
async function indexInstanceMcp(url, token) {
|
|
169
|
+
return withInstanceClient(url, token, async (client) => {
|
|
170
|
+
const tools = (await client.listTools()).tools;
|
|
171
|
+
if (!tools.some((tool) => tool.name === LIST_CLIENTS_NAME)) return { tools };
|
|
172
|
+
return {
|
|
173
|
+
tools,
|
|
174
|
+
clients: (await client.callTool({
|
|
175
|
+
name: LIST_CLIENTS_NAME,
|
|
176
|
+
arguments: {}
|
|
177
|
+
})).structuredContent?.clients ?? []
|
|
178
|
+
};
|
|
179
|
+
});
|
|
168
180
|
}
|
|
169
181
|
async function call(options, args) {
|
|
170
182
|
if (typeof args.port !== "number" || typeof args.tool !== "string") throw diagnostics.DF0049();
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import { Server, createMcpHandler } from "@modelcontextprotocol/server";
|
|
3
|
+
import { ProtocolError, ProtocolErrorCode, Server, createMcpHandler } from "@modelcontextprotocol/server";
|
|
4
4
|
import { DEVFRAME_EVENTS } from "devframe/constants";
|
|
5
5
|
import { argsToJsonSchema, diagnostics, formatMcpError, getMcpContent, returnToJsonSchema, stringifyForMcp } from "devframe/internal";
|
|
6
6
|
import { createHostContext } from "devframe/node";
|
|
@@ -29,19 +29,31 @@ function buildMcpServerFromContext(ctx, options) {
|
|
|
29
29
|
}, {
|
|
30
30
|
capabilities: {
|
|
31
31
|
tools: { listChanged: true },
|
|
32
|
-
resources: { listChanged: true }
|
|
32
|
+
resources: { listChanged: true },
|
|
33
|
+
...options.prompts?.size ? { prompts: { listChanged: false } } : {}
|
|
33
34
|
},
|
|
34
35
|
...options.instructions ? { instructions: options.instructions } : {}
|
|
35
36
|
});
|
|
36
37
|
registerToolHandlers(server, ctx, options.exposeSharedState);
|
|
37
|
-
|
|
38
|
+
const templates = options.resourceTemplates ?? [];
|
|
39
|
+
registerResourceHandlers(server, ctx, options.exposeSharedState, templates);
|
|
40
|
+
if (templates.length) server.setRequestHandler("resources/templates/list", async () => ({ resourceTemplates: templates.map(({ template: { uriTemplate, name, title, description, mimeType } }) => ({
|
|
41
|
+
uriTemplate,
|
|
42
|
+
name,
|
|
43
|
+
title,
|
|
44
|
+
description,
|
|
45
|
+
mimeType
|
|
46
|
+
})) }));
|
|
47
|
+
if (options.prompts?.size) registerPromptHandlers(server, options.prompts);
|
|
38
48
|
return server;
|
|
39
49
|
}
|
|
40
50
|
/**
|
|
41
51
|
* Publish devframe's `list_changed` events through a set of typed sinks:
|
|
42
52
|
* `tools()` for tool-list changes and `resources()` for resource-list
|
|
43
|
-
* changes (shared-state keys are surfaced as resources).
|
|
44
|
-
*
|
|
53
|
+
* changes (shared-state keys are surfaced as resources). An optional host
|
|
54
|
+
* subscription (`onResourcesChanged`) also publishes through `resources()`
|
|
55
|
+
* each time it calls `notify()`. Returns an unsubscribe function, which also
|
|
56
|
+
* removes the host subscription; a `notify()` after that does nothing.
|
|
45
57
|
*
|
|
46
58
|
* The HTTP path passes the handler's `notify` bus sugar; the stdio path
|
|
47
59
|
* passes the pinned server's `send*ListChanged` methods, which `serveStdio`
|
|
@@ -49,7 +61,7 @@ function buildMcpServerFromContext(ctx, options) {
|
|
|
49
61
|
*
|
|
50
62
|
* @internal
|
|
51
63
|
*/
|
|
52
|
-
function bridgeListChanged(ctx, sinks) {
|
|
64
|
+
function bridgeListChanged(ctx, sinks, onResourcesChanged) {
|
|
53
65
|
const offManifest = ctx.agent.events.on(DEVFRAME_EVENTS.bus.agentManifestChanged, () => {
|
|
54
66
|
sinks.tools();
|
|
55
67
|
sinks.resources();
|
|
@@ -57,9 +69,15 @@ function bridgeListChanged(ctx, sinks) {
|
|
|
57
69
|
const offKeyAdded = ctx.rpc.sharedState.onKeyAdded(() => {
|
|
58
70
|
sinks.resources();
|
|
59
71
|
});
|
|
72
|
+
let active = true;
|
|
73
|
+
const offHost = onResourcesChanged?.(() => {
|
|
74
|
+
if (active) sinks.resources();
|
|
75
|
+
}, ctx);
|
|
60
76
|
return () => {
|
|
77
|
+
active = false;
|
|
61
78
|
offManifest();
|
|
62
79
|
offKeyAdded();
|
|
80
|
+
offHost?.();
|
|
63
81
|
};
|
|
64
82
|
}
|
|
65
83
|
/**
|
|
@@ -72,6 +90,8 @@ async function createMcpServer(definition, options = {}) {
|
|
|
72
90
|
transport,
|
|
73
91
|
reason: "Only stdio transport is supported in this release."
|
|
74
92
|
});
|
|
93
|
+
const resourceTemplates = compileResourceTemplates(options.resourceTemplates);
|
|
94
|
+
const prompts = compilePrompts(options.prompts);
|
|
75
95
|
const ctx = await createHostContext({
|
|
76
96
|
cwd: process.cwd(),
|
|
77
97
|
mode: "dev",
|
|
@@ -93,7 +113,9 @@ async function createMcpServer(definition, options = {}) {
|
|
|
93
113
|
serverName: options.serverName ?? `${definition.id} (devframe)`,
|
|
94
114
|
serverVersion: options.serverVersion ?? definition.version ?? "0.0.0",
|
|
95
115
|
exposeSharedState: options.exposeSharedState ?? true,
|
|
96
|
-
instructions: options.instructions
|
|
116
|
+
instructions: options.instructions,
|
|
117
|
+
resourceTemplates,
|
|
118
|
+
prompts
|
|
97
119
|
};
|
|
98
120
|
let handle;
|
|
99
121
|
try {
|
|
@@ -107,7 +129,7 @@ async function createMcpServer(definition, options = {}) {
|
|
|
107
129
|
resources: () => {
|
|
108
130
|
server.sendResourceListChanged().catch(() => {});
|
|
109
131
|
}
|
|
110
|
-
});
|
|
132
|
+
}, options.onResourcesChanged);
|
|
111
133
|
const priorOnClose = server.onclose;
|
|
112
134
|
server.onclose = () => {
|
|
113
135
|
unbridge();
|
|
@@ -238,7 +260,7 @@ function registerToolHandlers(server, ctx, exposeSharedState) {
|
|
|
238
260
|
}
|
|
239
261
|
});
|
|
240
262
|
}
|
|
241
|
-
function registerResourceHandlers(server, ctx, exposeSharedState) {
|
|
263
|
+
function registerResourceHandlers(server, ctx, exposeSharedState, templates) {
|
|
242
264
|
const stateFilter = sharedStateFilter(exposeSharedState);
|
|
243
265
|
server.setRequestHandler("resources/list", async () => {
|
|
244
266
|
const resources = ctx.agent.list().resources.map((resource) => ({
|
|
@@ -256,6 +278,10 @@ function registerResourceHandlers(server, ctx, exposeSharedState) {
|
|
|
256
278
|
mimeType: "application/json"
|
|
257
279
|
});
|
|
258
280
|
}
|
|
281
|
+
for (const { template } of templates) for (const entry of await template.list?.(ctx) ?? []) resources.push({
|
|
282
|
+
...entry,
|
|
283
|
+
mimeType: entry.mimeType ?? template.mimeType
|
|
284
|
+
});
|
|
259
285
|
return { resources };
|
|
260
286
|
});
|
|
261
287
|
server.setRequestHandler("resources/read", async (request) => {
|
|
@@ -278,10 +304,143 @@ function registerResourceHandlers(server, ctx, exposeSharedState) {
|
|
|
278
304
|
text: stringifyForMcp(state.value())
|
|
279
305
|
}] };
|
|
280
306
|
}
|
|
307
|
+
for (const { template, match } of templates) {
|
|
308
|
+
const params = match(uri);
|
|
309
|
+
if (!params) continue;
|
|
310
|
+
const content = await template.read(params, uri, ctx);
|
|
311
|
+
return { contents: [{
|
|
312
|
+
uri,
|
|
313
|
+
mimeType: content.mimeType ?? template.mimeType ?? "application/json",
|
|
314
|
+
text: content.text ?? stringifyForMcp(content.json)
|
|
315
|
+
}] };
|
|
316
|
+
}
|
|
281
317
|
throw new Error(`[devframe/mcp] unknown resource URI "${uri}"`);
|
|
282
318
|
});
|
|
283
319
|
}
|
|
284
320
|
/**
|
|
321
|
+
* Index prompts by name, once per endpoint. A repeated name throws
|
|
322
|
+
* `DF0085`, since `prompts/get` finds a prompt by its name.
|
|
323
|
+
*
|
|
324
|
+
* @internal
|
|
325
|
+
*/
|
|
326
|
+
function compilePrompts(prompts = []) {
|
|
327
|
+
const byName = /* @__PURE__ */ new Map();
|
|
328
|
+
for (const prompt of prompts) {
|
|
329
|
+
if (byName.has(prompt.name)) throw diagnostics.DF0085({ name: prompt.name });
|
|
330
|
+
byName.set(prompt.name, prompt);
|
|
331
|
+
}
|
|
332
|
+
return byName;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Serve `prompts/list` and `prompts/get`. An unknown name, or a required
|
|
336
|
+
* argument that is missing or empty, is an invalid-params error. An error
|
|
337
|
+
* thrown by `get` goes to the MCP client.
|
|
338
|
+
*/
|
|
339
|
+
function registerPromptHandlers(server, prompts) {
|
|
340
|
+
server.setRequestHandler("prompts/list", async () => ({ prompts: [...prompts.values()].map(({ name, title, description, arguments: args }) => ({
|
|
341
|
+
name,
|
|
342
|
+
title,
|
|
343
|
+
description,
|
|
344
|
+
...args?.length ? { arguments: args.map((argument) => ({
|
|
345
|
+
name: argument.name,
|
|
346
|
+
description: argument.description,
|
|
347
|
+
required: argument.required
|
|
348
|
+
})) } : {}
|
|
349
|
+
})) }));
|
|
350
|
+
server.setRequestHandler("prompts/get", async (request) => {
|
|
351
|
+
const { name, arguments: given = {} } = request.params;
|
|
352
|
+
const prompt = prompts.get(name);
|
|
353
|
+
if (!prompt) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `[devframe/mcp] unknown prompt "${name}"`);
|
|
354
|
+
const missing = (prompt.arguments ?? []).filter((argument) => argument.required && !(Object.hasOwn(given, argument.name) && given[argument.name])).map((argument) => argument.name);
|
|
355
|
+
if (missing.length) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `[devframe/mcp] prompt "${name}" needs the argument(s): ${missing.join(", ")}`);
|
|
356
|
+
const { description, messages } = await prompt.get(given);
|
|
357
|
+
return {
|
|
358
|
+
description,
|
|
359
|
+
messages
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/** One `{name}` expression of a URI template. */
|
|
364
|
+
const TEMPLATE_EXPRESSION_RE = /\{([^{}]*)\}/;
|
|
365
|
+
/** RFC 6570 `varname`: letters, digits and `_`, with single dots between them. */
|
|
366
|
+
const TEMPLATE_VARNAME_RE = /^\w+(?:\.\w+)*$/;
|
|
367
|
+
/**
|
|
368
|
+
* Compile resource templates to URI matchers, once per endpoint. Only RFC
|
|
369
|
+
* 6570 level 1 is supported: plain `{name}` expressions. Each expression
|
|
370
|
+
* matches a non-empty value with no `/`, and its value is percent-decoded,
|
|
371
|
+
* so an encoded `%2F` reads back as `/`. A URI with malformed
|
|
372
|
+
* percent-encoding matches no template. Any other template syntax, and two
|
|
373
|
+
* expressions with nothing between them, throw `DF0084`.
|
|
374
|
+
*
|
|
375
|
+
* @internal
|
|
376
|
+
*/
|
|
377
|
+
function compileResourceTemplates(templates = []) {
|
|
378
|
+
return templates.map((template) => {
|
|
379
|
+
const { name, uriTemplate } = template;
|
|
380
|
+
const invalid = (reason) => diagnostics.DF0084({
|
|
381
|
+
name,
|
|
382
|
+
uriTemplate,
|
|
383
|
+
reason
|
|
384
|
+
});
|
|
385
|
+
const parts = uriTemplate.split(TEMPLATE_EXPRESSION_RE);
|
|
386
|
+
const literals = parts.filter((_, index) => index % 2 === 0);
|
|
387
|
+
const names = parts.filter((_, index) => index % 2 === 1);
|
|
388
|
+
if (literals.some((literal) => /[{}]/.test(literal))) throw invalid("a `{` or `}` is outside an expression.");
|
|
389
|
+
names.forEach((varname, index) => {
|
|
390
|
+
if (!TEMPLATE_VARNAME_RE.test(varname)) throw invalid(`the expression "{${varname}}" is not a plain \`{name}\`.`);
|
|
391
|
+
if (names.indexOf(varname) !== index) throw invalid(`the name "${varname}" is used more than once.`);
|
|
392
|
+
if (index > 0 && literals[index] === "") throw invalid(`"{${names[index - 1]}}" and "{${varname}}" have nothing between them, so a URI cannot show where one ends.`);
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
template,
|
|
396
|
+
match: (uri) => matchTemplate(uri, literals, names)
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Match `uri` in one pass from left to right, with no regular expression, so
|
|
402
|
+
* the time grows with the URI length only and a crafted URI cannot block the
|
|
403
|
+
* process. Each value runs to the first place the next literal appears
|
|
404
|
+
* outside a `%XX` escape (the last value runs to the tail). A value must not
|
|
405
|
+
* be empty or contain `/`.
|
|
406
|
+
* The params object has no prototype, so a variable such as `__proto__`
|
|
407
|
+
* keeps its value.
|
|
408
|
+
*/
|
|
409
|
+
function matchTemplate(uri, literals, names) {
|
|
410
|
+
const head = literals[0];
|
|
411
|
+
const tail = literals.at(-1);
|
|
412
|
+
const params = Object.create(null);
|
|
413
|
+
if (names.length === 0) return uri === head ? params : void 0;
|
|
414
|
+
if (!uri.startsWith(head) || !uri.endsWith(tail)) return void 0;
|
|
415
|
+
const end = uri.length - tail.length;
|
|
416
|
+
let position = head.length;
|
|
417
|
+
for (let index = 0; index < names.length; index++) {
|
|
418
|
+
const isLast = index === names.length - 1;
|
|
419
|
+
const next = literals[index + 1];
|
|
420
|
+
const stop = isLast ? end : findLiteral(uri, next, position);
|
|
421
|
+
if (stop <= position) return void 0;
|
|
422
|
+
const raw = uri.slice(position, stop);
|
|
423
|
+
if (raw.includes("/")) return void 0;
|
|
424
|
+
try {
|
|
425
|
+
params[names[index]] = decodeURIComponent(raw);
|
|
426
|
+
} catch {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
position = stop + (isLast ? 0 : next.length);
|
|
430
|
+
}
|
|
431
|
+
return params;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The first place `literal` appears after the value that starts at `start`
|
|
435
|
+
* (which must not be empty), or -1. A place inside a `%XX` escape of the
|
|
436
|
+
* value is skipped, so `{a}D{b}` reads `a%2DbDc` as `a-b` and `c`.
|
|
437
|
+
*/
|
|
438
|
+
function findLiteral(uri, literal, start) {
|
|
439
|
+
let at = uri.indexOf(literal, start + 1);
|
|
440
|
+
while (at !== -1 && (uri[at - 1] === "%" || at - 2 >= start && uri[at - 2] === "%")) at = uri.indexOf(literal, at + 1);
|
|
441
|
+
return at;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
285
444
|
* The content blocks a tool result carries through `withMcpContent`, checked
|
|
286
445
|
* and copied field by field, or `undefined` when it carries none. Invalid
|
|
287
446
|
* blocks throw DF0081, which the `tools/call` handler turns into an
|
|
@@ -429,11 +588,15 @@ async function isAuthorized(req, authorization) {
|
|
|
429
588
|
function createMcpFetchHandler(ctx, options) {
|
|
430
589
|
const allowedOrigins = options.allowedOrigins;
|
|
431
590
|
const authorization = options.authorization ?? false;
|
|
591
|
+
const resourceTemplates = compileResourceTemplates(options.resourceTemplates);
|
|
592
|
+
const prompts = compilePrompts(options.prompts);
|
|
432
593
|
const handler = createMcpHandler(() => buildMcpServerFromContext(ctx, {
|
|
433
594
|
serverName: options.serverName,
|
|
434
595
|
serverVersion: options.serverVersion,
|
|
435
596
|
exposeSharedState: options.exposeSharedState,
|
|
436
|
-
instructions: options.instructions
|
|
597
|
+
instructions: options.instructions,
|
|
598
|
+
resourceTemplates,
|
|
599
|
+
prompts
|
|
437
600
|
}));
|
|
438
601
|
const unbridge = bridgeListChanged(ctx, {
|
|
439
602
|
tools: () => {
|
|
@@ -442,7 +605,7 @@ function createMcpFetchHandler(ctx, options) {
|
|
|
442
605
|
resources: () => {
|
|
443
606
|
handler.notify.resourcesChanged();
|
|
444
607
|
}
|
|
445
|
-
});
|
|
608
|
+
}, options.onResourcesChanged);
|
|
446
609
|
const originOnlyDefault = allowedOrigins === void 0 && authorization === false;
|
|
447
610
|
async function handle(req, connection) {
|
|
448
611
|
const origin = req.headers.get("origin") ?? void 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devframes/agentic",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.1",
|
|
5
5
|
"description": "Agent-surface implementation for devframe: installing it next to devframe enables the MCP adapter (devframe/adapters/mcp) and the devframe connect gateway.",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -30,19 +30,19 @@
|
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"devframe": "1.
|
|
33
|
+
"devframe": "1.3.1"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
37
37
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
38
|
-
"h3": "^2.0.
|
|
38
|
+
"h3": "^2.0.7",
|
|
39
39
|
"pathe": "^2.0.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@standard-schema/spec": "^1.1.0",
|
|
43
|
-
"@types/node": "^26.6.
|
|
44
|
-
"devframe": "1.
|
|
45
|
-
"tsdown": "
|
|
43
|
+
"@types/node": "^26.6.5",
|
|
44
|
+
"devframe": "1.3.1",
|
|
45
|
+
"tsdown": "0.23.0",
|
|
46
46
|
"valibot": "^1.5.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|