Aralo is a free, open-source AI text expander. You type a short abbreviation and it becomes the full text, in any app. Snippets can ask a language model for part of their text, using your own API key or a model on your own machine. There is no Aralo account and no Aralo server.
It is a Rust core behind a thin native shell. macOS comes first.
Pre-alpha: M0/M1 in progress, nothing to install yet.
What exists today is the Rust base: the matching engine, the file format, the library loader, a placeholder parser, the bridge to Swift and a command-line tool that expands snippets in an imaginary text field. The Mac app is a first slice you build yourself: a menu bar agent that expands plain-text snippets and walks you through the permissions on first run. AI is under way (M4): the gateway, the first adapter, the AI settings, commands on selected text, AI blocks inside snippets, AI actions in the snippet editor, search by meaning and the master switch that turns all of it off exist. Expect everything to change, including the file format, which is at version 0.
A text expander has to see what you type. These are the promises, and each one is a property of the build that CI checks, not a policy.
| Promise | How it is held | State |
|---|---|---|
| Keystrokes stay in memory, 64 characters at most, and are zeroed on every reset | A fixed ring buffer in one small crate, aralo-engine. Tests assert the buffer is zero after every reset reason |
Enforced today |
| The code that sees keystrokes cannot write, send or log them | aralo-engine is #![no_std] and may depend on one crate, zeroize. CI inspects its dependency tree and scans its source for print and log macros |
Enforced today |
| Nothing is recorded while paused or in an excluded app | Checked before the buffer is touched. Password managers are excluded by default | Enforced today |
| No telemetry | There is no upload code and no endpoint to send to | True today |
| No Aralo servers | The app will contact only the endpoints you configure, and GitHub for updates | Planned |
| AI sends only the context a snippet declares | Undeclared context is never requested, not merely never sent. The text around an AI block is not sent either | Enforced today |
| Local-only mode means no network | One network guard constructs every HTTP client and refuses non-loopback addresses | Enforced today |
| API keys live only in the system keychain | Never in a file, never in the library folder | Enforced today |
| Model output cannot act | It is inserted as literal text and never parsed for placeholders | Enforced today |
| AI is off until you switch it on, and off means off | No model is asked anything and no model loads, the embedding model included. A test tries every way in with the switch off and watches a socket, the keychain and the model | Enforced today |
| Search by meaning runs on your Mac | The embedding model ships inside the app and is never downloaded at runtime. CI holds aralo-embed to a dependency allow-list with no networking crate in it |
Enforced today |
You can verify "not a keylogger" by reading one crate:
crates/aralo-engine. The checks are in
crates/aralo-engine/tests/privacy.rs
and scripts/check-deps.sh. See
docs/architecture.md for the full
table and SECURITY.md for reporting.
A library is a folder of Markdown files with YAML front matter. One snippet is one file, one group is one folder. Put the folder in iCloud Drive, Dropbox or a Git repository; Aralo does not sync and does not need to.
---
label: Best regards
abbr: ";br"
---
Best regards,The format is specified in docs/format/, with JSON
Schemas in schemas/.
One process on macOS: a menu bar agent that hosts the event tap, the Rust
core and every window. The Swift shell captures keys and injects text. The
Rust core decides everything else. The shell never decides what to expand,
and the core never touches the operating system. Plain data crosses the
bridge, which UniFFI generates: key events in, expansion plans out. The
keystroke path is one synchronous call, on_key, with a 1 ms budget. The
same core runs from the command line on macOS, Linux and Windows, which is
how most of it is tested.
More in docs/architecture.md. Decisions and their reasons are in docs/adr/.
| Crate | Role | Today |
|---|---|---|
aralo-engine |
Buffer, matcher, case rules, undo record. No I/O, no logging | Implemented |
aralo-snippet |
Data model: snippet, group and manifest files | Implemented |
aralo-template |
Placeholder parser, evaluator, ExpansionPlan |
Parser, the editor's outline of a body, the evaluator and plans, AI blocks included |
aralo-library |
Folder store, inheritance, atomic writes, watcher, index, search, conflict-copy merge | M2: load, write, watch, index, search and merge; vectors and hits by meaning |
aralo-core |
The facade the shells talk to | Open, expand, simulate, import, export, search by words and by meaning, edit, the runtime that watches, indexes and embeds, the AI settings, commands, AI blocks and editor actions |
aralo-ffi |
UniFFI bridge to Swift | The keystroke path, editing, search, interchange, change events, the AI settings, commands, AI blocks, editor actions and the embedding model |
aralo-cli |
aralo: init, validate, list, search, type, expand, import, export, ai |
M1, the M2 import and search slices, AI profiles and commands, expand --ai, ai write and search --model |
aralo-ai |
Gateway, network guard, framing, profiles | Gateway, network guard, secret store, Test connection and the capability probe |
aralo-providers |
Provider adapters, SSE parser, local-server detection | openai_compat and local-server detection |
aralo-embed |
Embedding runtime, vector scan | A static model's tokenizer and encoder, held to the reference implementation, and the vector scan |
aralo-import |
Importers, the import report, export to JSON, YAML and CSV | TextExpander, CSV, JSON and YAML in; JSON, YAML and CSV out |
aralo-script |
Script sandbox | Empty, v1 |
aralo-testkit |
Test helpers, a dev-dependency only | A temporary folder the key-leak scan can keep |
Dependencies point one way. aralo-engine, aralo-snippet and
aralo-template are the pure base. aralo-core composes. aralo-ffi and
aralo-cli sit on aralo-core. CI checks the graph.
Rust crates and the CLI build and test on macOS, Linux and Windows. You need Rust stable, 1.88 or later. You do not need a Mac.
cargo test --workspaceTry the core without the app:
cargo run -p aralo-cli -- init /tmp/aralo-library
cargo run -p aralo-cli -- list /tmp/aralo-library
cargo run -p aralo-cli -- search /tmp/aralo-library regards
cargo run -p aralo-cli -- type /tmp/aralo-library "see you ;br "
cargo run -p aralo-cli -- expand /tmp/aralo-library ";br"
cargo run -p aralo-cli -- validate /tmp/aralo-libraryMove snippets in and out of other tools:
cargo run -p aralo-cli -- import fixtures/import/csv/spreadsheet.csv \
/tmp/aralo-library --into Imported --dry-run
cargo run -p aralo-cli -- export /tmp/aralo-library - --format yamlimport reports what each snippet cost and exits 1 if anything needs an edit.
docs/format/import.md has the macro mapping and what
an import may lose.
search matches names, abbreviations, tags and groups fuzzily and bodies
literally, so a macro no importer could convert — left in the body as the text
it was — is one search away:
cargo run -p aralo-cli -- import fixtures/import/textexpander/work.textexpander \
/tmp/aralo-imported
cargo run -p aralo-cli -- search /tmp/aralo-imported '%delay'With the embedding model, it finds snippets by what they mean as well. The model is fetched from Hugging Face once, checked against recorded checksums, and never contacted again. It is a model, so it loads only while AI is on:
make model
cargo run -p aralo-cli -- ai on
cargo run -p aralo-cli -- search fixtures/search/library "money back" \
--model models/potion-base-8MThe Mac app needs macOS 14 or later and:
| Tool | Version | Install |
|---|---|---|
| Rust | stable, 1.88 or later | https://rustup.rs |
| Xcode | 16 or later | App Store |
| xcodegen | any recent | brew install xcodegen |
| SwiftLint | any recent | brew install swiftlint |
make bootstrap # XCFramework and Swift bindings from crates/aralo-ffi, then the Xcode project
make check # rustfmt, clippy, the Rust tests and scripts/check-deps.sh
make test-swift # Swift tests, which call the real Rust core
make run # build Aralo.app (Debug) and launch itmake matrix and make latency measure injection in real apps. They take
over the keyboard, so they are for a Mac set aside for it; see "The injection
matrix" in docs/architecture.md. One of the cases is a
snippet that asks a question: the form panel takes the keyboard without
activating, so the harness answers it with the same keys it typed the
abbreviation with. So far they have been run against TextEdit only.
make help lists every target.
On first launch a four-step window says what Aralo reads and never keeps,
walks through the Accessibility and Input Monitoring grants that macOS
requires before any app may watch and post keys, and ends in a field where
you try a snippet. Control+Option+Command+P pauses from any app. It keeps its library in ~/Aralo and
writes the starter snippets there if the folder is new; the search index and
the rest of what it can rebuild go in ~/Library/Application Support/Aralo.
Set ARALO_LIBRARY to use another folder, ARALO_STATE to put the
rebuildable files somewhere else, and ARALO_COMPAT to try a compatibility
table other than the built-in data/compat/apps.toml. Then type ty and a
space in any text field. A local
build is ad-hoc signed, so macOS forgets the permission after a rebuild. To
keep it, sign with a certificate from your keychain:
make run SIGN_IDENTITY="Your Certificate".
apps/macos/Generated/ and the .xcodeproj are build output. They are not
committed; make bootstrap rebuilds them.
aralo/
Cargo.toml workspace
Makefile bootstrap, check and the Swift targets
deny.toml cargo-deny: licences, advisories, bans, sources
crates/ the Rust core; see the table above
apps/
macos/ the Mac app, AraloKit (tap, injector) and
AraloHarness (injection matrix, latency)
data/
starter/ the snippets a new library starts with
compat/apps.toml how text goes into each app, by bundle ID
compat/matrix.json how the injection matrix reaches a text field in each
schemas/ JSON Schemas for the file format and the data tables
scripts/
check-deps.sh engine purity, crate layering, no unsafe in the bridge,
no network under the embedding runtime
build-xcframework.sh
fetch-model.sh the embedding model, checked against its checksums
conformance-hosted.sh the nightly conformance run against hosted endpoints
docs/
architecture.md
format/ the file format specification
adr/ architecture decision records
conformance/ what `aralo conformance` runs against, its evaluation
set, and the reports behind docs/compatibility.md
fixtures/
matrix/library/ the snippets the injection matrix types
import/ the import corpus and its golden reports
golden/ golden expansions, three locales on a stopped clock
embed/ a tiny model and what the reference implementation
makes of it, for the tokenizer and encoder
search/library/ a library to find things in by what they mean
models/ the embedding model, fetched by `make model`; not in Git
Weeks are from the plan and overlap on purpose.
| Milestone | Weeks | Exit criteria |
|---|---|---|
| M0 Foundations and spikes | 1–2 | A hard-coded snippet expands in TextEdit. Each spike has a written ADR |
| M1 Engine and injection | 2–5 | 13 of 15 matrix apps pass. on_key p99 under 1 ms. Typed-to-inserted p95 under 50 ms |
| M2 Library, editor, search, import | 4–8 | Import a TextExpander library, edit it and expand from it. Import fidelity 90% or better on the corpus |
| M3 Dynamic content and forms | 7–9 | Golden suite green. Forms work in every matrix app. Import fidelity 95% |
| M4 AI | 8–12 | Six endpoints pass conformance. Under 30 ms added to first token. The AI switch and local-only mode verified by tests |
| M5 Sync, hardening and beta | 11–13 | Notarised DMG and Homebrew cask live. Every security and privacy test green. Beta opens |
M2's import and search halves are in, and M3 has since raised the bar they are
held to. Import is in the Mac app as well as the CLI: the snippet window, or
Import Snippets… in the menu bar, opens a file on a dry run, says what it would
become before anything is written, and after the import opens each snippet that
needs an edit from the report. Export writes the selected group, or the whole
library, as JSON, YAML or CSV. The corpus in fixtures/import/ imports
at 95.4% clean, and crates/aralo-import/tests/fidelity.rs prints the number
and fails under the 95% M3 asks for on every run. The last two points came from
giving the format what the sources already said: a fill-in area lands in a box
with room to write in, and %key:tab% presses a real Tab
(ADR-0015). That corpus is
written from the documented formats rather than exported from a real
installation, which its README says at more length; spike S7 is where a real
export settles it.
M2's folder store is in too: a debounced notify watcher that tells Aralo's
own saves from everyone else's by content hash, and a SQLite index with FTS5
that syncs incrementally. crates/aralo-library/tests/index.rs asserts that an
incremental sync lands exactly where a rebuild lands, and that ten thousand
snippets index on a background thread while an abbreviation expands on the
main one. The conflict copies a sync client leaves are recognised by each
client's naming and merged three ways against the version this machine last
saw. A clean merge is written and the copy goes to the Trash. A clash leaves
both files and waits for the user: the snippet window shows the two side by
side, says what they changed differently, and keeps the original, the copy or
a version the user writes
(docs/architecture.md).
M3's evaluator is in, and with it the sessions that ask before anything is
inserted. A body writes the date and time in 15 locales from the format's own
tables, takes the clipboard, puts a form in front of the user, inlines other
snippets eight deep, presses a Tab or a Return the app acts on rather than a
character that looks like one, asks for a box with room to write in, and leaves
the caret where it says
(ADR-0014,
docs/format/placeholders.md). What the editor
previews is the expansion, byte for byte. The proof is
fixtures/golden/: every body expanded in
en_US, de_DE and ja_JP on a clock stopped at one minute, compared byte
for byte.
The Mac form panel is in, so a snippet that asks something now asks it: the
abbreviation is swallowed, nothing is typed, and a non-activating panel puts
the boxes in front of the user with a live preview of what Enter will insert.
It is the same panel however the snippet was asked for, typed or picked from
the palette. The clipboard is read when the body says it wants it and not
before, cancelling puts the swallowed keystroke back, and what is inserted goes
in through the same injector and arms the same undo as any other expansion.
FormSession in AraloKit is the panel apart from its window, so a second shell
writes the window and nothing else. See
the form panel.
Both the store and the index now run under aralo_core::Runtime, which is what a shell holds for the
lifetime of the app: edit a file in any text editor and the next keystroke
matches it, without the app asking. The editing calls are on the bridge
alongside them — create, save, move and delete a snippet or a group, with the
draft checks an editor shows before it saves.
The window that uses them opens from the menu bar: a group tree, the snippets
in the selected group with a search box over them, and an editor with the
snippet's settings shown against what its groups make of them, plus a live
preview. Every change it makes lands as a readable file diff. What it shows is
LibraryStore in AraloKit, which is the model a second shell would keep; the
SwiftUI views above it hold nothing a Windows one would have to write again.
The body editor draws the core's own reading of the snippet: every placeholder
is coloured, anything the parser could not read is underlined at its range with
the core's sentence under it, and an Insert menu offers the placeholders the
core knows and pre-selects the part to type over. It is one call,
outline_draft(body), and it is the same parse an expansion runs, so what the
editor marks is what would happen. Under the preview is a test field: type
the abbreviation and the draft on screen expands, through the same matcher and
expansion path as any app, before anything is saved. A form opens as a sheet,
Backspace takes an expansion back, and the live tap stands aside while the
field has the keyboard.
The inline search palette is in: ⌃⌥⌘Space anywhere, a few letters, and Enter puts the snippet into the app the user was already in. The window is a panel that does not take that app out of the front, so there is no focus to restore; it stops Aralo watching the keyboard while it is up, and gives the keyboard back before a character is typed. The text goes in through the same injector and arms the same undo as an expansion that was typed, because to the user it is the same thing arriving by another route. It is refused while Aralo is paused and in the apps it stays out of, and the palette is the one place a user is told why. See the search palette, the snippet window, the folder store and the bridge API.
M4 has begun with the gateway every AI feature will go through (task 4.1). It
checks the AI switch, local-only mode and a managed allow-list, asks the shell
only for the context a snippet declared, frames that context as data, and
meters tokens per feature and per profile. It is also the only code that can
reach the network. In local-only mode it refuses any URL that is not this
Mac before sending, and any address that is not loopback when connecting.
crates/aralo-ai/tests/local_only.rs proves both against a real listener.
The first adapter is in (task 4.2). openai_compat streams chat completions
from OpenAI and every API that copies it: OpenRouter, Groq, Together, LiteLLM
and the local servers. It reads the stream incrementally and meters usage
once, at the end. A cut or garbled stream is an error, never a short answer.
Dropping the stream closes the socket, and a test against a real listener
proves it. detect_local_servers finds Ollama, LM Studio, llama.cpp and vLLM
on their default ports. The adapter's tests replay transcripts from
fixtures/sse/. One is recorded from a live endpoint; the rest are
hand-written from each provider's documented format, and
crates/aralo-providers/tests/live.rs can record replacements. A native
Anthropic adapter (task 4.3) is deferred: Claude models are reachable through
OpenAI-compatible endpoints such as OpenRouter. See the AI gateway and
provider adapters.
Profiles and keys are in (task 4.4). A profile is a provider's address, a
model and optional headers, saved in profiles.toml in Aralo's state folder,
outside the library. Its key is kept in the login keychain, and the file
holds only a reference to it. The Mac app has a Settings window (⌘,) with an
AI tab. It has the switch and local-only mode, presets for ten providers with
a link to each one's key page, Test Connection, a model list, local-server
detection and a probe that shows what an endpoint can do. The terminal has
the same through aralo ai, which reads a key only from the environment or
standard input. scripts/key-leak-scan.sh runs every test and then looks
for a key in every file the run wrote. See
profiles and keys.
Commands on selected text are in (task 4.5). Select text in any app and press
⌃⌥⌘A, or pick Transform Selection… from the menu. Aralo reads the selection
through Accessibility, or copies it when the app does not say, and puts your
clipboard back. Then pick a command, such as "Fix spelling and grammar" or
"Make it shorter". The answer streams in and is shown against the selection
word by word. Enter replaces the selection with one paste, so one ⌘Z in the app
brings the original back. E edits the answer first and R asks again. The panel
names what was sent and to which profile and model. A command is a snippet with
type: command whose body is the instruction. Seven are built in, and a
library can add its own or replace them. aralo ai command list and
aralo ai command run do the same from the terminal, with the selection on
standard input. See
commands on selected text.
AI blocks inside snippets are in (task 4.6). A body asks a model for part of
its text with {{ai: prompt | fallback: text}}, and the snippet's ai.context
says what the model may see: the form's answers, the selection, the clipboard,
the app or its window. Nothing else is sent, not even the snippet's own text
around the block. The form panel shows the answer streaming into the
preview, marked apart from the snippet's own text: Enter inserts, R asks
again, E edits and Escape cancels. The model's words go in as literal text,
so a model that writes {{clipboard}} puts in those characters and reads
nothing. The text around a block is byte-identical whatever the model wrote;
a property test and fixtures/golden/ai.toml hold the evaluator to it. With
AI off, local-only mode refusing the host, or no network, the block puts in
its fallback and the panel says why. aralo expand --ai does the same from
the terminal, and without --ai a block puts in its fallback and says how to
ask. See AI blocks in snippets.
The snippet editor has AI actions (task 4.7). An AI menu beside Insert can
draft a body from the snippet's label, fix spelling and grammar, make the text
clearer or shorter, change its tone, translate it or suggest variations. It
works on the selection, or on the whole body when nothing is selected, and
that is all it sends; a draft sends only the label and a note. The answer
streams into a sheet with a word diff, and the sheet names any placeholder the
answer dropped or added, because one a model writes is one the snippet will
expand. Replace puts it in as one edit, so one ⌘Z in the editor takes it back.
aralo ai write proofread < body.txt does the same from a terminal. See
AI actions in the editor.
Search by meaning is in (task 4.8). "money back" finds the refund reply that
never uses either word. It runs on MinishLab's potion-base-8M, a static
embedding model that ships inside the app: a snippet embeds in about 25 µs,
the library is embedded in the background and kept in the index, and nothing
leaves the machine. Snippets found by meaning come after the ones your words
found, marked as such. make model fetches the model, checked against
recorded checksums; with AI on, aralo search --model models/potion-base-8M
uses it from the terminal. See search by meaning
and ADR-0016.
The AI master switch governs all of it (task 4.10). AI is off until you
switch it on in Settings or with aralo ai on, and while it is off no model is
asked anything and no model loads: the embedding model search by meaning uses
is dropped the moment AI goes off and loaded again when it comes back on.
Local-only mode keeps requests on this Mac and leaves the embedding model
alone, since it never leaves the machine. Under every answer, the panels say
what was sent and to whom; a click lists each kind the request could carry,
with the bytes that went or "nothing to send". See
the master switch.
The conformance suite is in (task 4.9). aralo conformance --profile <name>
runs protocol checks against a profile's endpoint (the models route,
streaming, the system prompt, cancel, and the errors for an unknown model and
a wrong key), each named in a JSON report that holds the host and never a key.
--evaluation adds the editor's actions on fixed texts, scored.
aralo conformance table turns reports into
docs/compatibility.md. Every push replays each
recorded provider stream through a mock and runs the suite against a real
Ollama and llama.cpp server; a nightly workflow runs the hosted providers
whose keys are repository secrets. Against the mock, Aralo adds about a
millisecond to the first token. The first table has two live endpoints, Ollama
and llama.cpp's server, both running Qwen2.5 0.5B in CI: both conform, and the
evaluation shows how little of the editor's work a model that small gets
right. The six hosted providers in conformance/endpoints.toml are listed as
not run live until their keys are repository secrets. See
the conformance suite.
M5 has begun with sync (task 5.1). A library in a Dropbox, iCloud Drive, Google Drive, OneDrive or Syncthing folder already worked, conflict copies included. Now a save no longer writes over what another Mac changed while the snippet was open here: the editor merges the file on disk into what you typed, so a rename there and a new line here both land. When both changed the same thing, nothing is written, and a sheet shows the two versions side by side until you keep yours, take the file on disk, or fold one into the other first. See a file that changes while it is open. A conflict copy is also merged against a better base now. Aralo used to take its own last save as the version both Macs shared, so the other Mac's copy looked like it had undone that save and won without a word; a save made here no longer becomes the base until a newer version arrives from elsewhere.
Six spikes are still owed in M0; S5, the embedding runtime, is answered by ADR-0016. They are listed in docs/adr/README.md.
| Document | What it is |
|---|---|
| docs/format/ | The library and snippet file format, placeholders, matching rules |
| docs/architecture.md | Process model, threads, the keystroke path, the bridge, privacy invariants |
| docs/data-flow.md | What Aralo reads, where it keeps it, and every host it can contact |
| docs/threat-model.md | Assets, adversaries, trust boundaries, and the test behind each defence |
| docs/compatibility.md | Which AI endpoints pass the conformance suite, check by check |
| docs/adr/ | Decision records |
| CONTRIBUTING.md | How to build, the rules CI enforces, sign-off |
| SECURITY.md | How to report a vulnerability |
| CODE_OF_CONDUCT.md | Community standards |
The documents in this repository are authoritative. Two longer planning documents sit outside it. They are private-by-default pages and may ask you for access:
IDs such as P1, E6 or D4 in the docs and the code refer to requirements in the PRD. Where those pages and this repository disagree, the repository wins.
Read CONTRIBUTING.md first. Most contributions need only
cargo test --workspace. Changes to aralo-engine, the event tap or the
network guard need code-owner review.
The project lives at https://github.com/anandghegde/aralo.
Apache-2.0. See NOTICE.