Work in progress EiN runs, but nothing is released yet: the installer builds it from source, and what this page shows may change. Nothing is released yet. How to try it

EiN GitHub

Everything is a Node.

EiN is a system you host yourself, built on one idea: files, accounts, logs, queues and even its own commands are nodes in one tree, each with an owner and permissions. You work on the tree from a web interface or a real bash shell, pipelines act on every change, and modules decide what it is for.

Each tile is a node: the Archive demo's people and albums, beside shells, logs, pipelines, recordings, places and letters.

One tree of nodes

A node has a type, the attributes that type needs, an owner, a group and permission bits, and a name in a folder. It can have names in several folders at once, as a hard link does, so one thing is never copied to be in two places. Each part of the tree is kept by a store, and a node looks the same whichever store holds it.

id
01JZ8Q4T2MXK6V3B9C7D5E1F0G, a ULID that never changes, whatever the node is renamed or moved to
type
file, user, queue, or a type a module adds
props
Owner, group and mode, with an optional access list; who created it, last changed it and last wrote it, and when; how many folders hold it
attrs
JSON shaped by the type: a file's mimetype, size and SHA-256, a log's retention, a position's shape on the map
What every node is made of.

The core's 17 node types

Content

directory file softlink

Space and similarity

position track embedding

People and access

user group session tty share-link

The machinery

command log queue pool router null

A track is a timed GPS route, an embedding a vector for searching by likeness. A pool holds background jobs that workers claim, a router broadcasts live events. Modules add types of their own, and the documentation lists every type with the module that adds it.

A file system for everything

The nodes form one virtual file system with a familiar layout. The commands are nodes under /usr/bin, every account is a node under /etc/users, and a service is a folder with a start script. Parts of the tree can be mounted from elsewhere: memory for /tmp, a folder on the host, or another machine over SFTP.

  • Permissions work as in Unix, with owner, group and other bits, plus access lists that only grant. One check applies in the browser, the shell, SSH and SFTP.
  • Every change is written to an audit trail in the same transaction: who, from which session or pipeline, and the values before and after.
  • A deleted node waits 30 days in its owner's trash, and restoring it puts back the same nodes, links and all.
  • Every change is an event that pipelines and commands can wait for: tail -f follows a log as it is written, and cat on a router node prints events as they arrive.
/
├── archive      what the modules keep
├── home/helen   an account's own files and trash
├── etc          users, groups, services, settings
├── var          logs, sessions, queues, pools,
│                pipeline runs
├── usr
│   ├── bin      every command, as a node
│   ├── modules  the installed modules
│   └── docs     the documentation
├── dev          null, and the live events
└── tmp          in memory, swept after a day

A real shell on the tree

EiN comes with a bash-compatible shell and more than 200 commands of its own, from ls, find and awk to managing users, services and pipelines. Modules add theirs beside them. The shell runs inside EiN, so a command works on nodes, as the person who runs it, with that person's permissions.

Through every door

Press Alt+T on any page for a terminal, or log in with SSH. The same server speaks SFTP, so the tree mounts as a folder.

$ ssh -p 2222 helen@ein.example
helen@:~$ ls /etc/services
helen@:~$ tail -f /var/logs/system
helen@:~$ JSON_OUTPUT=1 ls ~
helen@:~$ nohup ./reindex.sh &
$ sshfs -p 2222 helen@ein.example:/ ~/ein -o transform_symlinks
  • The parser and the executor are libraries of their own, @ein/bash-parser and @ein/bash-executor, and their bash compatibility is measured with GNU bash's own test suite.
  • With JSON_OUTPUT=1, commands answer in JSON; text tools such as grep stay text.
  • A bash script that declares its arguments becomes a command, with --help and Tab completion.
  • Jobs, pipes and signals work as in bash. A job started with nohup keeps running after the tab or the SSH session closes.
  • Every command declares whether it reads or writes, and some are for administrators only, so a whole command line can be judged before it runs.

A web interface that runs the same commands

The browser keeps one connection open to the server and gets each page's data by running shell commands as you, reading their answers in JSON. Anything you can click you can script, and what you may not see in the shell you cannot see in the browser either.

