@mgvdev/nestjs-ai 0.1.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +655 -0
  3. package/dist/ai-module-options.interface-B5X4AFLC.d.cts +192 -0
  4. package/dist/ai-module-options.interface-Ca6y_MY2.d.ts +192 -0
  5. package/dist/conversation-store.interface-CtQY-qcc.d.cts +30 -0
  6. package/dist/conversation-store.interface-CtQY-qcc.d.ts +30 -0
  7. package/dist/index.cjs +3209 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +1441 -0
  10. package/dist/index.d.ts +1441 -0
  11. package/dist/index.js +3163 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/stream-to-socket-fJphU0WN.d.cts +47 -0
  14. package/dist/stream-to-socket-fJphU0WN.d.ts +47 -0
  15. package/dist/testing.cjs +2395 -0
  16. package/dist/testing.cjs.map +1 -0
  17. package/dist/testing.d.cts +46 -0
  18. package/dist/testing.d.ts +46 -0
  19. package/dist/testing.js +2391 -0
  20. package/dist/testing.js.map +1 -0
  21. package/dist/typeorm.cjs +115 -0
  22. package/dist/typeorm.cjs.map +1 -0
  23. package/dist/typeorm.d.cts +44 -0
  24. package/dist/typeorm.d.ts +44 -0
  25. package/dist/typeorm.js +113 -0
  26. package/dist/typeorm.js.map +1 -0
  27. package/dist/websocket.cjs +154 -0
  28. package/dist/websocket.cjs.map +1 -0
  29. package/dist/websocket.d.cts +25 -0
  30. package/dist/websocket.d.ts +25 -0
  31. package/dist/websocket.js +152 -0
  32. package/dist/websocket.js.map +1 -0
  33. package/documentation/README.md +44 -0
  34. package/documentation/agents-and-tools.md +102 -0
  35. package/documentation/api-reference.md +117 -0
  36. package/documentation/configuration.md +87 -0
  37. package/documentation/content-safety.md +51 -0
  38. package/documentation/embeddings-and-rag.md +105 -0
  39. package/documentation/evals-and-testing.md +56 -0
  40. package/documentation/getting-started.md +96 -0
  41. package/documentation/guardrails-events-telemetry.md +72 -0
  42. package/documentation/jobs-and-realtime.md +69 -0
  43. package/documentation/memory.md +101 -0
  44. package/documentation/multimodal.md +49 -0
  45. package/documentation/orchestration-and-mcp.md +68 -0
  46. package/documentation/prompts.md +51 -0
  47. package/documentation/reliability.md +90 -0
  48. package/documentation/structured-output-and-streaming.md +81 -0
  49. package/package.json +146 -0
  50. package/skill/nestjs-ai/SKILL.md +154 -0
