From fsi to fsd: rebuilding formseal-decrypt

A day ago, I wrote about moving formseal to a GitHub organization. That post covered the big-picture migration - renaming repos, setting up org structure, and the PyPI naming situation.

But there was another migration hiding inside that one: renaming the CLI tool from fsi to fsd, and making it modular in the process.


The context

formseal-inbox was named with a vague idea in mind - I imagined it becoming some kind of terminal inbox, messages, viewing, maybe a TUI. That never happened. The tool was always just a decryptor:

ciphertexts.jsonl → decrypted data

At some point "inbox" started feeling wrong. The name implied features that didn't exist and kept me entertaining ideas that had no business being in a decryption tool. So I renamed it:

formseal-inbox → formseal-decrypt
fsi            → fsd

But while I was at it, I rebuilt the output system to be modular - so adding new formats down the line would be straightforward instead of a mess of if/else.


What changed

Package renamed

fsi/ → fsd/
fsi/fsi.py → fsd/fsd.py

Every import shifted from from fsi.ui import ... to from fsd.ui import .... Tedious but clean.

Output formats became modular

Originally, decrypt just hardcoded JSONL output:

with open(dest, 'w') as f:
    for msg in decrypted:
        f.write(json.dumps(msg) + '\n')

Now there's a proper format system with a base class:

# formats/formatter.py
class Formatter(ABC):
    name: str = ""
    extension: str = ""

    @abstractmethod
    def write(self, data: list[dict], path: Path):
        pass

Each format lives in its own file:

formats/
├── formatter.py   # base class
├── jsonl.py
└── json.py

Adding CSV or table output later is just: drop in a new file, register it, done. No touching the core decrypt command.

fsd format decision

Connect now asks for format

Source File (ciphertexts): formseal.ct.jsonl
Destination Directory: ./output
Output Format [JSON Lines, JSON]: json
Private Key: <your-key>

Saved in config so you don't have to specify it every time. And the before/after is pretty clean:

# Before
fsi decrypt  # jsonl, no choice

# After
fsd decrypt --format json

Issues I hit

Nothing catastrophic, but a few things that were easy to miss:

  • GitHub Actions still pointed to fsi/ directories that no longer existed. The inject-version workflow was trying to update a file path that had moved. Quick fix once I caught it.
  • Empty __init__.py files were scattered everywhere - unnecessary once you explicitly list packages in pyproject.toml. Deleted most of them, kept the ones with actual imports.
  • Docs took longer than expected. README, CONTRIBUTING, SECURITY - all had old repo references (grayguava/formseal-inbox → useFormseal/decrypt) and old command names. Not hard, just slow.

[!NOTE] Core decryption logic stayed completely untouched - NaCl sealed boxes, base64url handling, error flows. Only output handling changed.


Version bump

The old formseal-inbox was at v0.1.0. Given this is a new repo with a meaningful refactor, I bumped to v0.2.0. I'll create a v0.1.0 tag pointing back to the old inbox repo for continuity.


Where this leaves us

Tool Status
formseal-embed active
formseal-fetch active
formseal-decrypt active
formseal-inbox archived

The rename forced me to actually think about what the tool is. The answer was simple: just a decryptor. And that simplicity is worth preserving.