The core brings the frame and the pages every installation needs: a home page, search, a finder on Ctrl+K that also runs commands, the terminal, a node explorer, the system dashboard, accounts, groups and permissions, a trash, and documentation for every command, setting and node type. A module adds entries to the sidebar and the top bar and the pages they open, as a page, a dialog, a drawer or full screen.

  1. The doors The web interface, its terminal, SSH and SFTP
  2. The shell A command line, run as the person who sent it
  3. One permission check The same for every door, down to each node and its folders
  4. The tree Nodes, links and events, in whichever store holds them

Pipelines act on every change

A pipeline is a short YAML file: what starts it, a change to the tree or a fact a module announces, and the commands to run, as steps that may depend on each other. Work waits in durable queues, so nothing is lost when the server restarts, and a node waits once for a pipeline however many times it changes.

name: inbox-unzip
description: Unpack a zip dropped in an inbox, then remove it

trigger:
  event: updated
  nodeType: file
  match:
    nodePath: '/home/*/inbox/**'
    mimetype: application/zip
  # once the upload has ended: its checksum is written last
  filter: JSON_OUTPUT=1 attrs "$NODE_PATH" | jq -e .sha256 > /dev/null

steps:
  expand:
    command: unzip -n -d "$(dirname $NODE_PATH)" $NODE_PATH
  remove:
    command: rm $NODE_PATH
    dependsOn: [expand]

options:
  maxAttempts: 2

A whole pipeline: when a zip has finished arriving in someone's inbox folder, unpack it beside itself and remove it. pipeline define registers it.

  • A step can ask for a pool of workers and the resources it needs, such as room on the graphics card, and runs when they are free.
  • A step that meets a busy service hands its job back and waits, without counting it as a failure. Bad input fails at once and is not retried.
  • A failed run is tried again after an hour, six hours and a day, then marked as needing a person.
  • Bulk work, such as reprocessing everything a module keeps, gives way to what was just added.
  • Services, from the web server to the pipeline engine, are supervised and restarted by their own policy, each with its own log.

It tells you whether it is keeping up

The system dashboard opens on one word, Healthy, Catching up, Stuck or Needs a person, with the reasons for it. Its tabs show the machine's load, how long the disks will last, every pipeline with its backlog and when it will be done, the services and a live log.

The system dashboard saying Healthy, nothing waiting, with CPU, memory, GPU and video memory gauges, the work done per hour, and the free space on each disk

Everything else is a module

The core is the tree, the shell, the pipelines, accounts and sharing, and the web interface's frame. What EiN is for comes from modules. A module is a folder with a manifest, and what it adds is what its folders hold.

my-module/
├── mod.json     name, version, the core API it needs,
│                other modules, settings, node types
├── bin/         its commands
├── pipelines/   its pipelines
├── services/    what runs in the background
├── types/       the attributes of its node types
├── ui/          its menu entries and pages
├── docs/        its pages in the documentation
└── etc/         its own settings, and what it
                 gives other modules
  • A module declares the version of the core's API it is written for, and the modules it builds on.
  • Its settings and node types appear in the documentation from the manifest alone.
  • Names join a module to the core's pages: a command called search-… adds results to search, and one called news-… adds a section to the home page.
  • Modules meet without naming each other: what one keeps in its etc/<name>.d/ is linked into /etc/<name>.d/, for whichever module reads that folder.

Planned Modules installed from a URL: each is already a repository of its own under einhq.

Assistant

Ask EiN in your own words

The Assistant is a module that adds a chat beside every page, and a page of earlier conversations. It answers by running EiN's own commands, as you, with your permissions, and shows each command it ran.

It is one more door onto the same layers as the browser and the shell, so what you may not see there it cannot see either. On its own it knows the system; each module installed beside it teaches it that module's part.

The commands are its tools

A small language model cannot keep hundreds of commands in mind, so the assistant offers it the few used most and a way to find the rest. It searches every command on your path by what it does, by meaning and by words, reads that command's own --help, and runs a line of bash in EiN's shell. A new command is offered the moment it describes itself.

  • A line that only reads runs at once. A line that changes anything is shown to you to run or decline, and the assistant goes on from how it went.
  • It knows the page you have open: its title, what it shows and the node it is about.
  • Each line gets a minute. When lines run too long, it answers from what it has and says what was slow.
  1. Your question "Is a pipeline stuck?", and the page you are on
  2. find_commands Every command, searched by what it does
  3. command_help The command's arguments and flags, and an example
  4. run_command pipeline-overview, run in the shell as you
  5. The answer Text with links, and a table, a chart or a card when it helps