@@ -0,0 +1,101 @@
1
+ # Memory
2
+
3
+ Two complementary systems: **conversation history** (raw turns) and **semantic
4
+ memory** (embedding-based recall).
5
+
6
+ ## Conversation history
7
+
8
+ Pass a `conversationId` to load prior turns before a run and persist the new
9
+ exchange after.
10
+
11
+ ```ts
12
+ await agent.run('What about tomorrow?', { conversationId: user.id });
13
+ ```
14
+
15
+ The default `ConversationStore` is in-memory. Provide your own by implementing
16
+ the interface:
17
+
18
+ ```ts
19
+ interface ConversationStore {
20
+ load(conversationId: string): Promise<AiMessage[]>;
21
+ append(conversationId: string, messages: AiMessage[]): Promise<void>;
22
+ clear(conversationId: string): Promise<void>;
23
+ }
24
+ ```
25
+
26
+ ### TypeORM store
27
+
28
+ From `@mgvdev/nestjs-ai/typeorm` (needs `typeorm` + `@nestjs/typeorm`):
29
+
30
+ ```ts
31
+ import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm';
32
+ import { ConversationMessageEntity, TypeOrmConversationStore } from '@mgvdev/nestjs-ai/typeorm';
33
+
34
+ @Module({
35
+ imports: [
36
+ TypeOrmModule.forFeature([ConversationMessageEntity]),
37
+ AiModule.forRoot({
38
+ providers: { openai: { apiKey } },
39
+ conversationStore: {
40
+ useFactory: (repo) => new TypeOrmConversationStore(repo),
41
+ inject: [getRepositoryToken(ConversationMessageEntity)],
42
+ },
43
+ }),
44
+ ],
45
+ })
46
+ export class AppModule {}
47
+ ```
48
+
49
+ ### Prisma store
50
+
51
+ No build-time dependency — pass a model delegate:
52
+
53
+ ```ts
54
+ import { PrismaConversationStore } from '@mgvdev/nestjs-ai';
55
+
56
+ AiModule.forRoot({
57
+ conversationStore: {
58
+ useFactory: (prisma: PrismaService) => new PrismaConversationStore(prisma.aiMessage),
59
+ inject: [PrismaService],
60
+ },
61
+ });
62
+ ```
63
+
64
+ ```prisma
65
+ model AiMessage {
66
+ id String @id @default(cuid())
67
+ conversationId String
68
+ role String
69
+ content Json
70
+ position Int
71
+ createdAt DateTime @default(now())
72
+ @@index([conversationId])
73
+ }
74
+ ```
75
+
76
+ ## Semantic memory
77
+
78
+ `SemanticMemory` stores snippets per conversation as embeddings and recalls the
79
+ most relevant ones — useful for long-term facts beyond raw turns.
80
+
81
+ ```ts
82
+ await this.memory.remember(conversationId, 'The user prefers dark mode.');
83
+
84
+ // recall relevant snippets:
85
+ const hits = await this.memory.recall(conversationId, 'settings', { topK: 3 });
86
+
87
+ // summarize a conversation and store it (requires AiService):
88
+ await this.memory.rememberConversation(conversationId, messages, { summarize: true });
89
+ ```
90
+
91
+ ### Automatic recall in a run
92
+
93
+ ```ts
94
+ await agent.run('what are my settings?', {
95
+ conversationId,
96
+ recall: { topK: 3 }, // recalled snippets are prepended to the system prompt
97
+ });
98
+ ```
99
+
100
+ Semantic memory uses the configured `VectorStore` (isolated per conversation via
101
+ metadata), so it works with in-memory, pgvector, Qdrant, or Pinecone.
@@ -0,0 +1,49 @@
1
+ # Multimodal
2
+
3
+ Three injectable services wrap the AI SDK's image, speech, and transcription
4
+ APIs. Set `defaultImageModel` / `defaultSpeechModel` /
5
+ `defaultTranscriptionModel`, or pass `model` per call.
6
+
7
+ ## Image generation
8
+
9
+ ```ts
10
+ import { ImageService } from '@mgvdev/nestjs-ai';
11
+
12
+ const { image, images } = await this.images.generate('a fox in the snow', {
13
+ model: 'openai:dall-e-3',
14
+ size: '1024x1024',
15
+ aspectRatio: '16:9',
16
+ n: 1,
17
+ });
18
+ // image: GeneratedFile (base64 / bytes)
19
+ ```
20
+
21
+ ## Speech (text-to-speech)
22
+
23
+ ```ts
24
+ import { SpeechService } from '@mgvdev/nestjs-ai';
25
+
26
+ const { audio } = await this.speech.generate('Hello there', {
27
+ model: 'openai:tts-1',
28
+ voice: 'alloy',
29
+ outputFormat: 'mp3',
30
+ });
31
+ ```
32
+
33
+ ## Transcription (speech-to-text)
34
+
35
+ ```ts
36
+ import { TranscriptionService } from '@mgvdev/nestjs-ai';
37
+
38
+ const { text, segments } = await this.transcription.transcribe(audioBuffer, {
39
+ model: 'openai:whisper-1',
40
+ });
41
+ ```
42
+
43
+ `audio` accepts a `Uint8Array`, `ArrayBuffer`, `Buffer`, base64 string, or `URL`.
44
+
45
+ ## Provider support
46
+
47
+ Availability depends on the provider SDK. OpenAI supports image, speech, and
48
+ transcription; other providers vary. A clear error is thrown when a configured
49
+ provider does not support the requested capability.
@@ -0,0 +1,68 @@
1
+ # Orchestration & MCP
2
+
3
+ ## Multi-agent orchestration
4
+
5
+ Reference an `@Agent` class in another agent's `tools` to delegate to it
6
+ (supervisor / handoff). The sub-agent's output text is returned to the caller.
7
+
8
+ ```ts
9
+ @Agent({ model: 'openai:gpt-4o', system: 'Research facts.' })
10
+ export class ResearchAgent extends AiAgent {}
11
+
12
+ @Agent({ model: 'openai:gpt-4o', system: 'Write the final answer.' })
13
+ export class WriterAgent extends AiAgent {}
14
+
15
+ @Agent({
16
+ model: 'openai:gpt-4o',
17
+ system: 'Coordinate specialists to answer the user.',
18
+ tools: [ResearchAgent, WriterAgent], // sub-agents used as tools
19
+ })
20
+ export class SupervisorAgent extends AiAgent {}
21
+ ```
22
+
23
+ Register all three as providers. The supervisor calls each sub-agent by its class
24
+ name.
25
+
26
+ ### `AgentRegistry`
27
+
28
+ Indexes all agents by class name — used by orchestration and background jobs.
29
+
30
+ ```ts
31
+ this.agents.get('ResearchAgent'); // instance
32
+ this.agents.all(); // AgentEntry[]
33
+ ```
34
+
35
+ ### `createAgentTool`
36
+
37
+ Wrap an agent as a tool manually:
38
+
39
+ ```ts
40
+ import { createAgentTool } from '@mgvdev/nestjs-ai';
41
+ const tool = createAgentTool(researchAgent, { name: 'research', description: '…' });
42
+ ```
43
+
44
+ ## MCP (Model Context Protocol)
45
+
46
+ Adapt tools from an MCP server into an agent tool set. Bring your own client from
47
+ `@modelcontextprotocol/sdk` — any object with `listTools` / `callTool` / `close`.
48
+
49
+ ```ts
50
+ import { McpService } from '@mgvdev/nestjs-ai';
51
+
52
+ // connect once (e.g. on module init)
53
+ const toolSet = await this.mcp.connect('filesystem', mcpClient);
54
+
55
+ // use the tools
56
+ await this.ai.generateText({ model: 'openai:gpt-4o', tools: toolSet, prompt });
57
+
58
+ // or fetch later / merge
59
+ this.mcp.getToolSet('filesystem');
60
+ this.mcp.getAllTools();
61
+ ```
62
+
63
+ Clients are closed automatically on module destroy. MCP tools are exposed to the
64
+ model via the AI SDK's `dynamicTool`, with input schemas passed through from the
65
+ server's JSON schema.
66
+
67
+ > AI SDK v7 does not ship an MCP client; use `@modelcontextprotocol/sdk` directly
68
+ > to construct the transport + client, then hand it to `McpService`.
@@ -0,0 +1,51 @@
1
+ # Prompt registry
2
+
3
+ Register named, versioned prompt templates and render them with `{{var}}`
4
+ interpolation.
5
+
6
+ ## Register
7
+
8
+ ```ts
9
+ AiModule.forRoot({
10
+ prompts: [
11
+ { name: 'support', version: 'v1', template: 'Help {{user}} with {{topic}}.' },
12
+ { name: 'support', version: 'v2', template: 'Assist {{user}} regarding {{topic}}.' },
13
+ ],
14
+ });
15
+ ```
16
+
17
+ Also via `AiModule.forFeature({ prompts })`, or imperatively:
18
+
19
+ ```ts
20
+ this.prompts.register({ name: 'greeting', template: 'Hi {{name}}!' });
21
+ ```
22
+
23
+ ## Render
24
+
25
+ ```ts
26
+ this.prompts.render('support', { user: 'Ada', topic: 'billing' });
27
+ // latest version by default; pass { version: 'v1' } to pin
28
+ this.prompts.render('support', { user: 'Ada', topic: 'billing' }, { version: 'v1' });
29
+ ```
30
+
31
+ Unknown variables throw, so typos are caught early. Duplicate `(name, version)`
32
+ registration throws.
33
+
34
+ ## Use as an agent's system prompt
35
+
36
+ Resolve the system prompt per call from the registry:
37
+
38
+ ```ts
39
+ await agent.run(question, {
40
+ systemPrompt: { name: 'support', vars: { user, topic }, version: 'v2' },
41
+ });
42
+ ```
43
+
44
+ This overrides the agent's static `system`.
45
+
46
+ ## API
47
+
48
+ - `register(def)` / `registerAll(defs)`
49
+ - `render(name, vars?, { version? })`
50
+ - `get(name, version?)` — the raw `PromptDefinition`
51
+ - `has(name)`
@@ -0,0 +1,90 @@
1
+ # Reliability
2
+
3
+ Fallback models, retries, caching, rate limiting, tool approval, and cost budgets.
4
+
5
+ ## Fallback models & retries
6
+
7
+ Pass a **model array** to configure a fallback chain — each model is tried in
8
+ order when the previous one throws.
9
+
10
+ ```ts
11
+ @Agent({ model: ['openai:gpt-4o', 'anthropic:claude-sonnet-4'] })
12
+ export class ResilientAgent extends AiAgent {}
13
+
14
+ // or per call, plus SDK-native retries:
15
+ await agent.run(prompt, { model: ['openai:gpt-4o', 'openai:gpt-4o-mini'], maxRetries: 3 });
16
+ ```
17
+
18
+ Build a composite model manually with `createFallbackModel(models, { shouldRetry })`.
19
+
20
+ ## Caching
21
+
22
+ Memoize model responses (via middleware) and single embeddings.
23
+
24
+ ```ts
25
+ AiModule.forRoot({
26
+ providers: { openai: { apiKey } },
27
+ cache: InMemoryAiCache, // or a class/factory/value implementing AiCache
28
+ cacheTtlMs: 60_000,
29
+ });
30
+ ```
31
+
32
+ Implement `AiCache` (`get` / `set`) for Redis or another backend. Cache keys are
33
+ derived from the model id and call parameters.
34
+
35
+ ## Rate limiting
36
+
37
+ Throttle runs per conversation (or `"global"`).
38
+
39
+ ```ts
40
+ AiModule.forRoot({
41
+ providers: { openai: { apiKey } },
42
+ rateLimiter: {
43
+ useValue: new InMemoryRateLimiter({ capacity: 10, refillTokens: 10, intervalMs: 60_000 }),
44
+ },
45
+ });
46
+ ```
47
+
48
+ A throttled run throws `RateLimitedError`. Implement `RateLimiter`
49
+ (`consume(key, cost?)`) for a distributed limiter.
50
+
51
+ ## Tool approval (human-in-the-loop)
52
+
53
+ Flag a tool with `requiresApproval` and register an `ApprovalGate`.
54
+
55
+ ```ts
56
+ @Tool({ description: 'Delete a record', schema: z.object({ id: z.string() }), requiresApproval: true })
57
+ deleteRecord({ id }: { id: string }) { /* … */ }
58
+
59
+ @Injectable()
60
+ class QueueApprovalGate implements ApprovalGate {
61
+ async requestApproval({ tool, args }: ApprovalContext) {
62
+ return askAHuman(tool, args); // resolve true to allow, false to block
63
+ }
64
+ }
65
+
66
+ AiModule.forRoot({ providers: { openai: { apiKey } }, approvalGate: QueueApprovalGate });
67
+ ```
68
+
69
+ Defaults `AutoApproveGate` / `DenyApproveGate` are provided. A denied call throws
70
+ `ToolApprovalDeniedError`.
71
+
72
+ ## Cost tracking & budgets
73
+
74
+ Token usage and USD cost are tracked per conversation. Set a budget to block
75
+ runaway spend.
76
+
77
+ ```ts
78
+ AiModule.forRoot({
79
+ providers: { openai: { apiKey } },
80
+ maxCostPerConversation: 0.5, // blocks the next run once $0.50 is reached
81
+ pricing: { 'gpt-4o': { input: 2.5, output: 10 } }, // override defaults ($/1M tokens)
82
+ });
83
+
84
+ // inspect anytime
85
+ const { cost, inputTokens, runs } = this.usage.totals(conversationId);
86
+ ```
87
+
88
+ Listen with `@OnEvent('ai.usage')`. The budget check throws `BudgetExceededError`.
89
+ Costs use `DEFAULT_PRICING` (a small built-in table) merged with your `pricing`
90
+ overrides; unknown models cost `0`.
@@ -0,0 +1,81 @@
1
+ # Structured output & streaming
2
+
3
+ ## Structured output
4
+
5
+ Declare an `output` schema on the agent (or pass `schema` per call) to receive a
6
+ validated object instead of text.
7
+
8
+ ```ts
9
+ @Agent({
10
+ model: 'openai:gpt-4o',
11
+ system: 'Extract the order details.',
12
+ output: z.object({ product: z.string(), quantity: z.number() }),
13
+ })
14
+ export class OrderAgent extends AiAgent {}
15
+
16
+ const { object } = await orderAgent.run<{ product: string; quantity: number }>(text);
17
+ ```
18
+
19
+ Per call:
20
+
21
+ ```ts
22
+ const { object } = await agent.run(input, {
23
+ schema: z.object({ sentiment: z.enum(['positive', 'neutral', 'negative']) }),
24
+ });
25
+ ```
26
+
27
+ Structured-output runs use `generateObject` under the hood; the result's
28
+ `object` is validated against the schema.
29
+
30
+ ## Streaming
31
+
32
+ `.stream()` returns the raw Vercel AI SDK stream result. Iterate `textStream` or
33
+ pipe it to an HTTP response.
34
+
35
+ ```ts
36
+ const result = agent.stream('Tell me a story');
37
+ for await (const delta of result.textStream) {
38
+ process.stdout.write(delta);
39
+ }
40
+ ```
41
+
42
+ ### Stream to an HTTP response
43
+
44
+ ```ts
45
+ import { pipeAgentStream } from '@mgvdev/nestjs-ai';
46
+
47
+ @Post('chat')
48
+ chat(@Body('prompt') prompt: string, @Res() res: Response) {
49
+ pipeAgentStream(agent.stream(prompt), res, { protocol: 'ui' }); // 'ui' | 'text'
50
+ }
51
+ ```
52
+
53
+ - `'ui'` → AI SDK UI message stream (for `useChat` on the frontend).
54
+ - `'text'` → a plain text stream.
55
+
56
+ ### With an interceptor
57
+
58
+ ```ts
59
+ import { AgentStreamInterceptor } from '@mgvdev/nestjs-ai';
60
+
61
+ @UseInterceptors(new AgentStreamInterceptor({ protocol: 'ui' }))
62
+ @Post('chat')
63
+ chat(@Body('prompt') prompt: string) {
64
+ return agent.stream(prompt); // returned stream result is piped automatically
65
+ }
66
+ ```
67
+
68
+ ### Streaming structured objects
69
+
70
+ When the agent has an `output` schema, `.stream()` returns a `streamObject`
71
+ result — iterate `partialObjectStream` for progressive objects.
72
+
73
+ ### Realtime over WebSocket
74
+
75
+ See [Background jobs & realtime](./jobs-and-realtime.md) for `AgentGateway`
76
+ (`@mgvdev/nestjs-ai/websocket`).
77
+
78
+ ## Persistence during streaming
79
+
80
+ When you pass a `conversationId`, the new user turn and the model's response are
81
+ persisted on stream finish (`onFinish`), just like `run()`.
package/package.json ADDED
@@ -0,0 +1,146 @@
1
+ {
2
+ "name": "@mgvdev/nestjs-ai",
3
+ "version": "0.1.0",
4
+ "description": "Structured AI toolkit for NestJS — agents, tools, structured output, streaming and embeddings on top of the Vercel AI SDK.",
5
+ "author": "Maxence Guyonvarho",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "nestjs",
9
+ "ai",
10
+ "llm",
11
+ "agents",
12
+ "tools",
13
+ "openai",
14
+ "anthropic",
15
+ "vercel-ai-sdk",
16
+ "embeddings"
17
+ ],
18
+ "type": "module",
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "require": "./dist/index.cjs"
27
+ },
28
+ "./typeorm": {
29
+ "types": "./dist/typeorm.d.ts",
30
+ "import": "./dist/typeorm.js",
31
+ "require": "./dist/typeorm.cjs"
32
+ },
33
+ "./websocket": {
34
+ "types": "./dist/websocket.d.ts",
35
+ "import": "./dist/websocket.js",
36
+ "require": "./dist/websocket.cjs"
37
+ },
38
+ "./testing": {
39
+ "types": "./dist/testing.d.ts",
40
+ "import": "./dist/testing.js",
41
+ "require": "./dist/testing.cjs"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "documentation",
47
+ "skill",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit",
57
+ "prepublishOnly": "npm run build"
58
+ },
59
+ "peerDependencies": {
60
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
61
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
62
+ "@nestjs/event-emitter": "^2.0.0 || ^3.0.0",
63
+ "@nestjs/typeorm": "^10.0.0 || ^11.0.0",
64
+ "ai": "^7.0.0",
65
+ "reflect-metadata": "^0.1.13 || ^0.2.0",
66
+ "rxjs": "^7.0.0",
67
+ "typeorm": "^0.3.0 || ^1.0.0",
68
+ "zod": "^3.23.0 || ^4.0.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@ai-sdk/openai": {
72
+ "optional": true
73
+ },
74
+ "@ai-sdk/anthropic": {
75
+ "optional": true
76
+ },
77
+ "@ai-sdk/google": {
78
+ "optional": true
79
+ },
80
+ "@nestjs/event-emitter": {
81
+ "optional": true
82
+ },
83
+ "@nestjs/typeorm": {
84
+ "optional": true
85
+ },
86
+ "typeorm": {
87
+ "optional": true
88
+ },
89
+ "bullmq": {
90
+ "optional": true
91
+ },
92
+ "pg": {
93
+ "optional": true
94
+ },
95
+ "@modelcontextprotocol/sdk": {
96
+ "optional": true
97
+ },
98
+ "@nestjs/websockets": {
99
+ "optional": true
100
+ },
101
+ "@nestjs/platform-socket.io": {
102
+ "optional": true
103
+ },
104
+ "socket.io": {
105
+ "optional": true
106
+ },
107
+ "@qdrant/js-client-rest": {
108
+ "optional": true
109
+ },
110
+ "@pinecone-database/pinecone": {
111
+ "optional": true
112
+ },
113
+ "msw": {
114
+ "optional": true
115
+ }
116
+ },
117
+ "devDependencies": {
118
+ "@ai-sdk/anthropic": "^3.0.95",
119
+ "@ai-sdk/google": "^3.0.90",
120
+ "@ai-sdk/openai": "^3.0.82",
121
+ "@nestjs/common": "^11.0.0",
122
+ "@nestjs/core": "^11.0.0",
123
+ "@nestjs/event-emitter": "^3.1.0",
124
+ "@nestjs/platform-socket.io": "^11.1.28",
125
+ "@nestjs/testing": "^11.0.0",
126
+ "@nestjs/typeorm": "^11.0.3",
127
+ "@nestjs/websockets": "^11.1.28",
128
+ "@swc/core": "^1.15.43",
129
+ "@types/node": "^22.0.0",
130
+ "ai": "^7.0.17",
131
+ "better-sqlite3": "^12.11.1",
132
+ "msw": "^2.15.0",
133
+ "reflect-metadata": "^0.2.2",
134
+ "rxjs": "^7.8.1",
135
+ "socket.io": "^4.8.3",
136
+ "tsup": "^8.3.0",
137
+ "typeorm": "^1.0.0",
138
+ "typescript": "^5.6.0",
139
+ "unplugin-swc": "^1.5.9",
140
+ "vitest": "^2.1.0",
141
+ "zod": "^3.23.0"
142
+ },
143
+ "engines": {
144
+ "node": ">=22"
145
+ }
146
+ }