From 62ce47b7bac65c62e8c39413490cd835de79183f Mon Sep 17 00:00:00 2001 From: Aryan_GIG Date: Sat, 1 Aug 2026 15:45:56 +0530 Subject: [PATCH] add team aryan industry mcp sample --- .../.agents/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.agents/skills/ui-widgets/SKILL.md | 251 + .../skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.antigravity/skills/ui-widgets/SKILL.md | 251 + .../.claude/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.claude/skills/ui-widgets/SKILL.md | 251 + .../.codex/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.codex/skills/ui-widgets/SKILL.md | 251 + .../.copilot/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.copilot/skills/ui-widgets/SKILL.md | 251 + .../.cursor/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.cursor/skills/ui-widgets/SKILL.md | 251 + sample-apps/industry4.0mcp/.env.example | 22 + .../.gemini/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.gemini/skills/ui-widgets/SKILL.md | 251 + sample-apps/industry4.0mcp/.gitignore | 57 + .../.opencode/skills/auth-security/SKILL.md | 141 + .../skills/mcp-app-architecture/SKILL.md | 176 + .../skills/middleware-pipeline/SKILL.md | 235 + .../skills/tools-resources-prompts/SKILL.md | 287 ++ .../.opencode/skills/ui-widgets/SKILL.md | 251 + sample-apps/industry4.0mcp/README.md | 260 + sample-apps/industry4.0mcp/package-lock.json | 4578 +++++++++++++++++ sample-apps/industry4.0mcp/package.json | 39 + sample-apps/industry4.0mcp/src/app.module.ts | 27 + sample-apps/industry4.0mcp/src/index.ts | 29 + .../src/modules/industry/industry.data.ts | 47 + .../src/modules/industry/industry.module.ts | 9 + .../src/modules/industry/industry.prompts.ts | 39 + .../src/modules/industry/industry.tools.ts | 176 + .../industry4.0mcp/src/widgets/app/layout.tsx | 18 + .../industry4.0mcp/src/widgets/next-env.d.ts | 5 + .../industry4.0mcp/src/widgets/next.config.js | 45 + .../industry4.0mcp/src/widgets/package.json | 32 + .../industry4.0mcp/src/widgets/tsconfig.json | 28 + sample-apps/industry4.0mcp/tsconfig.json | 23 + 57 files changed, 14154 insertions(+) create mode 100644 sample-apps/industry4.0mcp/.agents/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.agents/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.agents/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.agents/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.agents/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.antigravity/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.antigravity/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.antigravity/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.antigravity/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.antigravity/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.claude/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.claude/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.claude/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.claude/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.claude/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.codex/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.codex/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.codex/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.codex/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.codex/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.copilot/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.copilot/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.copilot/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.copilot/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.copilot/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.cursor/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.cursor/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.cursor/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.cursor/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.cursor/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.env.example create mode 100644 sample-apps/industry4.0mcp/.gemini/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.gemini/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.gemini/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.gemini/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.gemini/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.gitignore create mode 100644 sample-apps/industry4.0mcp/.opencode/skills/auth-security/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.opencode/skills/mcp-app-architecture/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.opencode/skills/middleware-pipeline/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.opencode/skills/tools-resources-prompts/SKILL.md create mode 100644 sample-apps/industry4.0mcp/.opencode/skills/ui-widgets/SKILL.md create mode 100644 sample-apps/industry4.0mcp/README.md create mode 100644 sample-apps/industry4.0mcp/package-lock.json create mode 100644 sample-apps/industry4.0mcp/package.json create mode 100644 sample-apps/industry4.0mcp/src/app.module.ts create mode 100644 sample-apps/industry4.0mcp/src/index.ts create mode 100644 sample-apps/industry4.0mcp/src/modules/industry/industry.data.ts create mode 100644 sample-apps/industry4.0mcp/src/modules/industry/industry.module.ts create mode 100644 sample-apps/industry4.0mcp/src/modules/industry/industry.prompts.ts create mode 100644 sample-apps/industry4.0mcp/src/modules/industry/industry.tools.ts create mode 100644 sample-apps/industry4.0mcp/src/widgets/app/layout.tsx create mode 100644 sample-apps/industry4.0mcp/src/widgets/next-env.d.ts create mode 100644 sample-apps/industry4.0mcp/src/widgets/next.config.js create mode 100644 sample-apps/industry4.0mcp/src/widgets/package.json create mode 100644 sample-apps/industry4.0mcp/src/widgets/tsconfig.json create mode 100644 sample-apps/industry4.0mcp/tsconfig.json diff --git a/sample-apps/industry4.0mcp/.agents/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.agents/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.agents/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.agents/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.agents/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.agents/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.agents/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.agents/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.agents/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.agents/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.agents/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.agents/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.agents/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.agents/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.agents/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.antigravity/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.antigravity/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.antigravity/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.antigravity/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.antigravity/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.antigravity/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.antigravity/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.antigravity/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.antigravity/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.antigravity/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.antigravity/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.antigravity/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.antigravity/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.antigravity/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.antigravity/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.claude/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.claude/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.claude/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.claude/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.claude/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.claude/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.claude/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.claude/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.claude/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.claude/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.claude/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.claude/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.claude/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.claude/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.claude/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.codex/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.codex/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.codex/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.codex/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.codex/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.codex/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.codex/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.codex/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.codex/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.codex/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.codex/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.codex/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.codex/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.codex/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.codex/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.copilot/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.copilot/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.copilot/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.copilot/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.copilot/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.copilot/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.copilot/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.copilot/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.copilot/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.copilot/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.copilot/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.copilot/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.copilot/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.copilot/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.copilot/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.cursor/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.cursor/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.cursor/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.cursor/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.cursor/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.cursor/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.cursor/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.cursor/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.cursor/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.cursor/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.cursor/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.cursor/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.cursor/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.cursor/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.cursor/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.env.example b/sample-apps/industry4.0mcp/.env.example new file mode 100644 index 000000000..0c3f65fa5 --- /dev/null +++ b/sample-apps/industry4.0mcp/.env.example @@ -0,0 +1,22 @@ +# NitroStack Configuration +NITRO_LOG_LEVEL=info +NITROSTACK_APP_MODE=universal + +# Server Transport Configuration (Optional) +# ============================================================================= +# MCP_TRANSPORT_TYPE: Toggles transport mode. Values: stdio | http | dual. +# Defaults to 'stdio' in development and 'dual' in production/NODE_ENV=production. +# ============================================================================= +# MCP_TRANSPORT_TYPE=stdio +# PORT=3000 +# HOST=localhost +# ENABLE_CORS=true + +# Mapbox Configuration (Optional) +# ============================================================================= +# The map widget uses Mapbox GL for interactive maps. +# Get a free API key at: https://www.mapbox.com/ +# ============================================================================= + +# Your Mapbox Public Token (starts with pk.) +NEXT_PUBLIC_MAPBOX_TOKEN=pk.your_mapbox_token_here diff --git a/sample-apps/industry4.0mcp/.gemini/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.gemini/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.gemini/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.gemini/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.gemini/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.gemini/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.gemini/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.gemini/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.gemini/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.gemini/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.gemini/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.gemini/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.gemini/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.gemini/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.gemini/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/.gitignore b/sample-apps/industry4.0mcp/.gitignore new file mode 100644 index 000000000..b5b5532bc --- /dev/null +++ b/sample-apps/industry4.0mcp/.gitignore @@ -0,0 +1,57 @@ +# Dependencies +node_modules/ +src/widgets/node_modules/ + +# Build outputs +dist/ +src/widgets/.next/ +src/widgets/out/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage +coverage/ +.nyc_output/ + +# Uploads +uploads/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache +.npm/ + +# Optional eslint cache +.eslintcache + +# OAuth tokens/secrets (never commit these!) +*.pem +*.key +tokens.json diff --git a/sample-apps/industry4.0mcp/.opencode/skills/auth-security/SKILL.md b/sample-apps/industry4.0mcp/.opencode/skills/auth-security/SKILL.md new file mode 100644 index 000000000..17256a3e2 --- /dev/null +++ b/sample-apps/industry4.0mcp/.opencode/skills/auth-security/SKILL.md @@ -0,0 +1,141 @@ +--- +name: nitrostack-auth-security +description: Best practices for implementing JWT, API Keys, OAuth 2.1, and RBAC in a NitroStack application. +--- + +## When to Use +Use this skill when configuring security modules, implementing user authentication, restricting tool access via guards, or handling sensitive tokens. + +--- + +## 1. JSON Web Tokens (JWT) +To secure tools with JWT authentication: + +### Register `JWTModule`: +```typescript +import { JWTModule, Module, McpApp } from '@nitrostack/core'; + +@McpApp({ + server: { name: 'my-server', version: '1.0.0' } +}) +@Module({ + imports: [ + JWTModule.forRoot({ + secret: process.env.JWT_SECRET!, + expiresIn: '7d', + }), + ] +}) +export class AppModule {} +``` + +### Write a `JWTGuard`: +```typescript +import { Guard, ExecutionContext, Injectable, ConfigService } from '@nitrostack/core'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JWTGuard implements Guard { + constructor(private config: ConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const token = this.extractToken(context); + if (!token) return false; + + try { + const secret = this.config.get('JWT_SECRET'); + const payload = jwt.verify(token, secret) as any; + context.auth = { + subject: payload.sub, + role: payload.role, + token, + }; + return true; + } catch { + return false; + } + } + + private extractToken(context: ExecutionContext): string | null { + const auth = context.metadata?.authorization; + if (auth?.startsWith('Bearer ')) { + return auth.substring(7); + } + return null; + } +} +``` + +--- + +## 2. API Key Authentication +Use `ApiKeyModule` for service-to-service validation. + +### Register `ApiKeyModule`: +```typescript +import { ApiKeyModule, Module } from '@nitrostack/core'; + +@Module({ + imports: [ + ApiKeyModule.forRoot({ + keysEnvPrefix: 'API_KEY', // Reads API_KEY_1, API_KEY_2, etc. + headerName: 'x-api-key', + hashed: false, + }), + ] +}) +export class AppModule {} +``` + +### API Key Guard: +```typescript +import { Guard, ExecutionContext, ApiKeyModule } from '@nitrostack/core'; + +export class ApiKeyGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const apiKey = context.metadata?.['x-api-key'] || context.metadata?.apiKey; + if (!apiKey) return false; + + const isValid = await ApiKeyModule.validate(apiKey as string); + if (isValid) { + context.auth = { + subject: `apikey_${(apiKey as string).substring(0, 10)}`, + scopes: ['*'], + }; + return true; + } + return false; + } +} +``` + +--- + +## 3. Role-Based Access Control (RBAC) +Chain guards sequentially to implement user-role authorization. + +```typescript +import { Injectable, Guard, ExecutionContext, UseGuards, Tool, z } from '@nitrostack/core'; +import { JWTGuard } from './jwt.guard.js'; + +@Injectable() +export class AdminGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + // Requires JWTGuard to have populated context.auth first + return context.auth?.role === 'admin'; + } +} + +// Applying chained guards to a tool +export class SystemTools { + @Tool({ + name: 'reset_database', + description: 'Dangerous action: wipes database. Admin only.', + inputSchema: z.object({}), + }) + @UseGuards(JWTGuard, AdminGuard) // Chain auth first, then role check + async resetDatabase() { + return { success: true }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.opencode/skills/mcp-app-architecture/SKILL.md b/sample-apps/industry4.0mcp/.opencode/skills/mcp-app-architecture/SKILL.md new file mode 100644 index 000000000..fda31bdee --- /dev/null +++ b/sample-apps/industry4.0mcp/.opencode/skills/mcp-app-architecture/SKILL.md @@ -0,0 +1,176 @@ +--- +name: nitrostack-mcp-app-architecture +description: Best practices and guidelines for bootstrapping, defining modules, using dependency injection, managing server lifecycles, and handling events in the NitroStack SDK. +--- + +## When to Use +Use this skill whenever you are bootstrapping a new NitroStack MCP server, creating modules, injecting services, or handling application lifecycle events. + +## Bootstrapping a NitroStack App +A NitroStack application is initialized with the `@McpApp` decorator on a root class, accompanied by a root `@Module`. + +```typescript +import { McpApp, Module } from '@nitrostack/core'; +import { DatabaseModule } from './database/database.module.js'; +import { UsersModule } from './users/users.module.js'; + +@McpApp({ + module: AppModule, + server: { + name: 'user-management-server', + version: '1.0.0', + }, +}) +@Module({ + imports: [DatabaseModule, UsersModule], +}) +export class AppModule {} +``` + +## Modules +Modules organize your application structure. Use the `@Module` decorator to define imports, exports, and providers. + +* **`imports`**: Other modules whose exported providers should be available in this module. +* **`providers`**: Services, tools, resources, or prompts that should be instantiated and managed by the DI container within this module. +* **`exports`**: Providers defined in this module that should be visible to other modules importing this one. + +```typescript +import { Module } from '@nitrostack/core'; +import { UsersService } from './users.service.js'; +import { UsersTools } from './users.tools.js'; + +@Module({ + providers: [UsersService, UsersTools], + exports: [UsersService], +}) + +## Controllers +Use the `@ControllerDecorator` (or alias it as `@Controller`) to group tools, resources, and prompts together. Controllers are automatically registered as singletons in the DI container. + +### Key Controller Options: +* **`prefix`**: A string prefix applied to every `@Tool` defined in this controller. For example, `@ControllerDecorator('github')` prefixing a tool named `create_issue` exposes it to MCP clients as `github_create_issue`. + +```typescript +import { ControllerDecorator as Controller, Tool, ExecutionContext } from '@nitrostack/core'; + +@Controller('github') +export class GitHubController { + @Tool({ + name: 'create_issue', + description: 'Create an issue in a repository', + inputSchema: z.object({ /* ... */ }) + }) + async createIssue(input: any, ctx: ExecutionContext) { + // Exposed to clients as "github_create_issue" + } +} +``` + +## Dependency Injection (DI) +NitroStack uses a robust dependency injection container to manage class instances and lifecycles. + +### Injection Lifecycles +1. **Singleton (Default)**: A single instance is shared across the entire application. +2. **Transient**: A new instance is created every time it is resolved/injected. +3. **Scoped**: A new instance is created per incoming request or context. + +```typescript +import { Injectable, Scope } from '@nitrostack/core'; + +@Injectable({ scope: Scope.SINGLETON }) +export class UsersService { + constructor(private readonly db: DatabaseService) {} + + async getUser(id: string) { + return this.db.query('SELECT * FROM users WHERE id = $1', [id]); + } +} +``` + +## Lifecycles and Hooks +Implement NestJS-style lifecycle interfaces on modules, controllers, or providers to hook into application state changes: + +* **`OnModuleInit`** (`onModuleInit`): Called after modules have initialized but before the server starts listening. +* **`OnApplicationBootstrap`** (`onApplicationBootstrap`): Called once the server is fully started and listening. +* **`OnModuleDestroy`** (`onModuleDestroy`): Called when the module or application is shutting down. +* **`BeforeApplicationShutdown`** (`beforeApplicationShutdown(signal?: string)`): Called before the application starts shutting down. Receives the OS signal (e.g. `SIGINT`). +* **`OnApplicationShutdown`** (`onApplicationShutdown(signal?: string)`): Called during shutdown. Receives the OS signal. + +```typescript +import { + Injectable, + OnModuleInit, + OnApplicationBootstrap, + OnModuleDestroy, + BeforeApplicationShutdown, + OnApplicationShutdown +} from '@nitrostack/core'; + +@Injectable() +export class DatabaseService + implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown +{ + async onModuleInit() { + await this.connect(); + } + + async onApplicationBootstrap() { + console.log('App ready to handle connections.'); + } + + async onModuleDestroy() { + await this.cleanupPendingQueries(); + } + + async beforeApplicationShutdown(signal?: string) { + console.log(`Shutting down soon (signal: ${signal}).`); + } + + async onApplicationShutdown(signal?: string) { + await this.disconnect(); + } +} +``` + +--- + +## Eventing System (`emitEvent` and `@OnEvent`) +NitroStack includes an internal eventing system to decouple components. A service or tool can emit an event using `emitEvent`, and any injectable class (like a handler service or controller) can subscribe using the `@OnEvent` decorator. + +### 1. Emitting Events +Call `emitEvent` to dispatch an event payload asynchronously. + +```typescript +import { Injectable, emitEvent } from '@nitrostack/core'; + +@Injectable() +export class SpaceShipService { + async launchShip(shipId: string) { + // Process launch... + + // Dispatch event + emitEvent('ship.launched', { + shipId, + timestamp: new Date().toISOString(), + }); + } +} +``` + +### 2. Listening to Events +Decorate a method inside any `@Injectable()` class with `@OnEvent('event_pattern')` to register it as an event handler. + +```typescript +import { Injectable, OnEvent } from '@nitrostack/core'; + +@Injectable({ deps: [] }) +export class FlightLogHandler { + @OnEvent('ship.launched') + async logLaunch(data: { shipId: string; timestamp: string }) { + console.error(`🚀 [EVENT] Ship ${data.shipId} was successfully launched at ${data.timestamp}`); + } +} +``` + +> [!NOTE] +> For the `@OnEvent` decorator to register properly, the containing class must be declared as a provider inside an active module. diff --git a/sample-apps/industry4.0mcp/.opencode/skills/middleware-pipeline/SKILL.md b/sample-apps/industry4.0mcp/.opencode/skills/middleware-pipeline/SKILL.md new file mode 100644 index 000000000..dabe295ac --- /dev/null +++ b/sample-apps/industry4.0mcp/.opencode/skills/middleware-pipeline/SKILL.md @@ -0,0 +1,235 @@ +--- +name: nitrostack-middleware-pipeline +description: Best practices for implementing and applying Guards, Interceptors, Middleware, Pipes, and Exception Filters in the NitroStack SDK. +--- + +## When to Use +Use this skill when implementing request validation, authorization checks, response mapping, logging, error handling, or performance tracking on NitroStack tool methods. + +--- + +## 1. Guards (`Guard` and `@UseGuards`) +Guards determine if a request should be processed by a tool handler based on authentication, authorization, or other conditions. + +### Interface: +```typescript +import { Guard, ExecutionContext } from '@nitrostack/core'; + +export interface Guard { + canActivate(context: ExecutionContext): boolean | Promise; +} +``` + +### Example: +```typescript +import { Guard, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class RolesGuard implements Guard { + async canActivate(context: ExecutionContext): Promise { + const userRoles = context.clientMetadata?.roles || []; + return userRoles.includes('admin'); + } +} +``` + +Apply the guard using `@UseGuards(...)`: +```typescript +import { Tool, UseGuards, z } from '@nitrostack/core'; +import { RolesGuard } from './roles.guard.js'; + +export class AdminTools { + @Tool({ + name: 'delete_system_logs', + description: 'Delete all system logs from the server.', + inputSchema: z.object({}), + }) + @UseGuards(RolesGuard) + async deleteLogs() { + return { success: true }; + } +} +``` + +--- + +## 2. Interceptors (`InterceptorInterface` and `@UseInterceptors`) +Interceptors can transform/intercept input arguments or mapped output from a tool method execution. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface InterceptorInterface { + intercept(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { InterceptorInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class TimingInterceptor implements InterceptorInterface { + async intercept(context: ExecutionContext, next: () => Promise): Promise { + const start = Date.now(); + const result = await next(); + const duration = Date.now() - start; + context.logger.info(`Execution took ${duration}ms`); + return { + ...result, + _meta: { durationMs: duration } + }; + } +} +``` + +--- + +## 3. Exception Filters (`ExceptionFilterInterface` and `@UseFilters`) +Exception filters catch any errors thrown within guards, interceptors, or the tool handlers themselves, mapping them into user-friendly JSON payloads. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext): unknown | Promise; +} +``` + +### Example: +```typescript +import { ExceptionFilterInterface, ExecutionContext, Injectable } from '@nitrostack/core'; + +@Injectable() +export class CustomExceptionFilter implements ExceptionFilterInterface { + catch(exception: unknown, context: ExecutionContext) { + const message = exception instanceof Error ? exception.message : 'Unknown error'; + return { + error: true, + message, + timestamp: new Date().toISOString() + }; + } +} +``` + +Apply the filter using `@UseFilters(...)` on a tool method: + +```typescript +import { Tool, UseFilters, z } from '@nitrostack/core'; +import { CustomExceptionFilter } from './custom-exception.filter.js'; + +export class LoggingTools { + @Tool({ + name: 'generate_report', + description: 'Generates system usage reports.', + inputSchema: z.object({}), + }) + @UseFilters(CustomExceptionFilter) + async generateReport() { + throw new Error('Report generation is not implemented yet.'); + } +} +``` + +--- + +## 4. Middleware (`MiddlewareInterface`, `@Middleware` and `@UseMiddleware`) +Middleware executes before the request reaches the tool handler, and can wrap the handler execution by invoking `next()`. + +### Interface: +```typescript +import { ExecutionContext } from '@nitrostack/core'; + +export interface MiddlewareInterface { + use(context: ExecutionContext, next: () => Promise): Promise; +} +``` + +### Example: +```typescript +import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core'; + +@Middleware() +export class LoggingMiddleware implements MiddlewareInterface { + async use(context: ExecutionContext, next: () => Promise): Promise { + context.logger.info(`Entering tool: ${context.toolName}`); + try { + const result = await next(); + context.logger.info(`Exiting tool: ${context.toolName}`); + return result; + } catch (error) { + context.logger.error(`Error in tool: ${error}`); + throw error; + } + } +} +``` + +Apply the middleware using `@UseMiddleware(...)` on a tool method: +```typescript +import { Tool, UseMiddleware, z } from '@nitrostack/core'; +import { LoggingMiddleware } from './logging.middleware.js'; + +export class StationTools { + @Tool({ + name: 'fetch_logs', + description: 'Fetch station operations logs.', + inputSchema: z.object({}), + }) + @UseMiddleware(LoggingMiddleware) + async fetchLogs() { + return { status: 'operational' }; + } +} +``` + +--- + +## 5. Pipes (`PipeInterface`, `@Pipe` and `@UsePipes`) +Pipes are used to transform or validate input arguments before they reach the tool handler method. + +### Interface: +```typescript +import { ArgumentMetadata } from '@nitrostack/core'; + +export interface PipeInterface { + transform(value: T, metadata: ArgumentMetadata): R | Promise; +} +``` + +### Example: +```typescript +import { Pipe, PipeInterface, ArgumentMetadata } from '@nitrostack/core'; + +@Pipe() +export class TrimPipe implements PipeInterface, Record> { + transform(value: Record, metadata: ArgumentMetadata) { + const trimmed: Record = {}; + for (const [key, val] of Object.entries(value)) { + trimmed[key] = typeof val === 'string' ? val.trim() : val; + } + return trimmed; + } +} +``` + +Apply the pipe using `@UsePipes(...)` on a tool method: +```typescript +import { Tool, UsePipes, z } from '@nitrostack/core'; +import { TrimPipe } from './trim.pipe.js'; + +export class MessagingTools { + @Tool({ + name: 'send_message', + description: 'Send a message to other stations.', + inputSchema: z.object({ text: z.string() }), + }) + @UsePipes(TrimPipe) + async sendMessage(input: { text: string }) { + return { sentText: input.text }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.opencode/skills/tools-resources-prompts/SKILL.md b/sample-apps/industry4.0mcp/.opencode/skills/tools-resources-prompts/SKILL.md new file mode 100644 index 000000000..bf34b732d --- /dev/null +++ b/sample-apps/industry4.0mcp/.opencode/skills/tools-resources-prompts/SKILL.md @@ -0,0 +1,287 @@ +--- +name: nitrostack-tools-resources-prompts +description: Guidelines and patterns for defining Tools, Resources, and Prompts in a NitroStack application with schema validation via Zod, including caching, rate-limiting, and base64 file uploads. +--- + +## When to Use +Use this skill whenever you are defining, editing, or validating tools, resources, or prompts on a NitroStack MCP server. + +## Defining Tools with `@Tool` +An MCP tool exposes a function that an AI client can invoke. Decorate a service or controller method with `@Tool`. + +### Key Tool Options: +* `name`: Kebab-case or snake_case unique identifier. +* `description`: Detailed description explaining when and how the client should use it. +* `inputSchema`: A Zod object schema for strict validation of inputs. +* `outputSchema` (optional): Zod schema validating the output structure. + +```typescript +import { ToolDecorator as Tool, ControllerDecorator as Controller, InitialTool, z, ExecutionContext } from '@nitrostack/core'; + +@Controller('weather') +export class WeatherService { + @Tool({ + name: 'get_current_weather', + description: 'Get the current weather forecast for a specific city.', + inputSchema: z.object({ + city: z.string().describe('The name of the city, e.g., San Francisco'), + unit: z.enum(['celsius', 'fahrenheit']).default('celsius'), + }), + }) + @InitialTool() // Auto-invoked when the AI client initializes/starts + async getWeather( + input: { city: string; unit: 'celsius' | 'fahrenheit' }, + ctx: ExecutionContext + ) { + ctx.logger.info(`Fetching weather for ${input.city}`); + // implementation + return { + city: input.city, + temp: 22, + condition: 'Sunny', + }; + } +} +``` + +## Defining Resources with `@Resource` +An MCP resource exposes static or dynamic data files/URIs that the AI client can read. + +### Key Resource Options: +* `uri`: URI pattern (e.g., `git://{owner}/{repo}/file` or static `app://config`). +* `name`: Unique name of the resource. +* `description`: Explanation of what data this resource provides. +* `mimeType`: Mime type of the response (e.g., `text/plain`, `application/json`). + +```typescript +import { Resource, ExecutionContext } from '@nitrostack/core'; + +export class ConfigResources { + @Resource({ + uri: 'app://settings', + name: 'Application Settings', + description: 'System-wide configuration settings and parameters.', + mimeType: 'application/json', + }) + async getSettings(ctx: ExecutionContext) { + return { + environment: 'development', + debugMode: true, + }; + } +} +``` + +## Defining Prompts with `@Prompt` +An MCP prompt exposes reusable templates or instruction sets that guide LLMs. + +### Key Prompt Options: +* `name`: Name of the prompt. +* `description`: Describes what task this prompt helps accomplish. +* `arguments`: Declares parameters the client can supply to customize the prompt template. + +```typescript +import { Prompt, ExecutionContext } from '@nitrostack/core'; + +export class PromptTemplates { + @Prompt({ + name: 'code_review', + description: 'Provide an intensive code review for a given code snippet.', + arguments: [ + { name: 'language', description: 'The programming language, e.g., TypeScript', required: true }, + { name: 'code', description: 'The code snippet to review', required: true }, + ], + }) + async getCodeReviewPrompt( + args: { language: string; code: string }, + ctx: ExecutionContext + ) { + return { + messages: [ + { + role: 'user', + content: `You are an expert software engineer. Review this ${args.language} code:\n\n${args.code}`, + }, + ], + }; + } +} +``` + +--- + +## Tool Policies: Caching (`@Cache`) and Rate Limiting (`@RateLimit`) +You can control tool execution behaviors (such as performance optimization and throttling) using method decorators. + +### 1. Caching with `@Cache` +Use `@Cache` to cache tool execution outputs for a specified duration (TTL in seconds). This reduces database or API overhead for frequent identical requests. + +#### Options: +* `ttl`: Cache time-to-live in seconds (required). +* `key` (optional): Custom function `(input: any, context?: any) => string` that returns a unique cache key based on inputs. If not defined, a key is auto-generated from serialized input arguments. + +#### Example: +```typescript +import { ToolDecorator as Tool, Cache, z } from '@nitrostack/core'; + +export class StationTools { + @Tool({ + name: 'get_system_status', + description: 'Fetch real-time station metrics. Response is cached.', + inputSchema: z.object({}), + }) + @Cache({ ttl: 60 }) // Caches status for 60 seconds + async getSystemStatus() { + return { temperature: 21.5, oxygen: 0.98 }; + } + + @Tool({ + name: 'get_crew_status', + description: 'Fetch status of a crew member. Cached by crew ID.', + inputSchema: z.object({ id: z.string() }), + }) + @Cache({ + ttl: 300, + key: (input) => `crew:status:${input.id}` + }) + async getCrewStatus(input: { id: string }) { + // ... + } +} +``` + +### 2. Rate Limiting with `@RateLimit` +Use `@RateLimit` to restrict the number of tool invocations within a specified time window to prevent client abuse. + +#### Options: +* `requests`: Number of allowed requests in the window (required). +* `window`: Throttling duration window (required). Supports formats like `'1s'`, `'1m'`, `'1h'`. +* `key` (optional): Custom function `(context: ExecutionContext) => string` to group rate limits. Useful for rate-limiting per user role or API key. + +#### Example: +```typescript +import { ToolDecorator as Tool, RateLimit, z, ExecutionContext } from '@nitrostack/core'; + +export class DiagnosticTools { + @Tool({ + name: 'run_deep_diagnostic', + description: 'Run intensive diagnostics. Rate limited.', + inputSchema: z.object({}), + }) + @RateLimit({ requests: 3, window: '1m' }) // Max 3 requests per minute globally + async runDeepDiagnostic() { + return { diagnosticReport: 'All systems operational.' }; + } + + @Tool({ + name: 'request_supply_drop', + description: 'Request inventory supplies. Rate limited per user.', + inputSchema: z.object({ item: z.string() }), + }) + @RateLimit({ + requests: 5, + window: '1h', + key: (ctx: ExecutionContext) => ctx.auth?.subject || 'anonymous' + }) + async requestSupply(input: { item: string }, ctx: ExecutionContext) { + // ... + } +} +``` + +--- + +## Handling File Uploads in Tools +NitroStack supports file uploads from MCP clients (like NitroStudio) by passing the file as a base64-encoded string inside a tool's input parameters. + +### 1. Declaring Input Schema for File Uploads +To accept an uploaded file, define three Zod fields in your tool's `inputSchema`: +* `file_name`: The name of the file (e.g. `report.csv`). +* `file_type`: The MIME type (e.g. `text/csv`). +* `file_content`: The base64-encoded string containing the file data. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; + +export class FileTools { + @Tool({ + name: 'upload_document', + description: 'Upload a text document or image.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async uploadDocument(input: any, ctx: ExecutionContext) { + // Processing logic + } +} +``` + +### 2. Decoding Base64 Payloads +File uploads can arrive in two formats depending on the client: +1. **Data URL format**: `data:image/png;base64,iVBORw0KGgo...` +2. **Raw Base64 format**: `iVBORw0KGgo...` + +Use the following universal decoder pattern to parse either format into a Node `Buffer`: + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; + +function decodeBase64File(content: string): Buffer { + const matches = content.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); + + if (matches && matches.length === 3) { + // Data URL format - decode matches[2] + return Buffer.from(matches[2], 'base64'); + } else { + // Raw base64 format - decode input directly + return Buffer.from(content, 'base64'); + } +} +``` + +### 3. Secure File Saving Example +Always validate the directory paths to prevent directory traversal attacks when saving files to disk. + +```typescript +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const UPLOAD_DIR = path.join(process.cwd(), 'uploads'); + +export class SecureUploadTools { + @Tool({ + name: 'save_uploaded_file', + description: 'Decodes and saves an uploaded file securely.', + inputSchema: z.object({ + file_name: z.string().describe('Name of the uploaded file'), + file_type: z.string().describe('MIME type of the uploaded file'), + file_content: z.string().describe('Base64 encoded file content'), + }) + }) + async saveFile(input: any, ctx: ExecutionContext) { + // Ensure uploads directory exists + if (!fs.existsSync(UPLOAD_DIR)) { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + } + + // Secure destination path to prevent path traversal + const safeName = path.basename(input.file_name); + const filePath = path.join(UPLOAD_DIR, safeName); + if (!filePath.startsWith(UPLOAD_DIR)) { + throw new Error('Invalid file path detected (path traversal).'); + } + + // Decode and write to disk + const buffer = decodeBase64File(input.file_content); + fs.writeFileSync(filePath, buffer); + + ctx.logger.info(`Successfully saved file: ${safeName}`); + return { success: true, path: filePath }; + } +} +``` diff --git a/sample-apps/industry4.0mcp/.opencode/skills/ui-widgets/SKILL.md b/sample-apps/industry4.0mcp/.opencode/skills/ui-widgets/SKILL.md new file mode 100644 index 000000000..55b7af712 --- /dev/null +++ b/sample-apps/industry4.0mcp/.opencode/skills/ui-widgets/SKILL.md @@ -0,0 +1,251 @@ +--- +name: nitrostack-ui-widgets +description: Best practices for linking tools to interactive frontend widgets using @Widget and @nitrostack/widgets SDK (including state sync, tool calling, display modes, media queries, and chat actions). +--- + +## When to Use +Use this skill when designing, building, or modifying interactive user interface widgets that display custom React content inside AI clients or NitroStudio. + +--- + +## 1. Backend Definition (`@Widget`) +To display a React-based widget for a tool's output, decorate the tool method with `@Widget`. + +### Options: +* **String Route**: A simple string representing the route identifier in the frontend React app (e.g. `'product-card'`). +* **Object Route**: Object including: + * `route` (required): The route path. + * `domain` (optional): Allowed sandbox domain. + * `csp` (optional): Content Security Policy guidelines. + +### Example: +```typescript +import { Tool, Widget, z } from '@nitrostack/core'; + +export class CatalogTools { + @Tool({ + name: 'fetch_product', + description: 'Get product information by barcode.', + inputSchema: z.object({ barcode: z.string() }), + }) + @Widget('product-details') // Maps to the "product-details" frontend component + async fetchProduct(input: { barcode: string }) { + return { + name: 'Super Nitro Energy Drink', + price: 2.99, + sku: input.barcode, + }; + } +} +``` + +--- + +## 2. Frontend React Widget (`@nitrostack/widgets`) +In your React widget frontend application (typically a Next.js client component), use the `useWidgetSDK` hook to receive input data from the client host. + +### React Component Example: +```tsx +'use client'; + +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +interface ProductData { + name: string; + price: number; + sku: string; +} + +export default function ProductDetailsWidget() { + const { isReady, getToolOutput, theme } = useWidgetSDK(); + const data = getToolOutput(); + + if (!isReady) { + return
Connecting to host...
; + } + + if (!data) { + return
No product data received.
; + } + + return ( +
+

{data.name}

+

${data.price.toFixed(2)}

+ SKU: {data.sku} +
+ ); +} +``` + +--- + +## 3. State Management & Synchronization (`useWidgetState`) +Use `useWidgetState` to manage and persist client-side widget state (e.g. selected tabs, filter values, input states). This state automatically synchronizes with the host application context, persisting it across page re-renders. + +### Example: +```tsx +import React from 'react'; +import { useWidgetState } from '@nitrostack/widgets'; + +export default function StationPanelWidget() { + const [state, setState] = useWidgetState(() => ({ + selectedTab: 'overview', + showExtendedInfo: false, + })); + + return ( +
+ +

Current Tab: {state?.selectedTab}

+
+ ); +} +``` + +--- + +## 4. Calling Core Tools from Widgets (`callTool`) +You can invoke other backend MCP tools directly from the frontend widget using `callTool`. This is useful for tool chaining or triggering detailed audits. + +### Example: +```tsx +import React, { useState } from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function SystemDiagnostics() { + const { callTool, isReady } = useWidgetSDK(); + const [isRunning, setIsRunning] = useState(false); + const [result, setResult] = useState(null); + + const runDiagnostic = async () => { + if (!isReady) return; + setIsRunning(true); + try { + const response = await callTool('run_diagnostic', { system: 'oxygen_scrubber' }); + setResult(response.result as string); + } catch (err) { + setResult('Diagnostic execution failed.'); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +} +``` + +--- + +## 5. Layout & Display Controls +Widgets can dynamically request size mode changes (fullscreen, inline, picture-in-picture) and adapt layouts to safe areas (dynamic islands/notches) or maximum height constraints. + +### Key Methods: +* `requestFullscreen()`: Switch host widget display to fullscreen. +* `requestInline()`: Switch host widget display back to inline. +* `requestPip()`: Float widget in Picture-in-Picture. +* `requestClose()`: Dismiss the widget completely. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function StatusBoard() { + const { + requestFullscreen, + requestInline, + requestClose, + displayMode, // Reactive property ('fullscreen' | 'inline' | 'pip') + maxHeight, // Reactive maxHeight constraint (in pixels) + getSafeArea // Insets data: { top, right, bottom, left } + } = useWidgetSDK(); + + const safeArea = getSafeArea() || { top: 0, bottom: 0 }; + + return ( +
+

Mode: {displayMode}

+ + + +
+ ); +} +``` + +--- + +## 6. Chat Navigation & Actions +Widgets can interact with the host chat pane using external browser links and follow-up prompts. + +### Key Methods: +* `openExternal(url)`: Open the target URL safely in the user's primary external browser. +* `sendFollowUpMessage(prompt)`: Insert a message into the chat flow, automatically submitting it to the LLM agent. + +### Example: +```tsx +import React from 'react'; +import { useWidgetSDK } from '@nitrostack/widgets'; + +export default function MissionControl() { + const { openExternal, sendFollowUpMessage } = useWidgetSDK(); + + return ( +
+ {/* Open external documentation */} + + + {/* Ask LLM agent directly from the widget */} + +
+ ); +} +``` + +--- + +## 7. Media & Accessibility Queries +The SDK provides helper utilities to query target client capabilities for styling or accessibility. + +### Key Utilities: +* `prefersReducedMotion()`: Returns `true` if client settings specify reduced motion. Disable animations. +* `isPrimarilyTouchDevice()`: Returns `true` if the device has a coarse pointer (e.g. touch/mobile). Increase button target sizes. +* `isHoverAvailable()`: Returns `true` if pointer supports hover states. +* `prefersDarkColorScheme()`: Returns `true` if the system theme is dark. + +### Example: +```tsx +import React from 'react'; +import { isPrimarilyTouchDevice, prefersReducedMotion } from '@nitrostack/widgets'; + +export default function AccessiblePanel() { + const isTouch = isPrimarilyTouchDevice(); + const reducedMotion = prefersReducedMotion(); + + return ( +
+ +
+ ); +} +``` + +--- + +## 8. Testing Widgets +* Open your project in **NitroStudio** for visual preview. +* Invoke the tool from the AI chat or testing pane to verify the widget updates instantly with the returned JSON structure. diff --git a/sample-apps/industry4.0mcp/README.md b/sample-apps/industry4.0mcp/README.md new file mode 100644 index 000000000..2180808ee --- /dev/null +++ b/sample-apps/industry4.0mcp/README.md @@ -0,0 +1,260 @@ +# 🏭 Industry 4.0 Machine Health MCP Server + +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) +![NitroStack](https://img.shields.io/badge/NitroStack-Hackathon%202026-FF6B00?style=for-the-badge) +![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-000000?style=for-the-badge) +![ChatGPT](https://img.shields.io/badge/ChatGPT-Integration-74AA9C?style=for-the-badge) + +> **Bridging the gap between Industrial IoT Data and Conversational AI** + +--- + +## 📖 Table of Contents + +- [🌟 Overview](#-overview) +- [⚠️ Problem Statement](#-problem-statement) +- [💡 Solution & AI Integration](#-solution--ai-integration) +- [🔄 Architecture & Flow](#-architecture--flow) +- [📂 Project Structure](#-project-structure) +- [🛠️ Available MCP Tools](#-available-mcp-tools) +- [🚀 Getting Started (Local Setup)](#-getting-started-local-setup) +- [🧪 Testing via NitroStudio](#-testing-via-nitrostudio) +- [☁️ Deployment & ChatGPT Integration](#-deployment--chatgpt-integration) +- [🔮 Future Scope](#-future-scope) +- [🤝 Community & Links](#-community--links) + +--- + +## 🌟 Overview + +The **Industry 4.0 Machine Health MCP Server** is a Model Context Protocol (MCP) based application built for the **NitroStack Hackathon**. + +It empowers factory operators and managers to interact with complex industrial telemetry data using simple natural language via **ChatGPT**. + +> Instead of navigating through complex dashboards, a user can simply ask: +> +> **"What is the current temperature of Machine 1?"** +> +> And ChatGPT will fetch the real-time data through this MCP server. + +--- + +## ⚠️ Problem Statement + +In Industry 4.0 environments, factory machines generate telemetry data such as temperature, vibration, and RPM. In production, this would typically live in a time-series database like **InfluxDB**. + +Today, this demo runs against an in-memory `PlantDatabase` in `industry.data.ts`, which means: +- Data access is already standardized through MCP Tools +- Non-technical users can query it through ChatGPT +- The same tool contract can later target a real time-series database without changing the AI workflow + +--- + +## 💡 Solution & AI Integration + +We created an **MCP Server** using the NitroStack SDK. This server exposes structured tools that ChatGPT can call directly, while all machine data is served from the in-memory `PlantDatabase` defined in `src/modules/industry/industry.data.ts`. + +This keeps the AI layer decoupled from storage: +- MCP Tools define the contract +- `PlantDatabase` acts as the current data source +- A future InfluxDB connector can replace it without changing the AI workflow + +--- + +## 🔄 Architecture & Flow + +```mermaid +flowchart LR + A["Factory Machines / IoT Sensors"] -->|Telemetry Data| B["PlantDatabase industry.data.ts"] + B -->|In-Memory Mock Data| C["NitroStack MCP Server TypeScript"] + C -->|"@Tool Functions"| D["NitroCloud Hosted Deployment"] + D -->|Exposes Server URL| E["ChatGPT MCP Client"] + E -->|Natural Language Query| F["End User"] + + style A fill:#ff9f43,color:#fff + style B fill:#54a0ff,color:#fff + style C fill:#5f27cd,color:#fff + style D fill:#00d2d3,color:#fff + style E fill:#10ac84,color:#fff + style F fill:#feca57,color:#333 +``` + +### Data Flow + +```mermaid +sequenceDiagram + participant U as "User" + participant C as "ChatGPT" + participant S as "MCP Server" + participant DB as "PlantDatabase" + + U->>C: "What is the health of MCH-001?" + C->>S: Calls get_machine_health tool + S->>S: Validates input with Zod + S->>DB: Reads from industry.data.ts + DB-->>S: Returns machine data + S-->>C: JSON response + C-->>U: "Machine MCH-001 is running at 72C..." +``` + +--- + +## 📂 Project Structure + +``` +industry4-mcp/ +├── src/ +│ ├── index.ts # Application bootstrap +│ ├── app.module.ts # Root application module +│ └── modules/ +│ └── industry/ # Industry 4.0 module +│ ├── industry.module.ts +│ ├── industry.tools.ts # MCP Tools (get_machine_health) +│ ├── industry.prompts.ts # Plant orchestrator prompt +│ └── industry.data.ts # In-memory PlantDatabase +├── widgets/ # NitroStudio UI Widgets (Next.js) +├── package.json # Dependencies (@nitrostack/core, zod) +└── .env # Environment variables +``` + +--- + +## 🛠️ Available MCP Tools + +The server currently exposes the following tool to the AI: + +### `get_machine_health` + +| Property | Description | +|----------|-------------| +| **Purpose** | Fetches current health status, temperature, and vibration level of a specific machine | +| **Input** | `machine_id: string` (e.g., `"MCH-001"`) | +| **Output** | JSON object with telemetry data | + +#### Input Schema (Zod) + +```typescript +{ + machine_id: z.string() // e.g., "MCH-001" +} +``` + +#### Response Format + +```json +{ + "machine_id": "MCH-001", + "temperature": 72.5, + "vibration_level": 0.45, + "health_status": "healthy", + "last_maintenance": "2026-07-15" +} +``` + +--- + +## 🚀 Getting Started (Local Setup) + +### Prerequisites + +- 🟢 **Node.js** (v18+ required, v20.x recommended by NitroStack) +- 📦 **npm** or **npx** + +### Installation + +```bash +# 1. Clone the repository +git clone https://github.com/AryanPROOO/industry4-mcp.git +cd industry4-mcp + +# 2. Install dependencies +npm install + +# 3. Start the development server +npm run dev +``` + +The server will start running locally on the default STDIO/HTTP port. + +--- + +## 🧪 Testing via NitroStudio + +**NitroStudio** is the official desktop IDE to test MCP servers before deploying them. + +1. 📥 **Download & Install** — Get NitroStudio from [nitrostack.ai/studio](https://nitrostack.ai/studio) +2. 🔑 **Sign In** — Use your NitroCloud account +3. ➕ **Add Server** — Click `Add Server` → Select `Nitro Project` tab +4. 📁 **Browse Project** — Select the `industry4-mcp` folder +5. 🖥️ **Open App Canvas** — Navigate to the Studio App Canvas +6. 🔧 **Test Tool** — Go to `Tools` → Select `get_machine_health` +7. ▶️ **Execute** — Input `MCH-001` and click **Execute Tool** + +--- + +## ☁️ Deployment & ChatGPT Integration + +Once the tool is working locally, it's time to make it live! + +### Step 1: Deploy to NitroCloud + +1. In **NitroStudio**, click the **Deploy** button in the header +2. Follow the modal steps: + - 📦 Preparing bundle + - ⬆️ Uploading + - 🔨 Building + - ✅ Live +3. Copy your **Service URL** + +### Step 2: Connect to ChatGPT + +1. Open **ChatGPT** (Plus/Pro account required) +2. Go to **Settings → Plugins (Apps)** and enable **Developer Mode** +3. Click the **+ (Add Plugin)** button +4. Select **Server URL** as the connection type +5. Paste your Service URL and add `/sse` at the end: + ``` + https://xyz.nitrocloud.app/sse + ``` +6. Click **Create** and then **Connect** + +### Step 3: Talk to your Factory! 🗣️ + +Try asking ChatGPT: + +- 💬 *"What is the health of machine MCH-001?"* +- 💬 *"Is machine 4 running hot?"* +- 💬 *"Which machines need maintenance?"* + +--- + +## 🔮 Future Scope + +| Feature | Description | +|---------|-------------| +| 🗄️ **Live InfluxDB Integration** | Replace `PlantDatabase` with actual InfluxDB client queries for real time-series data | +| 🔮 **Predictive Maintenance** | Add tools that analyze historical data to predict machine failure | +| 🔔 **Alerting System** | Trigger alerts to maintenance teams if vibration exceeds threshold | + +--- + +## 🤝 Community & Links + +| Resource | Link | +|----------|------| +| 📚 **NitroStack Documentation** | [docs.nitrostack.ai](https://docs.nitrostack.ai) | +| ☁️ **NitroCloud** | [nitrocloud.ai](https://nitrocloud.ai) | +| 💬 **NitroStack Discord** | [Join Community](https://discord.gg/uVWey6UhuD) | +| 🐙 **NitroStack GitHub** | [github.com/nitrocloudofficial/nitrostack](https://github.com/nitrocloudofficial/nitrostack) | +| 📹 **YouTube** | [@nitrostackai](https://www.youtube.com/@nitrostackai) | +| 💼 **LinkedIn** | [nitrostack-ai](https://linkedin.com/company/nitrostack-ai) | + +--- + +
+ +**Built with ❤️ for the NitroStack Hackathon 2026** + +*Empowering Industry 4.0 with Conversational AI* + +
diff --git a/sample-apps/industry4.0mcp/package-lock.json b/sample-apps/industry4.0mcp/package-lock.json new file mode 100644 index 000000000..8e506734d --- /dev/null +++ b/sample-apps/industry4.0mcp/package-lock.json @@ -0,0 +1,4578 @@ +{ + "name": "industry4-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "industry4-mcp", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/ext-apps": ">=0.1.0", + "@nitrostack/core": "^1.0.14", + "dotenv": "^16.3.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@nitrostack/cli": "^1.0.15", + "@types/node": "^22.10.0", + "typescript": "^5.3.3" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz", + "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@nitrostack/cli": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@nitrostack/cli/-/cli-1.0.15.tgz", + "integrity": "sha512-xyIbeAj2/Tpd2khh6Xq1l8y1rbrxQ0t1/c3836g9WrWqC8aNFIKoUvTLuPEZoF4x4ATyjPoZ2NIK90pywuuCRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "archiver": "^7.0.1", + "chalk": "^5.3.0", + "chokidar": "^3.6.0", + "commander": "^12.1.0", + "esbuild": "^0.24.0", + "fs-extra": "^11.3.2", + "inquirer": "^9.3.7", + "open": "^10.1.0", + "ora": "^8.1.1", + "posthog-node": "^5.21.2" + }, + "bin": { + "cli": "dist/index.js", + "nitrostack-cli": "dist/index.js", + "nitrostack-pack": "dist/pack/standalone.js" + } + }, + "node_modules/@nitrostack/core": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@nitrostack/core/-/core-1.0.14.tgz", + "integrity": "sha512-FfG5rOxZwAztHiwPqRPj3xjgoiiPa1A06y2BBqGRlNAkK8R5izImD/f3zhvo1OrGKtoDC/qEw2iykKK24UR/FA==", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "jose": "^6.1.0", + "jsonwebtoken": "^9.0.2", + "reflect-metadata": "^0.2.1", + "uuid": "^11.0.5", + "winston": "^3.17.0", + "ws": "^8.18.3", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.6" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/ext-apps": ">=0.1.0" + } + }, + "node_modules/@nitrostack/core/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@nitrostack/core/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/@nitrostack/core/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@nitrostack/core/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@nitrostack/core/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@nitrostack/core/node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@nitrostack/core/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nitrostack/core/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@nitrostack/core/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nitrostack/core/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/@nitrostack/core/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@nitrostack/core/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@nitrostack/core/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@nitrostack/core/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@nitrostack/core/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@posthog/core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.46.1.tgz", + "integrity": "sha512-EoCFduRkvrg9E5ylMi4QnZCjlAdRJCq6tJouWfngBVR79XSI4iPvIWYA+CdzokAjk+TfSVBFVJ++4Im3r+T0Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@posthog/types": "^1.399.0" + } + }, + "node_modules/@posthog/types": { + "version": "1.399.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.399.0.tgz", + "integrity": "sha512-/WDwBzqIPko8VJ1B+0rlso2XQEz9+2sqtsY9Tqy3p1GhgTqsFakcz/PmMpAnA321LTEZVRcO6x5hAwABV4yrDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz", + "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", + "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.6.tgz", + "integrity": "sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.33", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.33.tgz", + "integrity": "sha512-+SwvkaiJtxsiPjhy9LivY/1m7UsNqCJetM1BrZl9A5DkQhlbHQDU730mMiDPWjnoCYOM8Chf3WrCJw27kNTPFQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.6.tgz", + "integrity": "sha512-HwMtbJjMw8rC8dUTwCNilHJD+fxTeKM3JV1eprSmTjS41qwXSSt6exJXgyPK1QOu0jB9eDYLESRDkB3qaT3jnw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/posthog-node": { + "version": "5.47.3", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.47.3.tgz", + "integrity": "sha512-mhKaZOGLgD5aKKTj6xNRE2K9vRJnRIj4FNZeguDNnCR0k8RKJh71KO78+UqVdOONBsMzoqb01AD/B+TtsK7YSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@posthog/core": "^1.46.1" + }, + "engines": { + "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/sample-apps/industry4.0mcp/package.json b/sample-apps/industry4.0mcp/package.json new file mode 100644 index 000000000..18165cfaa --- /dev/null +++ b/sample-apps/industry4.0mcp/package.json @@ -0,0 +1,39 @@ +{ + "name": "industry4-mcp", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "My awesome MCP server", + "scripts": { + "dev": "nitrostack-cli dev", + "build": "nitrostack-cli build", + "start": "npm run build && nitrostack-cli start", + "start:prod": "nitrostack-cli start", + "upgrade": "nitrostack-cli upgrade", + "install:all": "nitrostack-cli install", + "widget": "npm --prefix src/widgets" + }, + "keywords": [ + "nitrostack", + "mcp", + "industry4", + "iot", + "influxdb" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@nitrostack/core": "^1.0.14", + "zod": "^3.22.4", + "dotenv": "^16.3.1", + "@modelcontextprotocol/ext-apps": ">=0.1.0" + }, + "devDependencies": { + "@nitrostack/cli": "^1.0.15", + "@types/node": "^22.10.0", + "typescript": "^5.3.3" + }, + "nitrostack": { + "skillsVersion": "1.0.0" + } +} diff --git a/sample-apps/industry4.0mcp/src/app.module.ts b/sample-apps/industry4.0mcp/src/app.module.ts new file mode 100644 index 000000000..1ef8ea906 --- /dev/null +++ b/sample-apps/industry4.0mcp/src/app.module.ts @@ -0,0 +1,27 @@ +import { McpApp, Module, ConfigModule } from '@nitrostack/core'; +import { IndustryModule } from './modules/industry/industry.module.js'; + +/** + * Root Application Module + * Industry 4.0 Multi-Agent MCP System + */ +@McpApp({ + module: AppModule, + server: { + name: 'industry4-mcp', + version: '1.0.0' + }, + logging: { + level: 'info' + } +}) + +@Module({ + name: 'app', + description: 'Industry 4.0 Plant Orchestrator Multi-Agent System', + imports: [ + ConfigModule.forRoot(), + IndustryModule // Sirf aapka apna module rahega + ], +}) +export class AppModule { } \ No newline at end of file diff --git a/sample-apps/industry4.0mcp/src/index.ts b/sample-apps/industry4.0mcp/src/index.ts new file mode 100644 index 000000000..aac4e8725 --- /dev/null +++ b/sample-apps/industry4.0mcp/src/index.ts @@ -0,0 +1,29 @@ +/** + * Industry 4.0 Machine Health MCP Server + * + * Bridging Industrial IoT Data and Conversational AI. + * Showcases NitroStack MCP features including: + * - Tool decorators for machine health queries + * - Zod schema validation + * - Mock data fixtures for InfluxDB simulation + * - Modular architecture for Industry 4.0 workflows + */ + +import 'dotenv/config'; +import { McpApplicationFactory } from '@nitrostack/core'; +import { AppModule } from './app.module.js'; + +/** + * Bootstrap the application + */ +async function bootstrap() { + // Create and start the MCP server + const server = await McpApplicationFactory.create(AppModule); + await server.start(); +} + +// Start the application +bootstrap().catch((error) => { + console.error('❌ Failed to start server:', error); + process.exit(1); +}); diff --git a/sample-apps/industry4.0mcp/src/modules/industry/industry.data.ts b/sample-apps/industry4.0mcp/src/modules/industry/industry.data.ts new file mode 100644 index 000000000..c51355b68 --- /dev/null +++ b/sample-apps/industry4.0mcp/src/modules/industry/industry.data.ts @@ -0,0 +1,47 @@ +// src/modules/industry/industry.data.ts + +// Machine Data Structure +export interface Machine { + machine_id: string; + status: 'Running' | 'Idle' | 'Failed'; + temperature: number; + vibration: number; + tool_offset: number; + coolant_pressure: number; +} + +// Job Queue Structure +export interface Job { + job_id: string; + product_id: string; + assigned_machine: string; + status: 'Pending' | 'In Progress' | 'Delayed'; + energy_cost: number; +} + +// Humara In-Memory Synthetic Database +export const PlantDatabase = { + machines: [ + { machine_id: "CNC_1", status: "Running", temperature: 72.5, vibration: 4.1, tool_offset: 10.0, coolant_pressure: 5.0 }, + { machine_id: "CNC_2", status: "Idle", temperature: 25.0, vibration: 0.5, tool_offset: 12.0, coolant_pressure: 4.5 }, + { machine_id: "M1", status: "Running", temperature: 85.0, vibration: 8.5, tool_offset: 8.0, coolant_pressure: 6.0 } // Yeh machine kharab hone wali hai + ] as Machine[], + + jobQueue: [ + { job_id: "J_99", product_id: "Part_X", assigned_machine: "CNC_1", status: "In Progress", energy_cost: 15.5 }, + { job_id: "J_100", product_id: "Part_Y", assigned_machine: "M1", status: "Pending", energy_cost: 12.0 } + ] as Job[], + + // Helper function: Machine dhundhne ke liye + getMachineById(id: string): Machine | undefined { + return this.machines.find(m => m.machine_id === id); + }, + + // Helper function: Machine update karne ke liye (jaise offset change karna) + updateMachine(id: string, updates: Partial) { + const machine = this.getMachineById(id); + if (machine) { + Object.assign(machine, updates); + } + } +}; \ No newline at end of file diff --git a/sample-apps/industry4.0mcp/src/modules/industry/industry.module.ts b/sample-apps/industry4.0mcp/src/modules/industry/industry.module.ts new file mode 100644 index 000000000..f4b84f93f --- /dev/null +++ b/sample-apps/industry4.0mcp/src/modules/industry/industry.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nitrostack/core'; +import { IndustryTools } from './industry.tools.js'; +import { IndustryPrompts } from './industry.prompts.js'; + +@Module({ + name: 'industry', + controllers: [IndustryTools, IndustryPrompts], +}) +export class IndustryModule {} \ No newline at end of file diff --git a/sample-apps/industry4.0mcp/src/modules/industry/industry.prompts.ts b/sample-apps/industry4.0mcp/src/modules/industry/industry.prompts.ts new file mode 100644 index 000000000..22bb097bd --- /dev/null +++ b/sample-apps/industry4.0mcp/src/modules/industry/industry.prompts.ts @@ -0,0 +1,39 @@ +import { PromptDecorator as Prompt, ExecutionContext } from '@nitrostack/core'; + +export class IndustryPrompts { + @Prompt({ + name: 'plant_orchestrator', + description: 'Initializes the Plant Orchestrator Agent for Industry 4.0', + arguments: [], + }) + async plantOrchestrator(args: any, ctx: ExecutionContext) { + return { + messages: [ + { + role: 'system', + content: { + type: 'text', + text: `You are the Plant Orchestrator Agent (Central Command) for a smart factory. + You manage 3 conceptual sub-agents. Always think step-by-step and explain which sub-agent is taking action: + + 1. Data & Quality Agent: Handles tools [normalize_sensor_tags, adjust_machine_parameters]. Triggered when new machines are added or part defects are detected. + 2. Reliability Agent: Handles tools [predict_maintenance_window, reroute_node_red_flow]. Triggered when machine failure is predicted or occurs. If a machine cannot be fixed, this agent reroutes production. + 3. Ops & Admin Agent: Handles tools [optimize_energy_schedule, generate_compliance_audit_trail]. Triggered for cost savings, energy peaks, or legal compliance after incidents. + + RULES: + - If a defect is detected, try to fix it with Data & Quality Agent. If sensors show critical failure, switch to Reliability Agent. + - Whenever an incident happens (rerouting, defect, failure), Ops & Admin Agent MUST generate a compliance audit trail. + - Keep responses concise and in character.` + } + }, + { + role: 'user', + content: { + type: 'text', + text: 'System initialized. Waiting for plant operator commands.' + } + } + ] + }; + } +} \ No newline at end of file diff --git a/sample-apps/industry4.0mcp/src/modules/industry/industry.tools.ts b/sample-apps/industry4.0mcp/src/modules/industry/industry.tools.ts new file mode 100644 index 000000000..5c3c30d90 --- /dev/null +++ b/sample-apps/industry4.0mcp/src/modules/industry/industry.tools.ts @@ -0,0 +1,176 @@ +import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core'; +import { PlantDatabase } from './industry.data.js'; // Mock Database Import + +export class IndustryTools { + + // Tool 1: The Data Translator + @Tool({ + name: 'normalize_sensor_tags', + description: 'Translates raw weird sensor names into industry standard (ISA-95) formats. Call this when new machine data arrives.', + inputSchema: z.object({ + sensors: z.array(z.object({ + machine_id: z.string(), + raw_tag: z.string() + })) + }), + }) + async normalizeSensorTags(input: any, ctx: ExecutionContext) { + ctx.logger.info('Normalizing tags', input.sensors); + const machine = PlantDatabase.getMachineById(input.sensors[0].machine_id); + if (!machine) return { error: `Machine ${input.sensors[0].machine_id} not found in plant.` }; + + return input.sensors.map((s: any) => ({ + raw_tag: s.raw_tag, + standard_name: s.raw_tag.toLowerCase().includes('temp') || s.raw_tag.toLowerCase().includes('t_') ? "Temperature" : "Vibration", + unit: "C", + machine_id: s.machine_id, + status: "Standardized in DB" + })); + } + + // Tool 2: The Self-Healing Router + @Tool({ + name: 'reroute_node_red_flow', + description: 'Dynamically changes Node-RED flow configuration to reroute production data from a failed machine to a backup machine.', + inputSchema: z.object({ + failed_machine: z.string(), + backup_machine: z.string(), + product_id: z.string() + }), + }) + async rerouteNodeRedFlow(input: any, ctx: ExecutionContext) { + ctx.logger.info(`Rerouting from ${input.failed_machine} to ${input.backup_machine}`); + + PlantDatabase.updateMachine(input.failed_machine, { status: "Failed" }); + PlantDatabase.updateMachine(input.backup_machine, { status: "Running" }); + + return { + status: "success", + flow_id: "fl_8923", + message: `Node-RED flow updated. Data now routing to ${input.backup_machine}. DB Status Updated.`, + machine_status_in_db: PlantDatabase.getMachineById(input.backup_machine) + }; + } + + // Tool 3: The Money Saver + @Tool({ + name: 'optimize_energy_schedule', + description: 'Checks real-time energy prices and delays non-urgent jobs to off-peak hours to save electricity costs.', + inputSchema: z.object({ + job_queue: z.array(z.any()), + current_energy_price: z.number(), + threshold_price: z.number() + }), + }) + async optimizeEnergySchedule(input: any, ctx: ExecutionContext) { + ctx.logger.info('Optimizing energy schedule'); + + if (input.current_energy_price > input.threshold_price) { + const jobToDelay = PlantDatabase.jobQueue.find(j => j.status === "Pending"); + if (jobToDelay) { + jobToDelay.status = "Delayed"; + return { + action: "delay_job", + job_id: jobToDelay.job_id, + new_start_time: "2024-05-10T02:00:00Z", + reason: "Energy price too high. Job delayed in DB to off-peak hours.", + updated_job: jobToDelay + }; + } + } + return { action: "no_action", reason: "Energy price is within threshold or no pending jobs." }; + } + + // Tool 4: The Auto-Lawyer + @Tool({ + name: 'generate_compliance_audit_trail', + description: 'Compiles sensor logs and operator actions into a standard PDF audit report for FDA/ISO compliance after a deviation.', + inputSchema: z.object({ + batch_id: z.string(), + deviation_event: z.string() + }), + }) + async generateComplianceAuditTrail(input: any, ctx: ExecutionContext) { + ctx.logger.info('Generating compliance report'); + return { + file_path: `/audits/${input.batch_id}_report.pdf`, + status: "Generated ISO-9001 compliance report with automated root cause and corrective action.", + evidence_data: `Deviation: ${input.deviation_event} logged successfully.` + }; + } + + // Tool 5: The Smart Doctor + @Tool({ + name: 'predict_maintenance_window', + description: 'Predicts machine failure based on live sensor vibration from database.', + inputSchema: z.object({ + machine_id: z.string(), + sensor_history_hours: z.number() + }), + }) + async predictMaintenanceWindow(input: any, ctx: ExecutionContext) { + ctx.logger.info('Predicting maintenance'); + + const machine = PlantDatabase.getMachineById(input.machine_id); + if (!machine) return { error: "Machine not found" }; + + if (machine.vibration > 7.0) { + return { + probability_of_failure: "85% in 24 hours", + recommended_action: "Critical: Schedule maintenance immediately. Vibration too high.", + current_vibration: machine.vibration, + machine_status: machine.status + }; + } + + return { + probability_of_failure: "10% in 48 hours", + recommended_action: "No maintenance needed. Machine is healthy.", + current_vibration: machine.vibration + }; + } + + // Tool 6: The Quality Auto-Fixer (UPDATED - Autonomous) + @Tool({ + name: 'adjust_machine_parameters', + description: 'Calculates and adjusts machine tool offsets to fix quality defects (like dimension oversize) automatically. Fetches current recipe internally from the database.', + inputSchema: z.object({ + machine_id: z.string(), + defect_data: z.object({ + dimension: z.string(), + target_mm: z.number(), + actual_mm: z.number() + }) + // current_recipe removed from here so AI doesn't ask the user for it + }), + }) + async adjustMachineParameters(input: any, ctx: ExecutionContext) { + ctx.logger.info('Adjusting machine parameters'); + + const machine = PlantDatabase.getMachineById(input.machine_id); + if (!machine) return { error: "Machine not found" }; + + // Fetch current recipe internally from DB + const currentRecipe = { + tool_offset: machine.tool_offset, + coolant_pressure: machine.coolant_pressure + }; + + // Calculate new offset + const error = input.defect_data.actual_mm - input.defect_data.target_mm; // 0.3 + const newOffset = currentRecipe.tool_offset - error; // 10.0 - 0.3 = 9.7 + const oldOffset = currentRecipe.tool_offset; + + // Database update + PlantDatabase.updateMachine(input.machine_id, { tool_offset: newOffset }); + + return { + status: "success", + parameter_changed: "tool_offset", + old_value: oldOffset, + new_value: newOffset, + reason: `Compensated for ${error}mm ${input.defect_data.dimension} oversize. Database updated successfully.`, + updated_machine_data: PlantDatabase.getMachineById(input.machine_id) + }; + } +} \ No newline at end of file diff --git a/sample-apps/industry4.0mcp/src/widgets/app/layout.tsx b/sample-apps/industry4.0mcp/src/widgets/app/layout.tsx new file mode 100644 index 000000000..100a41ac9 --- /dev/null +++ b/sample-apps/industry4.0mcp/src/widgets/app/layout.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { WidgetLayout } from '@nitrostack/widgets'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/sample-apps/industry4.0mcp/src/widgets/next-env.d.ts b/sample-apps/industry4.0mcp/src/widgets/next-env.d.ts new file mode 100644 index 000000000..40c3d6809 --- /dev/null +++ b/sample-apps/industry4.0mcp/src/widgets/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/sample-apps/industry4.0mcp/src/widgets/next.config.js b/sample-apps/industry4.0mcp/src/widgets/next.config.js new file mode 100644 index 000000000..f35620cee --- /dev/null +++ b/sample-apps/industry4.0mcp/src/widgets/next.config.js @@ -0,0 +1,45 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['nitrostack'], + + // Static export for production builds + ...(process.env.NODE_ENV === 'production' && { + output: 'export', + distDir: 'out', + images: { + unoptimized: true, + }, + }), + + // Development optimizations to prevent cache corruption + ...(process.env.NODE_ENV === 'development' && { + // Use memory cache instead of filesystem cache in dev to avoid stale chunks + webpack: (config, { isServer }) => { + // Disable persistent caching in development to prevent chunk reference errors + if (config.cache && config.cache.type === 'filesystem') { + config.cache = { + type: 'memory', + }; + } + + // Improve cache busting for new files + if (!isServer) { + config.cache = false; // Disable cache completely on client in dev + } + + return config; + }, + + // Disable build activity indicator which can cause issues + devIndicators: { + buildActivity: false, + buildActivityPosition: 'bottom-right', + }, + + // Faster dev server + compress: false, + }), +}; + +export default nextConfig; diff --git a/sample-apps/industry4.0mcp/src/widgets/package.json b/sample-apps/industry4.0mcp/src/widgets/package.json new file mode 100644 index 000000000..e290dac4f --- /dev/null +++ b/sample-apps/industry4.0mcp/src/widgets/package.json @@ -0,0 +1,32 @@ +{ + "name": "industry4-widgets", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "next dev -p 3001 --port 3001", + "build": "next build", + "start": "next start -p 3001" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": ">=0.1.0", + "@nitrostack/core": "^1.0.14", + "@nitrostack/widgets": "^1.0.8", + "framer-motion": "^10.16.16", + "lucide-react": "^0.294.0", + "mapbox-gl": "^3.0.1", + "next": "^14.2.5", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/mapbox-gl": "^3.0.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5" + } +} diff --git a/sample-apps/industry4.0mcp/src/widgets/tsconfig.json b/sample-apps/industry4.0mcp/src/widgets/tsconfig.json new file mode 100644 index 000000000..2c0ad665d --- /dev/null +++ b/sample-apps/industry4.0mcp/src/widgets/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} + diff --git a/sample-apps/industry4.0mcp/tsconfig.json b/sample-apps/industry4.0mcp/tsconfig.json new file mode 100644 index 000000000..6df25d8a8 --- /dev/null +++ b/sample-apps/industry4.0mcp/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "rootDir": "./src", + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/widgets"] +} \ No newline at end of file