npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

agi-server

v1.0.3

Published

A modern, FastAGI server library for Node.js with full TypeScript support.

Readme

AGI Server

npm version TypeScript License: MIT

A modern, zero‑dependency FastAGI server and client library for Asterisk, written in TypeScript. It provides a clean event‑based API, full AGI command coverage, per‑route authentication, and strong type safety.

Table of Contents

Features

  • 🚀 FastAGI server – Listen for AGI connections from Asterisk
  • 📡 Full AGI command set – All standard AGI commands as async methods
  • 🔐 Per‑route & global authentication – Flexible auth functions
  • 📦 Zero runtime dependencies – Uses only Node.js core modules
  • 🧩 TypeScript first – Complete type definitions for events and commands
  • 🧵 Command queueing – Serializes AGI commands automatically
  • ⏱️ Command timeouts – Prevents hanging requests (30s default)
  • 🧹 Graceful shutdown – Proper cleanup of channels and server

Installation

npm install agi-server

Quick Start

import { AGIServer } from 'agi-server';

const server = new AGIServer();

// Optional global authentication
server.auth(async (channel, path, params, headers) => {
  const token = headers['x-api-token'];
  return token === 'my-secret';
});

// Register a route
server.agi('/welcome', async (channel, params) => {
  await channel.answer();
  await channel.streamFile('welcome');
  await channel.hangup();
});

// Start listening
await server.listen(4573, '0.0.0.0');
console.log('AGI server running on port 4573');

Using with NestJS

