Custom Encodings
The interface
Every built-in is an Encoding, and yours is the same shape:
interface Encoding {
readonly name: string;
info(): EncodingInfo;
encode(input: string | Uint8Array, options?: EncodeOptions): string;
decode(text: string): { bytes: Uint8Array; details: Record<string, string | number> };
}
No base class. info() describes it. Label, family, standard and the alphabet. Whether it has a checksum or padding. The options encode takes.
Octal, because a puzzle said so
import { DecodeError, encode, identify, register, type Encoding } from "@agntn/encodings";
const octal: Encoding = {
name: "octal",
info: () => ({
name: "octal",
label: "Octal",
description: "Three octal digits per byte, the way od -b prints them",
family: "octal",
standard: "od -b",
alphabet: "01234567",
checksum: false,
padding: false,
options: [],
}),
encode: (input) =>
Array.from(typeof input === "string" ? new TextEncoder().encode(input) : input, (byte) =>
byte.toString(8).padStart(3, "0"),
).join(" "),
decode(text) {
const groups = text.trim().split(/\s+/u);
if (groups.some((group) => !/^[0-3][0-7]{2}$/u.test(group))) {
throw new DecodeError("octal", "every byte is three digits, 000 to 377");
}
return { bytes: Uint8Array.from(groups, (group) => Number.parseInt(group, 8)), details: {} };
},
};
register(octal);
encode("octal", "hi"); // "150 151"
identify("150 151")[0]?.encoding; // "octal"
register replaces anything with the same name, built-ins included. Want your own base64 that pads differently? You can. Should you? Probably not.
What comes for free
Once registered, encode, decode, create, encodings() and encodingInfos() all know it. So does identify, with no extra wiring. So do the tools in the same process. info().alphabet feeds the small alphabet score. checksum: true gets the checksum score. Only claim a checksum your decode actually checks.
Errors
Throw DecodeError for text that isn't valid. ChecksumError for a checksum that doesn't match. InvalidOptionError for a bad option. All three extend EncodingError. The CLI turns that into one line on stderr, the MCP server into a tool error. Anything else counts as a bug and keeps its stack trace.