It knows the system, and shows its work

Without any other module, the assistant knows what the core keeps: files and folders, logs, services, pipelines, accounts and storage. Ask it as you would ask whoever runs the machine.

  • What is in my home folder?ls -l ~
  • Which services are running?service-list --detail
  • How much disk is left?df
  • Which users are there?user-list
  • Put "Hello" in a file called hello.txtecho "Hello" > ~/hello.txtRun
  • Unfold "Ran 2 commands" under an answer to see each command line and what it printed.
  • A link in an answer is kept only when the node exists and you may see it; a path the model made up becomes plain words.
  • A table or a chart of a log is read from the command's output, not copied out by the model, so no value moves to the wrong row.
  • It replies in the language you asked in.
  • Every conversation is a log in your own home, private to you.

Modules teach it their part

The assistant names no module. A module that has something for it keeps a folder for it, which EiN links into /etc/assistant.d/: the commands it offers as tools, a page on what it keeps and where, and the cards it can draw under an answer. The assistant brings its own: ls, stat, find and cat for the tree, and a chart.

/etc/assistant.d/
└── <module>/
    ├── tools        the commands it offers, one a line
    ├── prompt.md    what it keeps, and where
    └── cards.json   what it can draw under an answer

Your model, on your machine

By default the model is Qwen3.5-4B in llama.cpp, on your own graphics card or processor. Any server that speaks OpenAI's chat API can take its place: vLLM, Ollama, LM Studio, or a hosted provider such as OpenAI, OpenRouter, Mistral or Groq. With a hosted provider named, the chat tells everyone that their questions, and what the assistant reads to answer them, are sent there.

  • Its instructions are a file, /etc/assistant-prompt.md, that the administrator can change without a new release.
  • Sharing the graphics card with batch work, the model answers more slowly while that runs, and the chat says so.
  • While the model is starting, the chat says it is waiting, and gives up after three minutes.

Archive

A family's whole life, kept at home

EiN Archive, the first module, keeps a family's photos, home videos, recordings and letters. It dates every file, puts what has GPS on the map, recognises the faces and voices, transcribes speech and reads handwriting.

It is built from the core's pieces. People, albums, places and devices are node types it adds; its pages are routes in the core's frame; its 130 commands work in the shell like any other; and 26 pipelines, run by the core's engine, process every file as it arrives. The screenshots are from the demo: one invented family, 616 files from 1956 to 2026.

One picture, and everything around it

Open a photo and EiN shows which phone took it and whose phone it was, who is in it and how old each of them was that day, and where it was taken, down to the street. Each of those is a node of its own, linked to the picture: the phone, the three people and the place all have pages and timelines of their own.

EiN's photo viewer showing three people on a coastal lookout near Wellington, with a side panel listing the phone that took it, the album, the three people with their ages, the address and a small map, and similar pictures
  1. The phone that took it, and who owned it then
  2. The album it is in
  3. Who is in it, and how old each was that day
  4. Where, from the GPS in the file
  5. Pictures that look alike
Christmas in Wellington, 2025: Ruby with her aunt Helen and Helen's husband David. From the demo archive of the invented Wrenfield family.

The Archive, page by page

The Archive module is where a family's collection lives: albums, a full-screen viewer, people and their family tree, places, devices, a map and a year view. These screenshots are from the demo: one invented family, 616 files from 1956 to 2026.

The Albums page: a grid of album covers with titles and date ranges

Albums across the decades

Every album is a cover with its dates and how much it holds. Sort by date, name or size, filter by title, or jump across decades on the time rail. One picture can sit in several albums without being copied.

Every kind of file a family keeps

Drop in a phone's camera roll, a box of scans or a Google Takeout. Each file is recognised by its bytes, not its name, and gets what its kind needs: a picture to show in the browser, a copy that plays, its text and its date.

Photos
JPEG, PNG, HEIC, AVIF and the other common formats. Every picture is shown from AVIF copies made at the sizes the pages need.
Camera RAW
22 RAW formats, from Canon's CR2 and CR3, Nikon's NEF and Sony's ARW to DNG, Fujifilm's RAF and Olympus's ORF, developed with LibRaw.
Home videos
Phone videos and camcorder files, MPEG transport streams included, played from AV1 copies. Their stills are searched like photos, and what is said is transcribed.
Recordings
Audio gets a waveform for its picture, a transcript that follows the player, and its speakers told apart and recognised.
Letters and documents
PDF, Word, OpenDocument and plain text are read and searchable, and so is the text in pictures: signs, prints and handwriting.
Sidecars and tracks
Google Takeout's JSON and XMP sidecars give their dates and places to the files they describe, and GPX tracks join the devices that recorded them.

What the Archive works out by itself

Every file goes through a series of small steps, each a pipeline of its own, and the pages fill in as they run: media-ingest reads the file's metadata, media-faces finds the faces, media-transcribe listens, media-ocr reads, media-geocode finds the street and media-fingerprint finds the copies. The analysis goes to the media service on your own machine.

When

  • Each file is dated from its most trustworthy clock: a date someone typed, a sidecar file, GPS, the camera or the file name. EiN shows which one it used.
  • Time zones come from GPS or the device's own history, so a trip's photos sort into the right moment.
  • Vague dates stay vague: a print can be just "1975".

Where

  • Photos with GPS go on the map, with their street address.
  • A photo taken inside a place you have drawn is linked to it.
  • A GPX file joins the device it names, and its track shows on the albums' maps.

Who

  • Faces are found and grouped into people. Name a group once.
  • A photo shows how old each person in it was that day.
  • Recordings are split by speaker, and voices are recognised across recordings, as faces are.

What

  • Search for "birthday cake with candles" and find every one, with nothing tagged.
  • Text in pictures is read and searchable, handwriting in English, Swedish and Finnish included. Press t to lay it over the picture.
  • Speech is transcribed, and the player follows along in the transcript.

Copies

  • Exact copies, and the same picture saved smaller, re-encoded or cropped, are found and lined up.
  • Bursts taken seconds apart are stacked behind one photo.
  • A new file you already have is shown beside the original, so you keep the best one.
A recording in the viewer: a waveform with the playhead, and beside it the transcript with a timestamp on every line and a speaker label
A granddaughter asks her grandmother about her wedding. Every word is transcribed, and clicking a line plays it.
A handwritten postcard from 1968 in the viewer, with the text EiN read from the handwriting laid over it
A postcard from 1968. The words read from the handwriting can be laid over it, corrected and searched.

Ask about the family

Beside the Archive, the Assistant is at its most useful. It gets the Archive's commands as tools: search, the people and how they are related, the timelines of people, places and devices, and what a picture shows. It knows which picture you have open, and it can draw a family tree or a map under its answer.

  • A plain request for pictures, such as "show photos of Helen from 1998", is answered straight from the search when the search is sure what it asks for, in a fraction of the time.
  • It can set someone's parent or partner, add an event to a life or write a picture's description, each shown as a command that runs only when you press Run.
  • The model runs on your own machine by default. With a hosted provider, the chat tells everyone that their questions, and what it reads in the archive, leave the server.
The chat drawer beside the photo's details, answering who is in the picture: David Okafor, 69, Helen Okafor, 67, and Ruby Wrenfield, 26

The archive in the shell

Every photo, person and album is a path in the tree, so the shell reaches all of them. The Archive's 130 commands sit beside the core's, from person to person-merge, and the terminal shows pictures inline.

The terminal pane, maximized: the archive's top folders, a count of the family's 19 people, Peggy Wrenfield's relations, homes and events, and a photo shown inline

Copy a box of scans in

Uploads, SFTP copies and zips land in your own staging area first, outside the shared library.

$ ssh -p 2222 helen@ein.example
helen@:~$ find /archive/people -maxdepth 1 -type person | wc -l
19
$ scp -P 2222 -r scans/ \
    helen@ein.example:/home/helen/staging/files/inbox/
  • EiN recognises a Google Takeout, a scanner's output or a phone's camera folder.
  • It groups an import into events and trips, and proposes the album each group belongs in. Nothing is filed without you.

The media service does the looking

The Archive's pipelines hand every picture, video and recording to EMS, the media service: every media tool and model EiN uses, in one container behind one HTTP API, reached through a small module of its own. No photo, face or voice leaves your machine to be analysed.

With an NVIDIA card, each model family runs on it when it fits and on the CPU when it does not; without one, everything runs on the CPU. The models stand on the open tools below, all inside the same container.