import { Injectable, OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
import { AGIServer } from 'agi-server';

@Injectable()
export class AgiService implements OnApplicationBootstrap, OnApplicationShutdown {
  private server: AGIServer;

  async onApplicationBootstrap() {
    this.server = new AGIServer();
    this.server.auth(this.authenticate.bind(this));
    this.server.agi('/menu', this.handleMenu.bind(this));
    await this.server.listen(4573);
    console.log('AGI server started');
  }

  async onApplicationShutdown() {
    await this.server.close();
  }

  private authenticate(channel, path, params, headers) {
    return headers['x-token'] === process.env.AGI_TOKEN;
  }

  private async handleMenu(channel, params) {
    await channel.answer();
    const result = await channel.getData('menu-prompt', 10000, 1);
    if (result.data === '1') {
      await channel.streamFile('option1');
    }
    await channel.hangup();
  }
}

API Reference

AGIServer

constructor() Creates a new AGI server instance. Does not start listening until listen() is called.

listen(host?: string, port?: number): Promise<{ host: string; port: number }> Starts the server. Default host '0.0.0.0', default port 4573. Throws if already listening.

close(): Promise Closes the server and all active channels. Emits 'stopped' when done.

auth(authFn: AuthFunction): void Sets a global authentication function called before every route.

type AuthFunction = (
  channel: AGIChannel,
  path: string,
  params: Record<string, string | string[]>,
  headers: Record<string, string>
) => Promise<boolean> | boolean;

agi(path: string, handler: AGIHandler): void agi(path: string, config: AGIRouteConfig): void Registers a route. config can include a custom auth function that overrides the global one.

type AGIHandler = (channel: AGIChannel, params: Record<string, string | string[]>) => Promise<void>;

interface AGIRouteConfig {
  handler: AGIHandler;
  auth?: AuthFunction;
}

AGIChannel

All methods return Promise<AGIResponse> where:

interface AGIResponse {
  code: number;        // 200, 510, etc.
  result?: number | null;
  data?: string | null;
  error?: string;
}

Basic commands

| Method | Description | |--------|-------------| | answer() | Answers the channel | | hangup() | Hangs up and destroys the channel | | verbose(message, level?) | Sends log to Asterisk console (level 1-4) | | channelStatus(channel?) | Returns status 0-7 | | noop(message?) | Does nothing (debug) |

Audio playback & recording

| Method | Description | |--------|-------------| | streamFile(filename, escapeDigits?, offset?) | Plays a file, listens for DTMF | | controlStreamFile(filename, escapeDigits, skipms?, ffchar?, rewchar?, pausechar?) | Play with controls | | recordFile(filename, format, escapeDigits, timeout, offset?, beep?, silence?) | Records audio to file |

DTMF & input

| Method | Description | |--------|-------------| | waitForDigit(timeout?) | Waits for a DTMF digit | | getData(filename, timeout?, maxDigits?) | Plays file and collects digits | | getOption(filename, escapeDigits, timeout?) | Plays file with timeout | | sendDTMF(digits) | Sends DTMF digits |

Say commands (text-to-speech)

| Method | Description | |--------|-------------| | sayAlpha(text, escapeDigits?) | Says alphabetic string | | sayDigits(digits, escapeDigits?) | Says digits | | sayNumber(number, escapeDigits?, gender?) | Says number | | sayPhonetic(text, escapeDigits?) | Says phonetically | | sayDate(timestamp, escapeDigits?) | Says date | | sayTime(timestamp, escapeDigits?) | Says time | | sayDatetime(timestamp, escapeDigits?, format?, timezone?) | Says date and time |

Variables

| Method | Description | |--------|-------------| | getVariable(name) | Gets a channel variable | | getFullVariable(name, channel?) | Gets variable with channel spec | | setVariable(name, value) | Sets a channel variable |

Call flow control

| Method | Description | |--------|-------------| | setContext(context) | Sets continuation context | | setExtension(extension) | Sets continuation extension | | setPriority(priority) | Sets continuation priority | | gosub(context, extension, priority) | Executes a subroutine |

Other commands

| Method | Description | |--------|-------------| | setCallerId(number) | Changes caller ID | | setAutoHangup(seconds) | Auto‑hangup after seconds | | setMusic(enabled, musicClass?) | Music on hold | | sendText(text) | Sends text message | | sendImage(image) | Sends image | | databaseGet(family, key) | Gets from Asterisk DB | | databasePut(family, key, value) | Puts into Asterisk DB | | databaseDel(family, key) | Deletes from DB | | databaseDelTree(family, key?) | Deletes DB tree | | exec(application, options?) | Executes any dialplan application | | asyncagiBreak() | Interrupts Async AGI (Asterisk 1.8+) |

Events

AGIServer emits these typed events:

| Event | Payload | |-------|---------| | listening | (host: string, port: number) => void | | close | () => void | | stopped | () => void | | error | (error: Error) => void | | connection | (channel: AGIChannel) => void | | channel_ready | (data: { channel, path, params, headers }) => void | | channel_end | (channel: AGIChannel) => void | | channel_error | (data: { channel, error }) => void | | no_handler | (data: { path, channel }) => void | | handler_error | (data: { error, path, channel }) => void | | execute_error | (data: { error, channel }) => void | | auth_failed | (data: { channel, path, reason }) => void | | auth_error | (data: { channel, path, error }) => void |

Usage Examples

Authentication (global & per‑route)

// Global auth (all routes)
server.auth(async (channel, path, params, headers) => {
  return headers['x-token'] === process.env.GLOBAL_TOKEN;
});

// Route without auth (public)
server.agi('/public', async (channel) => {
  await channel.streamFile('public');
});

// Route with custom auth (overrides global)
server.agi('/admin', {
  handler: async (channel) => {
    await channel.streamFile('admin');
  },
  auth: async (channel, path, params, headers) => {
    return headers['x-admin-token'] === 'super-secret';
  }
});

Handling DTMF Input

server.agi('/survey', async (channel) => {
  await channel.answer();
  const result = await channel.getData('survey-prompt', 30000, 1);
  if (result.data === '1') {
    await channel.streamFile('thanks');
  } else {
    await channel.streamFile('goodbye');
  }
  await channel.hangup();
});

Audio Playback & Recording

// Play a file
await channel.streamFile('welcome', '#', 0);

// Record a message
await channel.recordFile('recording', 'wav', '#', 60000, 0, true, 2);

Using exec() for custom dialplan apps

// Run any Asterisk dialplan application
await channel.exec('MixMonitor', 'recording.wav');
await channel.exec('Dial', 'PJSIP/100,30,Tt');

Graceful Shutdown

// In plain Node.js
process.on('SIGTERM', async () => {
  console.log('Closing AGI server...');
  await server.close();
  process.exit(0);
});

// In NestJS (see NestJS example above)

Error Handling

The library uses a custom AGIError class with error codes:

| Code | Description | |------|-------------| | E_AGI_ALREADY_LISTENING | listen() called twice | | E_AGI_ROUTE_EXISTS | Duplicate route path | | E_AGI_AUTH_EXISTS | Global auth already set | | E_AGI_INVALID_PATH | Invalid route path | | E_AGI_COMMAND_TIMEOUT | No response from Asterisk in 30 seconds | | E_AGI_CHANNEL_DESTROYED | Channel already closed | | E_AGI_SOCKET_END | Socket ended unexpectedly | | E_AGI_SOCKET_ERROR | Socket error |

try {
  await channel.getData('prompt');
} catch (error) {
  if (error.code === 'E_AGI_COMMAND_TIMEOUT') {
    console.log('Asterisk did not respond');
  }
}

Development

Build from source

git clone https://github.com/your-username/agi-server.git
cd agi-server
npm install
npm run build

Project structure

src/
├── agi.channel.ts      # AGIChannel class (all commands)
├── agi.server.ts       # AGIServer class
├── agi.error.ts        # Custom error class
├── types.ts            # Shared types and interfaces
└── index.ts            # Public exports

Running tests

npm test

Compatibility

Node.js: 18.x or higher (uses ES2022 features)

Asterisk: 1.8 and above (all modern versions)

TypeScript: 4.9 or higher

License

MIT

Contributing

Issues and pull requests are welcome. Please ensure your code passes the existing tests and follows the TypeScript style.

Acknowledgements

Asterisk AGI documentation – docs.asterisk.org

Built with Node.js net and events modules.