Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@

## Ciphers

| Cipher | Family | Self-inverse | Options |
|--------|--------|:---:|---------|
| **caesar** | substitution-shift | ✗ | `--shift` (1-25, default 3) |
| **rot13** | substitution-shift | ✓ | - |
| **rot47** | substitution-shift | ✓ | - |
| **atbash** | substitution-reflection | ✓ | - |
| **vigenere** | polyalphabetic | ✗ | `--key` (required) |
| **trithemius** | polyalphabetic | ✗ | - |
| **alberti** | polyalphabetic | ✗ | `--key`, `--period` (both required) |
| **rail-fence** | transposition | ✗ | `--rails` (default 3) |
| **affine** | substitution-multiplicative | | `--a` (multiplier), `--b` (shift) |
| **playfair** | digraph | ✗ | `--key` (required) |
| **polybius** | fractionation | ✗ | `--key` (optional) |
| **morse** | fractionation | ✗ | - |
| **bacon** | fractionation | ✗ | - |
| **tap-code** | fractionation | ✗ | - |
| **columnar** | transposition | ✗ | `--key` (required) |
| **adfgvx** | fractionation | ✗ | `--key` (optional) |
| **bifid** | fractionation | ✗ | `--key` (optional), `--period` (default 5) |
| **enigma** | rotor | ✓ | `--positions`, `--rings`, `--plugboard` |
| Cipher | Family | Self-inverse | Options |
| -------------- | --------------------------- | :----------: | ------------------------------------------ |
| **caesar** | substitution-shift | ✗ | `--shift` (1-25, default 3) |
| **rot13** | substitution-shift | ✓ | - |
| **rot47** | substitution-shift | ✓ | - |
| **atbash** | substitution-reflection | ✓ | - |
| **vigenere** | polyalphabetic | ✗ | `--key` (required) |
| **trithemius** | polyalphabetic | ✗ | - |
| **alberti** | polyalphabetic | ✗ | `--key`, `--period` (both required) |
| **rail-fence** | transposition | ✗ | `--rails` (default 3) |
| **affine** | substitution-multiplicative | | `--a` (multiplier), `--b` (shift) |
| **playfair** | digraph | ✗ | `--key` (required) |
| **polybius** | fractionation | ✗ | `--key` (optional) |
| **morse** | fractionation | ✗ | - |
| **bacon** | fractionation | ✗ | - |
| **tap-code** | fractionation | ✗ | - |
| **columnar** | transposition | ✗ | `--key` (required) |
| **adfgvx** | fractionation | ✗ | `--key` (optional) |
| **bifid** | fractionation | ✗ | `--key` (optional), `--period` (default 5) |
| **enigma** | rotor | ✓ | `--positions`, `--rings`, `--plugboard` |

`alberti` implements the reproducible, simplified keyed-disk variant: the inner disk rotates one position after each `period` Latin letters.

Expand Down
5 changes: 1 addition & 4 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ export default defineBuildConfig({
entries: [
{
type: 'bundle',
input: [
'./src/index.ts',
'./src/cli.ts',
],
input: ['./src/index.ts', './src/cli.ts'],
},
],
})
46 changes: 25 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,27 @@
{
"name": "@agntn/ciphers",
"version": "0.1.1",
"type": "module",
"description": "Unified educational and puzzle cipher library for agents",
"bin": {
"ch": "./dist/cli.mjs"
},
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs"
}
},
"bin": {
"ch": "./dist/cli.mjs"
},
"pi": {
"extensions": [
"./packages/pi/extensions/*.ts"
]
},
"omp": {
"extensions": [
"./packages/omp/extensions/ciphers.ts"
]
},
"engines": {
"node": ">=25"
},
"packageManager": "pnpm@10.33.4",
"scripts": {
"build": "obuild",
"dev": "obuild --stub",
"check": "vp check",
"lint": "vp lint",
"format": "vp fmt",
"format:check": "vp fmt --check",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.omp.json",
"test": "vitest",
"test:run": "vitest --run"
"test": "vp test",
"test:run": "vp test"
},
"dependencies": {
"citty": "^0.2.2",
Expand All @@ -47,6 +37,20 @@
"tsx": "^4.22.4",
"typebox": "^1.1.39",
"typescript": "^6.0.3",
"vitest": "^4.1.7"
"vite-plus": "0.2.9"
},
"engines": {
"node": ">=25"
},
"packageManager": "pnpm@10.33.4",
"omp": {
"extensions": [
"./packages/omp/extensions/ciphers.ts"
]
},
"pi": {
"extensions": [
"./packages/pi/extensions/*.ts"
]
}
}
95 changes: 72 additions & 23 deletions packages/omp/extensions/ciphers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const MAX_BRUTE_TEXT_LENGTH = 2_000
const MAX_FREQUENCY_TEXT_LENGTH = 100_000
const MAX_KEY_LENGTH = 1_000