The models it runs, and their licences
Faces InsightFace buffalo_l Non-commercial research
Speech faster-whisper large-v3 MITSilero VAD MIT
Who speaks when NVIDIA Sortformer CC BY-NC 4.0NVIDIA TitaNet CC BY 4.0
The talking face LR-ASD MIT
Picture search SigLIP2 So400m Apache-2.0
Text search jina-embeddings-v5-text-small CC BY-NC 4.0
Printed text PP-OCRv6 Apache-2.0
Handwriting Multicentury PP-OCRv6, National Archives of Finland Apache-2.0Multicentury TrOCR, National Archives of Finland Apache-2.0, built on Microsoft's TrOCRMicrosoft TrOCR large handwritten None stated; trained on IAM, research only
Copies Meta SSCD MITChromaprint LGPL-2.1

The tools it runs, and their licences

  • FFmpeg GPL-3.0, as builtVideo and audio: reading streams, AV1 copies on the graphics card's encoder or the CPU, stills and waveforms
  • ImageMagick ImageMagick LicensePictures: turning, resizing and the AVIF copies every page shows
  • libheif LGPL-3.0HEIC and AVIF for ImageMagick, encoding with aom (BSD-2-Clause)
  • ExifTool Perl's terms: Artistic or GPLThe metadata in every file: dates, GPS, the camera and its serial number
  • LibRaw LGPL-2.1 or CDDL-1.0Camera RAW files, through rawpy (MIT)
  • Chromaprint LGPL-2.1Fingerprints of sound, to find the same recording twice
  • Pillow MIT-CMUPictures for the models, HEIC included through pillow-heif (GPL-2.0 wheels)
  • OpenCV Apache-2.0Pictures and video frames for the face and talking-face models
  • RapidOCR Apache-2.0Runs the PP-OCR models that find and read printed text
  • PyMuPDF AGPL-3.0Text and pages of PDF files
  • python-docx MITText of Word files
  • odfpy GPL-2.0 or Apache-2.0Text of OpenDocument files
  • Lingua Apache-2.0Which language a text is written in
  • PyTorch BSD-3-ClauseRuns most of the models, on CUDA or the CPU
  • ONNX Runtime MITRuns the face models
  • CTranslate2 MITRuns Whisper, through faster-whisper (MIT)
  • NVIDIA NeMo Apache-2.0Runs the speaker models
  • Hugging Face Transformers Apache-2.0Loads the search and handwriting models, with Sentence Transformers (Apache-2.0)
  • FastAPI MITThe HTTP API EiN talks to, served by uvicorn (BSD-3-Clause)

Shared on your terms

Every node has an owner and permissions, and the same check applies in the browser, the shell, SSH and SFTP. By default the accounts on your server see each other's albums, and nobody without an account sees anything. What someone may not open is not listed anywhere: not in folders, search, the map or the home page.

Sharing in plain words
The Share dialog lists who can view and who can edit. A few photos can stay private inside a shared album.
A link for one album
Open one album to someone without an account, for a day, a month, a year or for good, and revoke it when you like.
Visitors only if you say so
Turn on "Visitors may browse" and people who are not signed in see what you shared with visitors, and nothing else. They can never change anything.
Undo, and a record
What you delete on an album, photo or person page waits 30 days in a trash that can restore it. Changes to the archive, accounts and settings are kept in an audit trail.
Analysis stays at home
No photo, face or voice leaves the machine to be analysed. GPS positions and place names go to OpenStreetMap's address service, which you can swap for your own, and the browser loads map tiles from OpenStreetMap and Esri.

At home on a phone

Below tablet width EiN switches to a phone layout: swipe to open the menu, pinch to show two to five pictures a row, swipe up in the viewer for everything about a picture. Served over HTTPS, it can be added to the home screen and opens full screen, like an app.

EiN's home page on a phone: a feed of recently added albums, each with its first pictures
The Wellington photo of Ruby, Helen and David in the viewer on a phone, with the phone that took it, the album and the people's ages below it
Helen Okafor's page on a phone: her portrait and a timeline of the albums she is in

Getting started

Install

In progress Nothing is released yet and there are no published images, so the installer builds EiN from source on the machine it runs on and starts it with podman compose or docker compose. One command, for EiN Core or EiN Archive:

