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
$ pnpm add @agntn/ciphersCaesar Cipher
Shift every letter by N.
Wiring
- Options
- { shift: 3 }
- Keyspace
- 25
- Groups
- one letter, one cell
- Round trip
- plaintext comes back
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
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
shift 3 ranked 1st
The right key reads most like English, so it's on top.
- shift= 3ATTACK AT DAWN
- shift=25EXXEGO EX HEAR
- shift=11SLLSUC SL VSOF
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
IC 0.136
Close to English at 0.065. One alphabet, maybe moved, the shape survived.
Histogram
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"
classical
- caesar
- rot13
- rot47
- atbash
- substitution
- vigenere
- gronsfeld
- beaufort
- porta
- autokey
- running-key
- trithemius
- alberti
- quagmire-1
- quagmire-2
- quagmire-3
- quagmire-4
- chaocipher
- rail-fence
- affine
- playfair
- four-square
- two-square
- hill
- polybius
- nihilist
- morse
- fractionated-morse
- bacon
- tap-code
- a1z26
- book
- columnar
- route
- adfgvx
- bifid
- straddling-checkerboard
- enigma
block
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
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
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
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);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