asterisk-ami-node
v1.0.0
Published
Dependency-free Asterisk Manager Interface (AMI) client for Node.js. TypeScript types, event streaming, auto-reconnect, ESM and CommonJS.
Maintainers
Readme
asterisk-ami-node
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,originateThen 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 actionRejecting 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 withAmiErroron a bad secret.action(fields, timeoutMs?)sends an action and resolves with every message carrying itsActionID, the...Completeevent 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
isConnectedserverGreetingtheAsterisk Call Manager/x.y.zbanner
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.AmiErrorthe 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
