Skip to content

About

Dependency-free Asterisk Manager Interface (AMI) client for Node.js. TypeScript, event streaming, auto-reconnect, ESM + CommonJS.

Topics

Resources

Stars

14 stars

Watchers

0 watching

Forks

Repository files navigation

asterisk-ami-node

npm CI license

A small, dependency-free Asterisk Manager Interface client for Node.js.

AMI is a line protocol over TCP, so this package hand-rolls it against node:net and node:tls. Nothing else is pulled into your tree.

npm install asterisk-ami-node
  • Zero runtime dependencies
  • TypeScript types included, written in TypeScript
  • Ships both ESM and CommonJS
  • Action replies and the event stream kept apart, including during list actions
  • Optional auto-reconnect with backoff, and a keepalive that detects a dead socket
  • Refuses to send fields containing line breaks, which would otherwise inject a second action
  • Node 18 or newer

Quick start

import { AmiClient, listRows } from "asterisk-ami-node";

const ami = new AmiClient({
  host: "127.0.0.1",
  port: 5038,
  username: "admin",
  password: "secret",
});

await ami.connect();
console.log(ami.serverGreeting); // Asterisk Call Manager/7.0.3

const reply = await ami.action({ Action: "CoreShowChannels" });
for (const row of listRows(reply, "CoreShowChannel")) {
  console.log(row.Channel, row.CallerIDNum, row.Duration);
}

ami.close();

CommonJS works the same way:

const { AmiClient } = require("asterisk-ami-node");

Asterisk side setup

Add a manager account in /etc/asterisk/manager.conf:

[general]
enabled = yes
port = 5038
bindaddr = 127.0.0.1

[admin]
secret = secret
read = system,call,command,agent,user,config,dtmf,reporting,cdr,dialplan
write = system,call,command,agent,user,config,originate

Then asterisk -rx "manager reload". Keep bindaddr on localhost and reach it over a tunnel or a private network. AMI has no transport security of its own unless you turn on TLS.

Listening for events

Every event that is not part of an action reply is emitted twice: once on event, and once under its own name.

const ami = new AmiClient({ host, username, password, events: "on" });

ami.on("Newchannel", (e) => console.log("up", e.Channel));
ami.on("Hangup", (e) => console.log("down", e.Channel, e.Cause));
ami.on("event", (e) => metrics.count(e.Event));

await ami.connect();

Set events: "off" if you only want to run actions, or name specific classes such as events: "call,agent" to cut the volume down.

Rows belonging to a list action never reach these handlers. CoreShowChannel events raised by your own CoreShowChannels call are returned to the caller instead, which is usually what you wanted and is easy to get wrong by hand.

Running CLI commands

const out = await ami.command("pjsip show endpoints");
console.log(out);

command() returns the raw text Asterisk prints. Asterisk 14 and later send it back as a repeated Output header, which this client folds into one newline-joined string.

Anything passed to command() runs with full manager privileges. Never hand it unreviewed input. If you need to expose it to users, check the command against an allow list first.

Staying connected

const ami = new AmiClient({
  host,
  username,
  password,
  keepAliveMs: 30_000,
  reconnect: { delayMs: 1000, maxDelayMs: 30_000, retries: Infinity },
});

ami.on("reconnecting", (attempt, delay) => console.warn(`retry ${attempt} in ${delay}ms`));
ami.on("reconnected", () => console.log("back"));
ami.on("error", (err) => console.error(err.message));

Reconnect is off unless you ask for it. The delay doubles each attempt up to maxDelayMs. Calling close() stops it for good.

A dropped TCP connection can go unnoticed for a long time on an idle manager session, so keepAliveMs sends a Ping action on that interval and tears the socket down if it goes unanswered.

Line breaks are rejected, not stripped

AMI has no escaping. A value carrying CR or LF ends the current action early and the rest is read as a second action, so a channel name built from user input can turn a Originate into a Command.

await ami.action({ Action: "Originate", Channel: "PJSIP/1\r\nAction: Command" });
// AmiError: Refusing to send AMI field "Channel": line breaks in a value can inject a second action

Rejecting is deliberate. Silently stripping the break would send an action you did not write.

API

new AmiClient(options)

Option Default Meaning
host required Asterisk host
port 5038 Manager port
username required Manager account name
password required Manager secret
tls false Wrap the socket in TLS, needs tlsenable=yes
rejectUnauthorized false Verify the certificate when tls is on
timeoutMs 10000 Connect timeout, and default action timeout
events "on" Value sent in the Events field at login
keepAliveMs off Interval for the background Ping
reconnect off true, or { retries, delayMs, maxDelayMs }

Methods

  • connect() opens the socket and logs in. Rejects with AmiError on a bad secret.
  • action(fields, timeoutMs?) sends an action and resolves with every message carrying its ActionID, the ...Complete event included.
  • command(cli, timeoutMs?) runs a CLI command and resolves with its text output.
  • send(fields) writes an action without waiting for a reply.
  • close() disconnects and cancels any reconnect.

Properties

  • isConnected
  • serverGreeting the Asterisk Call Manager/x.y.z banner

Events

connect, close, error, reconnecting, reconnected, message (every parsed message), event (unsolicited events only), plus one channel per AMI event name.

Helpers

  • parseMessage(raw) parses one AMI message, folding repeated keys with newlines.
  • serializeAction(fields) builds an action frame, throwing on line breaks.
  • listRows(messages, eventName) keeps only the rows of a list action.
  • AmiError the error class everything here throws.

Timeouts on list actions

If a list action times out after some rows have already arrived, those rows are resolved rather than thrown away. A missing ...Complete event on a busy switch is common, and partial data beats an exception that loses the lot.

Related

  • pbx-mcp, a Model Context Protocol server that gives AI assistants a read-only window into Asterisk and FreeSWITCH. This client came out of building it.
  • freeswitch-esl-node, the same idea for the FreeSWITCH Event Socket.
  • ICTCore, the open source telephony framework behind our products.

About

Built and maintained by Tahir Almas at ICT Innovations, who have been shipping open source telephony since 2005.

Issues and pull requests are welcome. Bug reports with the actual error string are the most useful thing you can send.

License

MIT

About

Dependency-free Asterisk Manager Interface (AMI) client for Node.js. TypeScript, event streaming, auto-reconnect, ESM + CommonJS.

Topics

Resources

Stars

14 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages