vitals-node
v1.1.3
Published
Multi-framework health monitoring & system metrics for NestJS, Express, and Bun
Maintainers
Readme
vitals-node
Multi-framework health monitoring and AI agent integration for Node.js. Supports NestJS, Express, and Bun with zero-config setup.
Features
- Multi-framework — NestJS, Express, Bun
- Mode system —
monitor(health/metrics) ·agent(AI integration) ·both - Real-time dashboard — Charts, dark mode, route panel
- System metrics — CPU, Memory, Disk monitoring
- Route discovery — Auto-detect all registered API routes
- AI schema analysis — Claude or Ollama reads source code and infers request schemas
- Works across all frameworks: NestJS decorator lookup, Express/Bun method-call lookup
- DTO resolution — follows
this.service.method(body)chains to resolveany-typed parameters "any"type for fields that genuinely cannot be determined
- Per-route table detection — identifies which DB tables each route reads/writes via static regex or LLM (
tableDetection: 'static' | 'llm' | 'auto'); follows service delegation chains - Auth detection — Scans
@UseGuards, global guards,@Public()per route; LLM classifies auth type - Auth-aware
call_api—cookiesparameter for session-based auth flows - DB schema exposure — reads Prisma/Drizzle/TypeORM schema; auto-registers
get_db_schemaagent tool; included in registry payload - Registry payload — Includes
version,environment,authSummary,dbSchema, and per-routetablesauto-computed - Manual registry push — POST route list + schemas to a central server on demand
- Alert notifications — LINE, Email, Telegram
- Prometheus export — Ready for Grafana/Prometheus
- Prisma integration — Database query monitoring + auto-generate agent tools
- Env auto-config — Zero code change via
VITALS_*environment variables - TypeScript first — Full type safety
Installation
npm install vitals-nodeQuick Start
NestJS
import { Module } from '@nestjs/common';
import { VitalsPulseModule } from 'vitals-node';
@Module({
imports: [
VitalsPulseModule.forRoot({
mode: 'both',
agent: { enabled: true, apiKey: process.env.VITALS_AGENT_API_KEY }
})
]
})
export class AppModule {}With mode: 'both' NestJS registers two controllers automatically:
HealthController— mounts at/healthAgentsController— mounts at/agents
Express
import express from 'express';
import { expressPulse } from 'vitals-node/express';
const app = express();
app.use(express.json());
app.use('/health', expressPulse({ app, mode: 'monitor' }));
app.use('/agents', expressPulse({ app, mode: 'agent', agent: { apiKey: process.env.VITALS_AGENT_API_KEY } }));
app.listen(3000);Bun
import { bunPulse } from 'vitals-node/bun';
const pulse = bunPulse({ mode: 'both' });
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname.startsWith('/health')) return pulse.handler(req);
return new Response('OK');
}
});Mode System
| Mode | What it enables | Metrics interval |
|---|---|---|
| monitor | health, metrics, dashboard, prometheus, alerts | runs |
| agent | route discovery, AI tools, registry push, call_api | skipped (saves resources) |
| both | all features (default) | runs |
In NestJS mode: 'both' creates two separate controllers: HealthController at /health and AgentsController at /agents. In Express/Bun you mount each middleware at the path you choose.
Endpoints
Monitor endpoints
Available when mode: 'monitor' or 'both' (mounted at your chosen base path, e.g. /health):
| Endpoint | Description |
|---|---|
| GET /health | Liveness probe |
| GET /health/detailed | Full system metrics + health checks |
| GET /health/dashboard | Real-time HTML dashboard |
| GET /health/metrics | Prometheus format |
| GET /health/prometheus | Prometheus format (alias) |
| GET /health/history | Metrics history |
| GET /health/alerts | Alert history |
| POST /health/test-alerts | Test notification channels |
Monitor endpoints (additional)
| Endpoint | Description |
|---|---|
| GET /health/raw | Raw system metrics (JSON) |
Agent endpoints
Available when mode: 'agent' or 'both'. NestJS uses /agents/*; Express/Bun use the path you mount with mode: 'agent':
| Endpoint | Description |
|---|---|
| GET /agents/routes | All discovered routes with inferred schemas and auth requirements |
| GET /agents/tools | Agent tool schemas (Anthropic format) |
| GET /agents/schema/status | Schema analysis progress snapshot |
| GET /agents/schema/stream | Live SSE stream of schema analysis progress |
| POST /agents/invoke | Invoke an agent tool { name, input } |
| POST /agents/registry/push | Manually push registry to a webhook |
All agent endpoints require Authorization: Bearer <apiKey> when agent.apiKey is set.
In Express/Bun with mode: 'both', agent endpoints are nested under the monitor base path:
/health/agent/tools, /health/agent/schema/status, /health/agent/schema/stream, /health/agent/invoke, /health/agent/registry/push.
NestJS Controllers
VitalsPulseModule.forRoot() registers controllers based on mode:
mode: 'monitor' → HealthController at /health
mode: 'agent' → AgentsController at /agents
mode: 'both' → both controllers (default)You can import the controllers directly for custom configuration:
import { HealthController, AgentsController, PULSE_MONITOR } from 'vitals-node';Environment Variables
Target projects only need VITALS_AGENT_API_KEY — all other config is passed in request bodies.
| Variable | Description |
|---|---|
| VITALS_MODE | monitor · agent · both |
| VITALS_PATH | Base path override |
| VITALS_AGENT_ENABLED | true to enable agent endpoints |
| VITALS_AGENT_API_KEY | Bearer token for /agents/* protection |
Manual Registry Push
Registry push is triggered on demand via POST /agents/registry/push. No config is stored in the target project — all parameters come from the request body.
POST /agents/registry/push
Authorization: Bearer <apiKey>
{
"webhook": "https://central-server.com/register", // optional — omit to get data in response only
"groupId": "my-company", // optional — group multiple services together
"projectId": "my-service",
"projectPath": "/api", // optional — override auto-detected base path
"tags": "crm,billing", // optional — help AI categorize this service
"apiKey": "central-server-bearer-token",
"baseUrl": "https://my-service.example.com",
"schemaAnalysis": { // optional — run AI schema analysis before push
"provider": "anthropic",
"apiKey": "sk-ant-xxx",
"model": "claude-haiku-4-5-20251001"
}
}Response always includes the full payload (same data sent to webhook):
{
"success": true,
"groupId": "my-company",
"projectId": "my-service",
"baseUrl": "https://my-service.example.com",
"projectPath": "/api",
"tags": "crm,billing",
"version": "2.3.1",
"environment": "production",
"authSummary": {
"types": ["cookie", "none"],
"cookieNames": ["session"],
"loginPaths": ["/api/auth/login"],
"publicRoutes": 2,
"protectedRoutes": 6
},
"routes": [
{
"method": "GET",
"path": "/api/orders",
"tables": ["Order", "OrderItem"],
"schema": { "query": { "status": { "type": "string", "required": false } } },
"auth": { "guards": ["CookieAuthGuard"], "isPublic": false, "requirements": { "type": "cookie", "cookieName": "session", "description": "..." } }
}
],
"agentTools": [...],
"dbSchema": [
{
"orm": "prisma",
"tables": [
{ "name": "Order", "columns": [{ "name": "id", "type": "String", "nullable": false, "isPrimary": true }] }
],
"readAt": 1748247600000
}
],
"registeredAt": 1748247600000
}Auto-populated fields (no config needed):
version— read from the project'spackage.jsonenvironment— fromNODE_ENVauthSummary— computed from per-routeauth.requirementsdetected by guard scanningdbSchema— included whendbSchemais configured at startup (Prisma/Drizzle/TypeORM)routes[].tables— included whenschemaAnalysis.tableDetectionis sethandleris stripped from routes before push — it is an internal implementation detail
If webhook is omitted the push is skipped but the response still contains the full payload — useful for inspecting what would be sent.
AI Schema Analysis
Pass schemaAnalysis in the POST /agents/registry/push body to run schema inference before pushing. No API keys are stored in the target project.
Using Anthropic Claude
{
"schemaAnalysis": {
"provider": "anthropic",
"apiKey": "sk-ant-xxx",
"model": "claude-haiku-4-5-20251001",
"concurrency": 5,
"timeout": 30000,
"tableDetection": "auto"
}
}Using Ollama (local LLM)
{
"schemaAnalysis": {
"provider": "ollama",
"baseUrl": "http://localhost:11434",
"model": "llama3.2",
"concurrency": 1,
"timeout": 120000,
"tableDetection": "static"
}
}How it works
- Routes are discovered from the running app
- Handler source is located per framework:
- NestJS — searches
@Get('segment')/@Post('segment')decorators (handler names are empty at runtime) - Express — uses function name if available; falls back to
.get('/path', handler)call-site lookup for anonymous handlers - Bun (Elysia/Hono) — matches
.post('/path', handler)call sites; captures inline Elysia schema definitions automatically
- NestJS — searches
- Source files are loaded once into a cache and reused across all routes
- Handler body is extracted using brace-counting (not a fixed line limit) to capture the full function
- DTO resolution — when a parameter is typed as
any, vitals follows the service call chain:- Detects
this.someService.method(body)→ findsmethod(dto: CreateUserDto)in@Injectablefiles → findsclass CreateUserDto { ... }→ appends to LLM context
- Detects
- LLM returns
body/params/queryschema as JSON; fields that cannot be determined use type"any" - Schemas are merged into
/agents/routesand included in the push payload
Field types
| Type | When used |
|---|---|
| string number boolean object array | LLM determined the type from source |
| any | Type is any and no DTO chain was resolvable — check description for hints |
Example schema output
{
"method": "POST",
"path": "/credits/adjust",
"tables": ["CreditTransaction", "User"],
"schema": {
"description": "Adjust credit balance for a user account",
"body": {
"userId": { "type": "string", "description": "Target user ID", "required": true },
"amount": { "type": "number", "description": "Amount — positive adds, negative deducts", "required": true },
"reason": { "type": "string", "description": "Reason for adjustment", "required": false }
}
}
}Schema Analysis Progress
When POST /agents/registry/push with schemaAnalysis is called, schema inference runs in the background. Poll the progress or stream it in real time.
Snapshot
GET /agents/schema/status
Authorization: Bearer <apiKey>{
"status": "running",
"total": 12,
"completed": 5,
"staticCount": 3,
"llmCount": 2,
"currentRoute": "POST /api/orders",
"startedAt": 1750161600000,
"eta": 4200
}Live SSE stream
GET /agents/schema/stream
Authorization: Bearer <apiKey>Server-Sent Events — one data: frame every ~300 ms until status is "done" or "error", then the connection closes automatically.
const es = new EventSource('/agents/schema/stream', {
headers: { Authorization: `Bearer ${apiKey}` }
});
es.onmessage = ({ data }) => console.log(JSON.parse(data));Progress fields
| Field | Description |
|---|---|
| status | idle · running · done · error |
| total | Total routes to process |
| completed | Routes processed so far |
| staticCount | Routes resolved by static regex (no LLM call) |
| llmCount | Routes sent to the LLM |
| currentRoute | Route currently being processed |
| startedAt | Unix ms when analysis started |
| finishedAt | Unix ms when analysis finished (once done) |
| elapsedMs | Wall-clock ms elapsed |
| eta | Estimated ms remaining (only while running) |
Per-Route Table Detection
When tableDetection is set in schemaAnalysis, vitals detects which database tables each route reads or writes and adds a tables: string[] field to every route entry.
Modes
| Mode | Behaviour |
|---|---|
| 'static' | Regex scan only — no LLM cost, runs instantly |
| 'llm' | Always asks the LLM — more accurate for complex patterns |
| 'auto' | Static first; if nothing found, falls back to LLM |
What is detected
Prisma — this.prisma.user.findMany(), prisma.order.create(), etc.
Drizzle — db.select().from(orders), db.insert(users).values(...), db.update(products)
TypeORM — getRepository(Entity), @InjectRepository(Entity), Repository<Entity>
Service following
If a handler delegates to a service (this.orderService.create(dto)), vitals finds the service class in the source files and scans that method body too — one level deep:
// Handler → delegates
async create(@Body() dto: CreateOrderDto) {
return this.orderService.create(dto); // ← vitals follows this
}
// OrderService.create → vitals scans this for DB calls
async create(dto: CreateOrderDto) {
const order = await this.prisma.order.create({ data: dto }); // → "Order" detected
await this.prisma.orderItem.createMany({ data: dto.items }); // → "OrderItem" detected
return order;
}Names are normalized to PascalCase (prisma.orderItem → "OrderItem").
DB Schema Exposure
Configure dbSchema at startup to read your ORM schema and expose it as an agent tool:
import { PrismaClient, Prisma } from '@prisma/client';
VitalsPulseModule.forRoot({
mode: 'both',
dbSchema: {
prisma: { client: prisma, dmmf: Prisma.dmmf }
// drizzle: { schema: drizzleSchema }
// typeorm: { dataSource: AppDataSource }
}
});This automatically:
- Registers a
get_db_schemaagent tool the AI can call - Includes the full table/column schema in the registry push payload under
dbSchema
get_db_schema tool
POST /agents/invoke
{ "name": "get_db_schema", "input": { "orm": "prisma", "table": "order" } }| Parameter | Required | Description |
|---|---|---|
| orm | no | Filter: prisma · drizzle · typeorm. Omit to return all. |
| table | no | Case-insensitive partial match on table name. Omit to return all tables. |
Supported ORMs
| ORM | Config key | How schema is read |
|---|---|---|
| Prisma | prisma.client | Via Prisma.dmmf (pass dmmf for reliability) |
| Drizzle | drizzle.schema | import * as schema from './db/schema' |
| TypeORM | typeorm.dataSource | Initialized DataSource instance |
Multiple ORMs can be configured at once — each appears as a separate entry in dbSchema.
Auth Detection
When schemaAnalysis is active, vitals automatically scans source files for auth guards per route. No extra config required.
What is detected
@UseGuards(GuardClass)at method and class level- Global guards registered via
APP_GUARDorapp.useGlobalGuards() @Public()/@IsPublic()/@SkipAuth()opt-out decorators- Passport strategy source (resolved by strategy key string, not class name convention)
Auth requirements in routes
{
"method": "GET",
"path": "/api/orders",
"auth": {
"guards": ["CookieAuthGuard"],
"isPublic": false,
"requirements": {
"type": "cookie",
"cookieName": "session",
"description": "Requires valid session cookie issued by POST /api/auth/login"
}
}
}Auth types: bearer · cookie · apikey · none · custom
Auth summary in registry payload
The registry payload includes a service-level summary so AI agents know the auth pattern at a glance:
"authSummary": {
"types": ["cookie", "none"],
"cookieNames": ["session"],
"loginPaths": ["/api/auth/login"],
"publicRoutes": 2,
"protectedRoutes": 6
}call_api Built-in Tool
When vitals starts, it auto-registers a call_api tool that lets the AI invoke any route on the service directly.
POST /agents/invoke
Authorization: Bearer <apiKey>
{
"name": "call_api",
"input": {
"method": "POST",
"path": "/api/auth/login",
"body": { "email": "[email protected]", "password": "secret" }
}
}
→ { "result": { "status": 200, "ok": true, "body": { "token": "..." } } }Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| method | string | yes | GET POST PUT PATCH DELETE |
| path | string | yes | API path e.g. /api/users/123 |
| body | object | no | Request body for POST/PUT/PATCH |
| headers | object | no | Extra request headers e.g. { "Authorization": "Bearer <token>" } |
| cookies | object | no | Cookies to send e.g. { "session": "<value>" } — use when route auth.requirements.type is "cookie" |
| query | object | no | Query string params |
Handling cookie-protected routes
Check auth.requirements on the route first, then use the cookies parameter:
{
"name": "call_api",
"input": {
"method": "GET",
"path": "/api/orders",
"cookies": { "session": "<session_value>" }
}
}Typical flow for cookie auth:
1. call_api POST /api/auth/login { email, password }
→ response Set-Cookie: session=abc123
2. call_api GET /api/orders
cookies: { "session": "abc123" }
→ protected route accessed successfullyThe tool description instructs Claude to check auth.requirements from GET /agents/routes before calling, so the AI knows which credential type to use.
AI Agent Flow
customer reports problem
→ chat → AI agent (Claude)
→ GET /agents/routes — discover APIs, schemas, and auth requirements
→ GET /agents/tools — discover invokable diagnostic tools
→ POST /agents/invoke — call the right tool or API to fix the problem
→ response to customerExample: agent fixes a credit mismatch
// AI discovers routes and tools, then calls:
POST /agents/invoke
{ "name": "adjust_credit", "input": { "userId": "U123", "amount": 100 } }
→ { "result": { "success": true, "newBalance": 500 } }
// Or directly calls the API:
POST /agents/invoke
{ "name": "call_api", "input": { "method": "POST", "path": "/credits/adjust", "body": { "userId": "U123", "amount": 100 } } }
→ { "result": { "status": 200, "ok": true, "body": { "success": true } } }Example: agent calls a cookie-protected route
// Step 1 — login
POST /agents/invoke
{ "name": "call_api", "input": { "method": "POST", "path": "/api/auth/login", "body": { "email": "...", "password": "..." } } }
→ Set-Cookie: session=abc123
// Step 2 — call protected route
POST /agents/invoke
{ "name": "call_api", "input": { "method": "GET", "path": "/api/orders", "cookies": { "session": "abc123" } } }
→ { "orders": [...] }Route Discovery
NestJS
Routes are captured automatically via OnApplicationBootstrap lifecycle hook.
Express
Discovery is lazy — routes registered after the middleware call are still captured.
app.use('/health', expressPulse({ app }));
// All routes registered below are still discovered
app.get('/api/orders', handler);Fastify
monitor.discoverRoutes(fastifyApp, 'fastify');Manual Registration
import { createMonitor } from 'vitals-node/core';
const monitor = createMonitor();
monitor.registerRoute({ method: 'GET', path: '/api/orders', handler: 'OrderController.list' });
monitor.registerRoutes([
{ method: 'POST', path: '/api/orders' },
{ method: 'PUT', path: '/api/orders/:id/status' }
]);
// Manual schema annotation — overrides AI analysis for this route
monitor.annotateRoute('POST', '/api/orders', {
description: 'Create a new order',
body: {
userId: { type: 'string', description: 'User ID', required: true },
items: { type: 'array', description: 'Order items', required: true }
}
});Configuration Reference
interface VitalsConfig {
mode?: 'monitor' | 'agent' | 'both'; // default: 'both'
path?: string; // default: '/health'
dashboard?: boolean; // default: true
collectInterval?: number; // ms (default: 5000)
timezone?: string; // default: 'Asia/Bangkok'
prometheus?: boolean; // default: true
alerts?: {
thresholds?: {
cpu?: number; // default: 80%
memory?: number; // default: 90%
disk?: number; // default: 85%
responseTime?: number; // default: 3000ms
};
cooldown?: number; // ms between alerts (default: 5 min)
line?: { token: string; enabled?: boolean };
email?: { host: string; port: number; user: string; password: string; from: string; to: string[]; secure?: boolean };
telegram?: { botToken: string; chatId: string; enabled?: boolean };
};
healthChecks?: HealthCheckDefinition[];
agent?: {
enabled?: boolean; // must be true to expose /agent/* endpoints
apiKey?: string; // Bearer token protecting agent endpoints
tools?: AgentToolDefinition[];
};
dbSchema?: {
prisma?: { client: PrismaClient; dmmf?: typeof Prisma.dmmf };
drizzle?: { schema: Record<string, unknown> };
typeorm?: { dataSource: DataSource };
};
}schemaAnalysis (including tableDetection) and registry config are passed in the POST /agents/registry/push request body, not in startup config — this keeps secrets out of the target project's environment.
// schemaAnalysis options (in request body)
{
provider?: 'anthropic' | 'ollama'; // default: 'anthropic'
apiKey?: string; // Anthropic key (or ANTHROPIC_API_KEY env)
model?: string; // default: claude-haiku-4-5-20251001
sourceDir?: string; // path to .ts source files (default: cwd)
concurrency?: number; // parallel LLM calls (default: 5)
timeout?: number; // ms per call (default: 30000)
guards?: boolean; // auth guard scanning (default: true)
tableDetection?: 'static' | 'llm' | 'auto'; // per-route table detection
}Custom Health Checks
monitor.addHealthCheck({
name: 'redis',
critical: true,
timeout: 5000,
check: async () => {
const start = Date.now();
try {
await redis.ping();
return { name: 'redis', status: 'healthy', responseTime: Date.now() - start };
} catch (err) {
return { name: 'redis', status: 'unhealthy', responseTime: Date.now() - start, error: err.message };
}
}
});Agent Tools
Register custom tools that the AI can invoke via POST /agents/invoke:
import { getMonitor } from 'vitals-node/core';
getMonitor().registerAgentTool({
name: 'adjust_credit',
description: 'Adjust user credit balance',
parameters: {
userId: { type: 'string', description: 'User ID', required: true },
amount: { type: 'number', description: 'Amount to adjust (+/-)', required: true }
},
handler: async ({ userId, amount }) =>
creditService.adjust(String(userId), Number(amount))
});Tool schemas follow the Anthropic input_schema format — compatible with Claude tool use directly.
Prisma Integration
import { PrismaClient } from '@prisma/client';
import { prismaMiddleware, createPrismaHealthCheck, createPrismaAgentTools } from 'vitals-node/prisma';
import { getMonitor } from 'vitals-node/core';
const prisma = new PrismaClient();
prisma.$use(prismaMiddleware({ slowQueryThreshold: 1000, logSlowQueries: true }));
const monitor = getMonitor();
monitor.addHealthCheck(createPrismaHealthCheck(prisma));
const tools = createPrismaAgentTools(prisma, {
dmmf: Prisma.dmmf,
models: ['User', 'CreditTransaction'],
sensitiveFields: ['internalNote']
});
monitor.registerAgentTools(tools);System Metrics
{
"cpu": { "usage": 45.5, "cores": 8, "model": "Intel...", "speed": 3.2 },
"memory": { "total": 16384, "used": 8192, "free": 8192, "percentage": 50 },
"disk": { "total": 512000, "used": 256000, "free": 256000, "percentage": 50 },
"process": { "uptime": 86400, "pid": 1234, "nodeVersion": "v20.0.0", "memoryUsage": 128, "cpuUsage": 2.5 }
}Event Handling
const monitor = getMonitor();
monitor.on('metrics:collected', (event) => console.log('Metrics:', event.data));
monitor.on('alert:triggered', (event) => console.log('Alert!', event.data));Available events: metrics:collected · alert:triggered · alert:resolved · health:changed · database:slow-query · database:error
Prometheus / Grafana
scrape_configs:
- job_name: 'my-app'
static_configs:
- targets: ['localhost:3000']
metrics_path: '/health/metrics'Alert Configuration
LINE Notify
alerts: { line: { token: 'YOUR_LINE_NOTIFY_TOKEN' } }Telegram
alerts: { telegram: { botToken: 'BOT_TOKEN', chatId: 'CHAT_ID' } }Email (SMTP)
alerts: { email: { host: 'smtp.gmail.com', port: 587, user: '...', password: '...', from: '...', to: ['admin@...'] } }API Reference
NestJS
import { VitalsPulseModule, NestPulseModule } from 'vitals-node';
// in AppModule.imports:
VitalsPulseModule.forRoot(options) // recommended
NestPulseModule.forRoot(options) // aliasExpress
import { expressPulse, createExpressRouter } from 'vitals-node/express';
app.use('/health', expressPulse(options)); // middleware
app.use('/health', createExpressRouter(options)); // router variantBun
import { bunPulse, withBunPulse } from 'vitals-node/bun';
const pulse = bunPulse(options);
// pulse.handler(req) — use in Bun.serve fetch
Bun.serve(withBunPulse({ ...options, fetch(req) { return new Response('OK'); } }));Core (Standalone)
import { createMonitor, getMonitor } from 'vitals-node/core';
const monitor = createMonitor(options);
await monitor.start();
// Metrics
const metrics = await monitor.getMetrics();
const health = await monitor.getHealth();
const detailed = await monitor.getDetailedHealth();
// Routes
monitor.discoverRoutes(app, 'express');
monitor.registerRoute({ method: 'GET', path: '/api/orders' });
monitor.annotateRoute('POST', '/api/orders', schema);
await monitor.analyzeRouteSchemas(schemaAnalysisConfig);
const registry = monitor.getRoutes();
// Registry push
const payload = await monitor.buildRegistryPayload({ groupId, projectId, projectPath, tags, baseUrl });
await monitor.pushRegistryTo({ url: webhook, groupId, projectId, projectPath, tags, apiKey, baseUrl });
// Agent tools
monitor.registerAgentTool(tool);
monitor.registerAgentTools(tools);
monitor.getAgentToolSchemas();
await monitor.invokeAgentTool(name, input);
monitor.enableCallApi('http://localhost:3000'); // register call_api built-in tool
monitor.stop();Requirements
- Node.js 18+ or Bun 1.0+
- TypeScript 5+ (recommended)
License
MIT