type CipherParams = {
cipher: string
text: string
Expand All @@ -34,9 +33,10 @@ let libraryPromise: Promise<CiphersLibrary> | undefined
/** Load source in a Git checkout, or the built package export after installation. */
function loadLibrary(): Promise<CiphersLibrary> {
const isCheckout = existsSync(fileURLToPath(checkoutMarker))
libraryPromise ??= isCheckout && existsSync(fileURLToPath(sourceEntry))
? import(sourceEntry.href)
: import('@agntn/ciphers')
libraryPromise ??=
isCheckout && existsSync(fileURLToPath(sourceEntry))
? import(sourceEntry.href)
: import('@agntn/ciphers')
return libraryPromise
}

Expand All @@ -57,24 +57,64 @@ function buildOptions(params: CipherParams): Record<string, unknown> {
return options
}


/** Register local educational and puzzle-cipher tools in OMP. */
export default function ciphersExtension(omp: ExtensionAPI): void {
const { Type } = omp.typebox
const cipherParams = Type.Object({
cipher: Type.String({ maxLength: 32, description: 'Exact built-in cipher name' }),
text: Type.String({ maxLength: MAX_TRANSFORM_TEXT_LENGTH, description: 'Text to transform' }),
shift: Type.Optional(Type.Integer({ minimum: 1, maximum: 25, description: 'Caesar shift (1-25; default 3)' })),
key: Type.Optional(Type.String({ maxLength: MAX_KEY_LENGTH, description: 'Key for keyed ciphers' })),
rails: Type.Optional(Type.Integer({ minimum: 2, description: 'Rail Fence rails (at least 2; default 3)' })),
a: Type.Optional(Type.Integer({ minimum: 1, maximum: 25, description: 'Affine multiplier, coprime with 26 (default 5)' })),
b: Type.Optional(Type.Integer({ minimum: 0, maximum: 25, description: 'Affine additive shift (0-25; default 8)' })),
period: Type.Optional(Type.Integer({ minimum: 1, description: 'Rotation or fractionation period for Alberti and Bifid' })),
preserveCase: Type.Optional(Type.Boolean({ description: 'Preserve letter case (default true)' })),
stripNonAlpha: Type.Optional(Type.Boolean({ description: 'Remove non-letter characters before processing (default false)' })),
positions: Type.Optional(Type.String({ pattern: '^[A-Za-z]{3}$', description: 'Enigma initial rotor positions (default AAA)' })),
rings: Type.Optional(Type.String({ pattern: '^[A-Za-z]{3}$', description: 'Enigma ring settings (default AAA)' })),
plugboard: Type.Optional(Type.String({ maxLength: 38, description: 'Enigma plugboard pairs, for example \"AV BS CG\"' })),
shift: Type.Optional(
Type.Integer({ minimum: 1, maximum: 25, description: 'Caesar shift (1-25; default 3)' }),
),
key: Type.Optional(
Type.String({ maxLength: MAX_KEY_LENGTH, description: 'Key for keyed ciphers' }),
),
rails: Type.Optional(
Type.Integer({ minimum: 2, description: 'Rail Fence rails (at least 2; default 3)' }),
),
a: Type.Optional(
Type.Integer({
minimum: 1,
maximum: 25,
description: 'Affine multiplier, coprime with 26 (default 5)',
}),
),
b: Type.Optional(
Type.Integer({
minimum: 0,
maximum: 25,
description: 'Affine additive shift (0-25; default 8)',
}),
),
period: Type.Optional(
Type.Integer({
minimum: 1,
description: 'Rotation or fractionation period for Alberti and Bifid',
}),
),
preserveCase: Type.Optional(
Type.Boolean({ description: 'Preserve letter case (default true)' }),
),
stripNonAlpha: Type.Optional(
Type.Boolean({
description: 'Remove non-letter characters before processing (default false)',
}),
),
positions: Type.Optional(
Type.String({
pattern: '^[A-Za-z]{3}$',
description: 'Enigma initial rotor positions (default AAA)',
}),
),
rings: Type.Optional(
Type.String({ pattern: '^[A-Za-z]{3}$', description: 'Enigma ring settings (default AAA)' }),
),
plugboard: Type.Optional(
Type.String({
maxLength: 38,
description: 'Enigma plugboard pairs, for example "AV BS CG"',
}),
),
})

omp.registerTool({
Expand Down Expand Up @@ -116,7 +156,10 @@ export default function ciphersExtension(omp: ExtensionAPI): void {
label: 'Brute Force Caesar',
description: 'Decode Caesar ciphertext with every shift from 1 through 25.',
parameters: Type.Object({
text: Type.String({ maxLength: MAX_BRUTE_TEXT_LENGTH, description: 'Caesar ciphertext to brute-force' }),
text: Type.String({
maxLength: MAX_BRUTE_TEXT_LENGTH,
description: 'Caesar ciphertext to brute-force',
}),
}),
approval: 'read',
loadMode: 'essential',
Expand All @@ -138,10 +181,11 @@ export default function ciphersExtension(omp: ExtensionAPI): void {
description: 'Analyze A-Z letter frequencies and compare their order with English or Polish.',
parameters: Type.Object({
text: Type.String({ maxLength: MAX_FREQUENCY_TEXT_LENGTH, description: 'Text to analyze' }),
lang: Type.Optional(Type.Union([
Type.Literal('en'),
Type.Literal('pl'),
], { description: 'Reference language (default en)' })),
lang: Type.Optional(
Type.Union([Type.Literal('en'), Type.Literal('pl')], {
description: 'Reference language (default en)',
}),
),
}),
approval: 'read',
loadMode: 'essential',
Expand All @@ -153,12 +197,17 @@ export default function ciphersExtension(omp: ExtensionAPI): void {
}

const maximum = analysis.counts[0]?.[1] ?? 1
const lines = [`Frequency Analysis (${analysis.total} letters, lang=${analysis.language}):`, '']
const lines = [
`Frequency Analysis (${analysis.total} letters, lang=${analysis.language}):`,
'',
]

for (const [character, count] of analysis.counts) {
const percentage = ((count / analysis.total) * 100).toFixed(1)
const bar = '#'.repeat(Math.ceil((count / maximum) * 15))
lines.push(` ${character} ${String(count).padStart(4)} (${percentage.padStart(5)}%) ${bar}`)
lines.push(
` ${character} ${String(count).padStart(4)} (${percentage.padStart(5)}%) ${bar}`,
)
}
lines.push('', `Expected (${analysis.language}): ${analysis.reference.split('').join(' ')}`)
lines.push(`Actual: ${analysis.counts.map(([character]) => character).join(' ')}`)
Expand Down
Loading