ID@agntn/ciphersv0.7.0

Pick a cipher. One call.

Caesar to ChaCha20 behind the same two methods. Encode, decode, brute force a Caesar, count letters. It all runs in your process, no network, no keys to sign up for, nothing to configure. For lessons, and for the puzzle you're stuck on at 1am.

Ciphers
72
38 classical · 26 block · 8 stream
Families
16
11 agent tools on top
Network calls
0
every value computed in place
Install$ pnpm add @agntn/ciphers
Callencode("caesar", "ATTACK AT DAWN", …)shift · 01 / 72
Cipher / classical

Caesar Cipher

Shift every letter by N.

Wiring

In14 chars
Out14 chars
Options
{ shift: 3 }
Keyspace
25
Groups
one letter, one cell
Round trip
plaintext comes back
family 3 / 72

Same two methods, every cipher

create("vigenere") gives you a class with encode and decode. Each cipher keeps its own option names. Case survives by default, punctuation passes through, and stripNonAlpha flattens the input when a puzzle wants one clean block. This file walks through 72 ciphers and none of it is a recording, the library computes every line in your browser.

  • create(name) returns one cached instance. resolveCipher fixes case and spaces, nothing fuzzier
  • Every result is { text, cipher, operation, options }. One log line tells you what ran
  • Missing key is a MissingOptionError, shift 26 an InvalidOptionError. Never a silent identity
Read Encode and decode
Filecaesar.tsclassical · computed here

Round trip

import { create } from "@agntn/ciphers";// Caesar Cipher, substitution-shiftconst cipher = create("caesar");const options = { shift: 3 };const encoded = cipher.encode("ATTACK AT DAWN", options);encoded.text;     // "DWWDFN DW GDZQ"encoded.options;  // { shift: 3, preserveCase: true, stripNonAlpha: false }cipher.decode(encoded.text, options).text;// "ATTACK AT DAWN"

Twenty-five shifts, best fit on top

A Caesar has 25 keys and the fastest attack is all of them. The panel takes the current plaintext, shifts it by 3, then decodes it with every shift and ranks the results by how much they read like English. Short samples are the honest failure, three letters don't make a histogram, so the right key sometimes lands second.

  • ciphers brute prints every shift, the one that reads most like English on top
  • ciphers_caesar_brute returns the same list as text, so a model can pick too
  • Shift 0 is not on the list. An identity is not a decode
Read Brute force and frequency
Callciphers brute "DWWDFN DW GDZQ"25 shifts · best fit first
Verdict / caesar

shift 3 ranked 1st

The right key reads most like English, so it's on top.

  1. shift= 3ATTACK AT DAWN
  2. shift=25EXXEGO EX HEAR
  3. shift=11SLLSUC SL VSOF
shift 1 → 25

Count letters before guessing a key

analyzeFrequency(text, "en") tells you whether a ciphertext still has English underneath. A shift keeps the histogram's shape and only moves it. A Vigenère flattens it, and the block ciphers leave nothing but a to f. The panel counts whatever the walk just produced, so watch it collapse when Enigma comes around.

  • Counts sorted by frequency, the expected order for English, Polish or Japanese romaji next to them
  • Index of coincidence near 0.065 is one alphabet, near 0.038 several or none
  • No letters in, undefined out. Not an empty histogram
Read analyzeFrequency
CallanalyzeFrequency(caesar output, "en")12 letters
Counts / caesar

IC 0.136

Close to English at 0.065. One alphabet, maybe moved, the shape survived.

Histogram

English near 0.065, random near 0.038

Seventy-two ciphers, sixteen families

Thirty-eight classical, from Caesar to Enigma, twenty-six block ciphers from DES to Serpent, and eight stream ciphers from XOR to ChaCha20. Latin alphabets are A to Z. Playfair and Polybius fold J into I, tap code shares C and K, Enigma is the Wehrmacht M3 with rotors I, II, III and reflector B, and the block and stream ciphers count bytes, not letters. Every one of those is a choice, and every choice is written down on the cipher's page. A puzzle answer you can't reproduce is not an answer.

Importimport { ciphers, create } from "@agntn/ciphers"

Callciphers()72 names · 38 classical · 26 block · 8 stream

Eleven tools, three hosts

ciphers mcp serves the tools over stdio, the Pi and OMP extensions render them in the terminal. All three call the same executors, so they answer identically and a fix lands once. The page runs those executors too, the text in the dialog is what a model reads. Nothing leaves the machine, there is nowhere for it to go.

  • ciphers_encode, ciphers_decode, ciphers_caesar_brute, ciphers_frequency, ciphers_period_estimate, ciphers_family_guess, ciphers_passphrase_probe, ciphers_crib_drag, ciphers_hidden_text_read, ciphers_key_recover, ciphers_info
  • Arguments checked against the published JSON Schema before a cipher sees them
  • Text and key lengths are bounded. A model can't hand the process a novel
Read MCP, Pi and OMP
Callciphers_info("caesar")
Tool / Shift

Caesar Cipher

Before a model encodes anything it asks which options this cipher takes, and gets them with their defaults.

cipher
"caesar"
required
none
optional
shift
keyspace
25
MCP · Pi · OMPciphers mcp · stdio

Extend Cipher, call register

Every built-in is a concrete class extending the exported abstract Cipher. Yours is the same shape, one file. Throw InvalidOptionError when an option is wrong and let normalizeError wrap the rest. No base class magic, no plugin manifest.

  • name(), info(), encode() and decode(). The same four the built-ins implement
  • register(name, Class) makes it visible to create and resolveCipher
  • Register a name again and the cached instance is dropped, so a hot reload takes
Read Custom ciphers
Filereverse.tsfolded · copy is whole
import { Cipher, register } from "@agntn/ciphers";import type { CipherInfo, CipherResult } from "@agntn/ciphers";class Reverse extends Cipher {  name() { return "reverse"; }  info(): CipherInfo { /* name, label, family, options */ }  encode(text: string): CipherResult {    const out = [...text].reverse().join("");    return { text: out, cipher: "reverse", operation: "encode", options: {} };  }  decode(text: string): CipherResult { /* encode, as decode */ }}register("reverse", Reverse);
Startpnpm add @agntn/ciphersNode.js 26 or newer

Start with one command

One install gives you the library, the ciphers CLI and the MCP server. Pick a cipher by name, hand it text and options, and read the result back the same way for all of them.

  • PinPre-1.0, so pin exact versions.
  • SecretsFine for a riddle, a lesson or a CTF. Useless for a real secret.
  • OfflineNothing to fetch, no key to sign up for, nothing to configure.

First call

$ pnpm add @agntn/ciphersimport { analyzeFrequency, create } from "@agntn/ciphers";const vigenere = create("vigenere");vigenere.encode("ATTACK AT DAWN", { key: "LEMON" }).text;  // "LXFOPV EF RNHR"vigenere.decode("LXFOPV EF RNHR", { key: "LEMON" }).text;  // "ATTACK AT DAWN"analyzeFrequency("LXFOPV EF RNHR")?.ic;  // 0.030