EiN Core

The bare bones EiN installation: add to it when you want.

$ curl -fsSL https://einhq.eu/install.sh | bash

EiN Archive

The full Archive installation, with the media service and the chat.

$ curl -fsSL https://einhq.eu/install.sh | bash -s -- --ems --llm --module archive --module assistant

Either checks the machine against the system requirements below and stops if something is missing. Then it fetches deploy into ~/ein, builds and starts, and prints the admin password to sign in with. Read it first.

Adding to it later

  1. The media service, which the Archive needs: ~/ein/setup.sh --ems. An image of about 30 GB with every model in it, built once, and the graphics card if there is one.
  2. The assistant's language model: --llm. Or a hosted provider, set in ~/ein/.env.
  3. A module: --module archive, which brings the modules it needs with it.
  4. Updates: ~/ein/setup.sh again fetches the newest sources, rebuilds and restarts, keeping every option chosen before.

To see it work before your own files go in, load the invented family's 48-file sample from test-content.

System requirements

EiN runs in containers on one Linux machine: EiN and its database always, the media service and the language model when you add them. A graphics card is optional: without one every feature still works, more slowly. The recommended machine is the one EiN is tuned and measured on; the minimum is an estimate that has not yet been run end to end.

Part Minimum Recommended
System Linux on x86-64, with rootless podman or Docker, and compose
Processor 4 cores 6 cores or more
Memory 16 GB, with the assistant off or on a hosted model, and smaller memory caps 32 GB
Graphics card None. Transcribing takes about twice the length of the audio. NVIDIA with 16 GB, room for the media models and the assistant side by side. 8 and 12 GB cards work, with less room for the assistant.
Storage About 100 GB free on an SSD for the images and the database with the media service, 10 GB without, plus your files The database on an SSD, your originals on a large disk or a NAS
Browser A current browser that shows AVIF and plays AV1 video: Chrome, Edge or Firefox, or Safari 17 and newer on an Apple device that decodes AV1 (M3, A17 Pro or later). Other Apple devices show a video's poster but cannot play it.

What runs

EiN
The web interface, the shell, SSH and SFTP, sharing, and the pipelines that process every file. Deno, no graphics card, about 0.7 GB in use under load.
PostgreSQL 18
Every node, link, permission, vector, track and log, with TimescaleDB, pgvector and PostGIS. About 150 KB per original file.
Media service Option
Every model and media tool, which the Archive needs. An image of about 30 GB with the models built in, so nothing is downloaded at run time. Runs on an NVIDIA card or the CPU.
Language model Option
Qwen3.5-4B in llama.cpp for the assistant, 5.6 GB on the card or in memory. Or a hosted provider, or none.
  • Ports: 8080 for the web, 2222 for SSH and SFTP. Put an HTTPS proxy in front of EiN, and keep the database and media service behind a firewall.
  • Three of the default models, for faces, text search and speaker turns, are licensed by their makers for non-commercial use only, and the English handwriting model, which the Finnish one builds on, was trained on research-only data. That is fine for a family at home; using EiN commercially means replacing or licensing them.
  • Graphics cards must be NVIDIA, with driver 610 or newer. There is no AMD, Intel or Apple path yet.

Open source

EiN and the libraries it is built on, on GitHub under einhq. The three libraries are on JSR too.

  • coreEiN itself: the node tree, the shell, the pipelines, the web interface, SSH and SFTP. TypeScript on Deno. MIT.
  • module-archiveThe Archive: people and their family tree, photos and videos, places, staging. MIT.
  • module-assistantThe chat that answers questions about the archive. MIT.
  • module-emsEiN's client for the media service. MIT.
  • emsThe media service: every media tool and model behind one HTTP API. Python.
  • deployEiN on one machine with podman compose or docker compose: what the installer fetches and runs. MIT.
  • dbThe PostgreSQL 18 image and its run script.
  • llmThe assistant's local language model: llama.cpp's server and its run script.
  • bash-parserParses bash 5.2 syntax into a syntax tree. @ein/bash-parser on JSR. MIT.
  • bash-executorRuns it: expansions, control flow, builtins, line editing and completion. @ein/bash-executor on JSR. MIT.
  • ssh2An SSH client and server in TypeScript, with no native bindings. @ein/ssh2-ts on JSR. MIT.