@nylorun/runtime 0.1.1-beta

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.
@@ -0,0 +1,589 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Hono } from "hono";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { agUiEvents, sse } from "./ag-ui.js";
5
+ import { observedPayload } from "./digests.js";
6
+ import { memoryHistory, scrub, } from "../adapters/journal.js";
7
+ import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, } from "../adapters/media.js";
8
+ import { projectSecrets } from "../model/settings.js";
9
+ export async function createRuntime(options) {
10
+ const agents = [...options.agents];
11
+ const byId = new Map(agents.map((agent) => [agent.id, agent]));
12
+ if (byId.size !== agents.length)
13
+ throw new Error("Agent IDs must be unique.");
14
+ for (const agent of agents) {
15
+ if (!/^[a-zA-Z0-9_-]+$/.test(agent.id))
16
+ throw new Error(`Invalid agent ID: ${agent.id}`);
17
+ if (agent.id !== agent.manifest.id || agent.name !== agent.manifest.name)
18
+ throw new Error("Agent identity must match its manifest.");
19
+ }
20
+ const media = options.media;
21
+ const journal = options.persistence ?? memoryHistory();
22
+ const redact = (value) => scrub(value, projectSecrets());
23
+ const live = new Map();
24
+ const app = new Hono();
25
+ app.onError((error, context) => context.json({ error: String(redact(error.message)) }, error instanceof HTTPException ? error.status : 500));
26
+ app.use("*", async (context, next) => {
27
+ await next();
28
+ if (context.res.headers.get("content-type")?.includes("application/json")) {
29
+ const value = await context.res.json();
30
+ context.res = new Response(JSON.stringify(redact(value)), {
31
+ status: context.res.status,
32
+ headers: context.res.headers,
33
+ });
34
+ }
35
+ });
36
+ app.use("*", async (context, next) => {
37
+ const origin = context.req.header("origin");
38
+ if (origin && permitted(origin, options.origins ?? [])) {
39
+ context.header("access-control-allow-origin", origin);
40
+ context.header("access-control-allow-headers", "content-type");
41
+ context.header("access-control-allow-methods", "GET, POST, OPTIONS");
42
+ context.header("vary", "origin");
43
+ }
44
+ if (context.req.method === "OPTIONS")
45
+ return context.body(null, 204);
46
+ await next();
47
+ });
48
+ app.get("/v1/agents", (context) => context.json({
49
+ protocolVersion: 2,
50
+ agents: agents.map((agent) => ({
51
+ id: agent.id,
52
+ manifestUrl: `/agents/${agent.id}/manifest.json`,
53
+ })),
54
+ }));
55
+ app.get("/agents/:agentId/manifest.json", (context) => {
56
+ const agent = byId.get(context.req.param("agentId"));
57
+ return agent === undefined
58
+ ? context.json({ error: "unknown agent" }, 404)
59
+ : context.json(manifest(agent, media !== undefined));
60
+ });
61
+ app.get("/agents/:agentId/v1/media/:session/:assetId", async (context) => {
62
+ const agent = requireAgent(context.req.param("agentId"));
63
+ if (!agent)
64
+ return context.json({ error: "unknown agent" }, 404);
65
+ const asset = await media?.read(agent.id, context.req.param("session"), context.req.param("assetId"));
66
+ if (!asset)
67
+ return context.json({ error: "unknown media asset" }, 404);
68
+ context.header("cache-control", "no-store");
69
+ return context.body(asset.bytes, 200, {
70
+ "content-type": asset.asset.mediaType,
71
+ });
72
+ });
73
+ app.get("/agents/:agentId/v1/sessions", async (context) => {
74
+ const agent = requireAgent(context.req.param("agentId"));
75
+ if (agent === undefined)
76
+ return context.json({ error: "unknown agent" }, 404);
77
+ const listed = await journal.list(agent.id);
78
+ return context.json({
79
+ sessions: listed.map((summary) => {
80
+ const found = live.get(keyOf(agent.id, summary.session));
81
+ if (found?.status === "running")
82
+ return { ...summary, status: "running" };
83
+ if (found?.status === "waiting")
84
+ return { ...summary, status: "waiting" };
85
+ return summary;
86
+ }),
87
+ });
88
+ });
89
+ app.get("/agents/:agentId/v1/sessions/:session", async (context) => {
90
+ const agent = requireAgent(context.req.param("agentId"));
91
+ if (!agent)
92
+ return context.json({ error: "unknown agent" }, 404);
93
+ const key = keyOf(agent.id, context.req.param("session"));
94
+ const found = live.get(key);
95
+ const events = found?.events ??
96
+ (await journal.events(agent.id, context.req.param("session")));
97
+ if (!found && events.length === 0)
98
+ return context.json({ error: "unknown session" }, 404);
99
+ return context.json({
100
+ id: context.req.param("session"),
101
+ state: found?.status ?? status(events),
102
+ pending_interaction: pending(events),
103
+ });
104
+ });
105
+ app.post("/agents/:agentId/v1/sessions/:session", async (context) => {
106
+ const agent = requireAgent(context.req.param("agentId"));
107
+ if (!agent)
108
+ return context.json({ error: "unknown agent" }, 404);
109
+ const found = live.get(keyOf(agent.id, context.req.param("session")));
110
+ if (!found)
111
+ return context.json({ error: "session is no longer live" }, 409);
112
+ const payload = await context.req
113
+ .json()
114
+ .catch(() => undefined);
115
+ const interaction = payload?.interaction;
116
+ if (!interaction || typeof interaction.id !== "string")
117
+ return context.json({ error: "expected interaction" }, 400);
118
+ const waiting = pending(found.events);
119
+ if (found.status !== "waiting" || !waiting || waiting.id !== interaction.id)
120
+ return context.json({ error: "interaction is no longer pending" }, 409);
121
+ if (interaction.kind === "approval") {
122
+ if (typeof interaction.approved !== "boolean")
123
+ return context.json({ error: "expected approval interaction" }, 400);
124
+ found.status = "running";
125
+ await submit(found, {
126
+ kind: "approve",
127
+ interactionId: interaction.id,
128
+ approved: interaction.approved,
129
+ });
130
+ return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
131
+ }
132
+ if (interaction.kind === "respond") {
133
+ if (!("value" in interaction))
134
+ return context.json({ error: "expected respond interaction" }, 400);
135
+ found.status = "running";
136
+ await submit(found, {
137
+ kind: "respond",
138
+ interactionId: interaction.id,
139
+ value: interaction.value,
140
+ });
141
+ return context.json({ session_id: context.req.param("session"), state: found.status }, 202);
142
+ }
143
+ return context.json({ error: "expected approval or respond interaction" }, 400);
144
+ });
145
+ app.get("/agents/:agentId/v1/sessions/:session/events", async (context) => {
146
+ const agent = requireAgent(context.req.param("agentId"));
147
+ if (!agent)
148
+ return context.json({ error: "unknown agent" }, 404);
149
+ const after = Number(context.req.query("after") ?? "0");
150
+ const session = context.req.param("session");
151
+ const events = live.get(keyOf(agent.id, session))?.events ??
152
+ (await journal.events(agent.id, session));
153
+ return context.json({
154
+ events: events.filter((event) => event.seq > after),
155
+ next_cursor: events.at(-1)?.seq ?? after,
156
+ });
157
+ });
158
+ app.get("/agents/:agentId/v1/ag-ui/sessions/:session", async (context) => {
159
+ const agent = requireAgent(context.req.param("agentId"));
160
+ if (!agent)
161
+ return context.json({ error: "unknown agent" }, 404);
162
+ const found = live.get(keyOf(agent.id, context.req.param("session")));
163
+ return context.json({
164
+ messages: found?.messages ??
165
+ messages(agent.id, await journal.events(agent.id, context.req.param("session"))),
166
+ });
167
+ });
168
+ app.post("/agents/:agentId/v1/ag-ui", async (context) => {
169
+ const agent = requireAgent(context.req.param("agentId"));
170
+ if (!agent)
171
+ return context.json({ error: "unknown agent" }, 404);
172
+ const payload = await context.req
173
+ .json()
174
+ .catch(() => undefined);
175
+ const threadId = typeof payload?.threadId === "string" && payload.threadId
176
+ ? payload.threadId
177
+ : randomUUID();
178
+ if (!isSessionId(threadId))
179
+ return context.json({ error: "threadId may only contain letters, digits, '.', '_' and '-'" }, 400);
180
+ const runId = typeof payload?.runId === "string" && payload.runId
181
+ ? payload.runId
182
+ : randomUUID();
183
+ let message;
184
+ try {
185
+ message = await latestMessage(payload?.messages, media, agent.id, threadId);
186
+ }
187
+ catch (error) {
188
+ return context.json({
189
+ error: error instanceof Error
190
+ ? error.message
191
+ : "Invalid AG-UI user message",
192
+ }, 400);
193
+ }
194
+ const found = await begin(agent, threadId, message);
195
+ const start = found.events.length;
196
+ await submit(found, message.input);
197
+ return sse(agUiEvents(found.events.slice(start), threadId, runId), context.res.headers);
198
+ });
199
+ function requireAgent(id) {
200
+ return byId.get(id);
201
+ }
202
+ async function begin(agent, sessionId, message) {
203
+ const key = keyOf(agent.id, sessionId);
204
+ const existing = live.get(key);
205
+ if (existing) {
206
+ existing.messages.push(message.chat);
207
+ return existing;
208
+ }
209
+ const archived = await journal.events(agent.id, sessionId);
210
+ const concurrent = live.get(key);
211
+ if (concurrent) {
212
+ concurrent.messages.push(message.chat);
213
+ return concurrent;
214
+ }
215
+ if (archived.length)
216
+ throw new HTTPException(409, {
217
+ message: "This session is archived. Start a new conversation.",
218
+ });
219
+ const session = agent.run({ id: sessionId });
220
+ const entry = {
221
+ session,
222
+ events: [],
223
+ messages: [message.chat],
224
+ status: "running",
225
+ sequence: 0,
226
+ writes: Promise.resolve(),
227
+ unsubscribe: () => { },
228
+ };
229
+ live.set(key, entry);
230
+ entry.unsubscribe = session.observe((event) => {
231
+ const image = generatedImageMessage(event, agent.id, sessionId);
232
+ if (image)
233
+ entry.messages.push(image);
234
+ add(entry, agent.id, event.type, observedPayload(event));
235
+ });
236
+ return entry;
237
+ }
238
+ function add(entry, agentId, type, payload) {
239
+ const event = {
240
+ session: entry.session.id,
241
+ seq: ++entry.sequence,
242
+ ts: new Date().toISOString(),
243
+ type,
244
+ payload: redact(payload),
245
+ };
246
+ entry.events.push(event);
247
+ entry.writes = entry.writes.then(() => journal.append(agentId, event));
248
+ // The submit/close paths observe persistence errors; avoid an unhandled rejection meanwhile.
249
+ void entry.writes.catch(() => { });
250
+ }
251
+ async function submit(entry, input) {
252
+ const agentId = [...live]
253
+ .find(([, value]) => value === entry)[0]
254
+ .split(":")[0];
255
+ entry.status = "running";
256
+ const inputEvent = typeof input === "string"
257
+ ? { kind: "user-message", text: input }
258
+ : "kind" in input
259
+ ? input
260
+ : { kind: "user-message", ...input };
261
+ const message = chatFromInput(inputEvent, agentId, entry.session.id);
262
+ add(entry, agentId, "session.run.started", {
263
+ input_kind: inputEvent.kind,
264
+ input: message ? firstText(message) : undefined,
265
+ ...(message ? { message } : {}),
266
+ ...("approved" in inputEvent ? { approved: inputEvent.approved } : {}),
267
+ ...("value" in inputEvent ? { value: inputEvent.value } : {}),
268
+ });
269
+ try {
270
+ const result = await entry.session.input(input).completed;
271
+ for (const event of result.events) {
272
+ if (event.type === "final" && event.output !== undefined) {
273
+ entry.messages.push({
274
+ id: randomUUID(),
275
+ role: "assistant",
276
+ content: finalContent(event.output),
277
+ });
278
+ add(entry, agentId, "final", { output: event.output });
279
+ }
280
+ else if (event.type === "interaction.required") {
281
+ add(entry, agentId, event.type, { interaction: event.interaction });
282
+ }
283
+ }
284
+ entry.status =
285
+ result.status === "waiting"
286
+ ? "waiting"
287
+ : result.status === "completed"
288
+ ? "completed"
289
+ : "failed";
290
+ await entry.writes;
291
+ }
292
+ catch (error) {
293
+ entry.status = "failed";
294
+ add(entry, agentId, "error", {
295
+ message: error instanceof Error ? error.message : String(error),
296
+ });
297
+ await entry.writes;
298
+ throw error;
299
+ }
300
+ }
301
+ let closing;
302
+ return Object.freeze({
303
+ app,
304
+ hasSession: (agentId, sessionId) => live.has(keyOf(agentId, sessionId)),
305
+ close: () => (closing ??= (async () => {
306
+ const sessions = await Promise.allSettled([...live.values()].map(async (entry) => {
307
+ try {
308
+ await entry.session.stop();
309
+ await entry.writes;
310
+ }
311
+ finally {
312
+ entry.unsubscribe();
313
+ }
314
+ }));
315
+ const resources = await Promise.allSettled(agents.map((agent) => agent.close?.()));
316
+ const failure = [...sessions, ...resources].find((result) => result.status === "rejected");
317
+ if (failure?.status === "rejected")
318
+ throw failure.reason;
319
+ })()),
320
+ });
321
+ }
322
+ function manifest(agent, media) {
323
+ return {
324
+ protocolVersion: 2,
325
+ id: agent.manifest.id,
326
+ name: agent.manifest.name,
327
+ manifest: agent.manifest,
328
+ endpoints: {
329
+ agUi: `/agents/${agent.id}/v1/ag-ui`,
330
+ sessions: `/agents/${agent.id}/v1/sessions`,
331
+ },
332
+ ...(media
333
+ ? {
334
+ mediaInput: {
335
+ acceptedTypes: IMAGE_MEDIA_TYPES,
336
+ maxBytes: MAX_IMAGE_BYTES,
337
+ },
338
+ }
339
+ : {}),
340
+ };
341
+ }
342
+ function keyOf(agent, session) {
343
+ return `${agent}:${session}`;
344
+ }
345
+ async function latestMessage(value, media, agentId, sessionId) {
346
+ if (!Array.isArray(value))
347
+ throw new Error("AG-UI requires a user message.");
348
+ for (let i = value.length - 1; i >= 0; i -= 1) {
349
+ const item = value[i];
350
+ if (item?.role !== "user")
351
+ continue;
352
+ const content = await incomingContent(item.content, media, agentId, sessionId);
353
+ if (content)
354
+ return content;
355
+ }
356
+ throw new Error("AG-UI requires a non-empty user message.");
357
+ }
358
+ function pending(events) {
359
+ for (let i = events.length - 1; i >= 0; i -= 1) {
360
+ if (events[i].type === "interaction.required")
361
+ return events[i].payload.interaction;
362
+ if (events[i].type === "final")
363
+ return undefined;
364
+ }
365
+ return undefined;
366
+ }
367
+ function status(events) {
368
+ return pending(events)
369
+ ? "waiting"
370
+ : events.some((event) => event.type === "final")
371
+ ? "completed"
372
+ : "incomplete";
373
+ }
374
+ function messages(agentId, events) {
375
+ return events.flatMap((event) => event.type === "final" && "output" in event.payload
376
+ ? [
377
+ {
378
+ id: String(event.seq),
379
+ role: "assistant",
380
+ content: finalContent(event.payload.output),
381
+ },
382
+ ]
383
+ : event.type === "session.run.started" &&
384
+ isChatMessage(event.payload.message)
385
+ ? [
386
+ {
387
+ ...event.payload.message,
388
+ id: String(event.seq),
389
+ },
390
+ ]
391
+ : generatedImageFromEvent(event, agentId)
392
+ ? [generatedImageFromEvent(event, agentId)]
393
+ : []);
394
+ }
395
+ async function incomingContent(value, media, agentId, sessionId) {
396
+ const parts = [];
397
+ const chat = [];
398
+ if (typeof value === "string") {
399
+ if (value.trim() === "")
400
+ return undefined;
401
+ parts.push({ type: "text", text: value });
402
+ chat.push({ type: "text", text: value });
403
+ }
404
+ else if (Array.isArray(value)) {
405
+ let images = 0;
406
+ for (const raw of value) {
407
+ const part = raw;
408
+ if (part?.type === "text" && typeof part.text === "string") {
409
+ if (part.text !== "") {
410
+ parts.push({ type: "text", text: part.text });
411
+ chat.push({ type: "text", text: part.text });
412
+ }
413
+ continue;
414
+ }
415
+ if (part?.type !== "image")
416
+ throw new Error("Only text and image inputs are supported.");
417
+ if (++images > 1)
418
+ throw new Error("Attach only one image per message.");
419
+ const source = part.source;
420
+ if (!source ||
421
+ source.type !== "data" ||
422
+ typeof source.value !== "string" ||
423
+ typeof source.mimeType !== "string")
424
+ throw new Error("Image input must contain base64 data and a media type.");
425
+ if (!media)
426
+ throw new Error("This runtime does not support image input.");
427
+ const asset = await media.saveInput(agentId, sessionId, source.mimeType, source.value);
428
+ parts.push({
429
+ type: "media",
430
+ mediaType: asset.mediaType,
431
+ reference: { agentId, assetId: asset.id },
432
+ });
433
+ chat.push({
434
+ type: "image",
435
+ url: mediaUrl(agentId, sessionId, asset.id),
436
+ mediaType: asset.mediaType,
437
+ });
438
+ }
439
+ }
440
+ else
441
+ return undefined;
442
+ if (parts.length === 0)
443
+ return undefined;
444
+ return Object.freeze({
445
+ input: { content: Object.freeze(parts) },
446
+ chat: { id: randomUUID(), role: "user", content: Object.freeze(chat) },
447
+ });
448
+ }
449
+ function chatFromInput(event, agentId, sessionId) {
450
+ if (event.kind !== "user-message")
451
+ return undefined;
452
+ if (typeof event.text === "string")
453
+ return {
454
+ id: randomUUID(),
455
+ role: "user",
456
+ content: [{ type: "text", text: event.text }],
457
+ };
458
+ if (!event.content)
459
+ return undefined;
460
+ const content = event.content.flatMap((part) => {
461
+ if (part.type === "text")
462
+ return [{ type: "text", text: part.text }];
463
+ const reference = part.reference;
464
+ return typeof reference.assetId === "string"
465
+ ? [
466
+ {
467
+ type: "image",
468
+ url: mediaUrl(agentId, sessionId, reference.assetId),
469
+ mediaType: part.mediaType,
470
+ },
471
+ ]
472
+ : [];
473
+ });
474
+ return content.length === 0
475
+ ? undefined
476
+ : { id: randomUUID(), role: "user", content };
477
+ }
478
+ function finalContent(output) {
479
+ return typeof output === "string"
480
+ ? [{ type: "text", text: output }]
481
+ : [{ type: "json", value: output }];
482
+ }
483
+ function generatedImageMessage(event, agentId, sessionId) {
484
+ if (event.type !== "tool.completed")
485
+ return undefined;
486
+ const image = imageFromToolResult(event.attributes);
487
+ return image
488
+ ? {
489
+ id: randomUUID(),
490
+ role: "assistant",
491
+ content: [
492
+ {
493
+ type: "image",
494
+ url: mediaUrl(agentId, sessionId, image.id),
495
+ mediaType: image.mediaType,
496
+ },
497
+ ],
498
+ }
499
+ : undefined;
500
+ }
501
+ function generatedImageFromEvent(event, agentId) {
502
+ if (event.type !== "tool.completed")
503
+ return undefined;
504
+ const image = imageFromToolResult(event.payload.attributes);
505
+ return image
506
+ ? {
507
+ id: String(event.seq),
508
+ role: "assistant",
509
+ content: [
510
+ {
511
+ type: "image",
512
+ url: mediaUrl(agentId, event.session, image.id),
513
+ mediaType: image.mediaType,
514
+ },
515
+ ],
516
+ }
517
+ : undefined;
518
+ }
519
+ function imageFromToolResult(value) {
520
+ if (!value || typeof value !== "object")
521
+ return undefined;
522
+ const result = value;
523
+ if (result.kind !== "completed" ||
524
+ !result.output ||
525
+ typeof result.output !== "object")
526
+ return undefined;
527
+ const image = result.output.image;
528
+ return image &&
529
+ typeof image.id === "string" &&
530
+ typeof image.mediaType === "string" &&
531
+ image.kind === "generated"
532
+ ? image
533
+ : undefined;
534
+ }
535
+ function isChatMessage(value) {
536
+ if (!value || typeof value !== "object")
537
+ return false;
538
+ const message = value;
539
+ return (typeof message.id === "string" &&
540
+ typeof message.role === "string" &&
541
+ Array.isArray(message.content));
542
+ }
543
+ function firstText(message) {
544
+ return message.content.find((part) => part.type === "text")?.text;
545
+ }
546
+ /** Session IDs name journal and media files; keep them path-safe up front. */
547
+ function isSessionId(value) {
548
+ return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
549
+ }
550
+ function mediaUrl(agentId, sessionId, assetId) {
551
+ return `/agents/${encodeURIComponent(agentId)}/v1/media/${encodeURIComponent(sessionId)}/${encodeURIComponent(assetId)}`;
552
+ }
553
+ /** Host headers a loopback bind answers to, on the port actually in use. */
554
+ export function loopbackHosts(port) {
555
+ return [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`];
556
+ }
557
+ /**
558
+ * Decide whether a request's Host header is served. Entries are exact
559
+ * `host:port` values or bare host names that accept any port; `*` accepts all.
560
+ */
561
+ export function allowedHost(header, allowed) {
562
+ if (allowed.includes("*"))
563
+ return true;
564
+ if (!header)
565
+ return false;
566
+ let hostname;
567
+ try {
568
+ hostname = new URL(`http://${header}`).hostname;
569
+ }
570
+ catch {
571
+ return false;
572
+ }
573
+ const bare = hostname.replace(/^\[(.*)\]$/u, "$1");
574
+ return allowed.some((entry) => entry.toLowerCase() === header.toLowerCase() ||
575
+ entry.toLowerCase() === hostname ||
576
+ entry.toLowerCase() === bare);
577
+ }
578
+ function permitted(origin, configured) {
579
+ try {
580
+ const url = new URL(origin);
581
+ return (configured.includes(origin) ||
582
+ url.hostname === "localhost" ||
583
+ url.hostname.endsWith(".localhost") ||
584
+ url.hostname === "127.0.0.1");
585
+ }
586
+ catch {
587
+ return false;
588
+ }
589
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@nylorun/runtime",
3
+ "version": "0.1.1-beta",
4
+ "description": "Portable agent hosting, model providers, and the Nylorun CLI.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/nylorun/harness.git",
10
+ "directory": "runtime"
11
+ },
12
+ "engines": {
13
+ "node": ">=22.19.0"
14
+ },
15
+ "bin": {
16
+ "nylorun": "dist/cli.js"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ },
34
+ "scripts": {
35
+ "build": "node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json",
36
+ "check": "npm run build && npm test && node scripts/check-package.mjs",
37
+ "typecheck": "node ../scripts/check-typescript-version.mjs && tsc -p tsconfig.json --noEmit",
38
+ "test": "vitest run",
39
+ "prepack": "npm run build"
40
+ },
41
+ "dependencies": {
42
+ "@earendil-works/pi-ai": "0.84.4",
43
+ "@hono/node-server": "^1.19.10",
44
+ "hono": "^4.11.10",
45
+ "vite": "^8.2.2"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.18.0",
49
+ "@typescript/native": "npm:typescript@^7.0.2",
50
+ "typescript": "npm:@typescript/typescript6@^6.0.2",
51
+ "vitest": "^4.1.11"
52
+ }
53
+ }