Services
Every capability is a small daemon behind a Unix socket. Call an action by name; the executor routes it, retries it and logs it.
Wire protocol
Newline-delimited JSON, many requests per connection. Requests are capped at 1 MiB. There is no TCP anywhere. Every action must be idempotent, because a call can be retried after a timeout and the first attempt may already have taken effect.
// one JSON object per line, over a Unix socket
-> {"op":"describe"}
<- {"ok":{"protocol":1,"name":"indexd","actions":["index.search", ...]}}
-> {"id":"c1","action":"index.names.find","input":{"query":"tax return","limit":5}}
<- {"ok":{"exact":[...],"hits":[...]}}
<- {"error":{"kind":"transient|unavailable|permanent","message":"..."}} Error kinds drive retry: transient retries with backoff, unavailable moves to the next runtime, permanent stops. A service that is not running yet is not a load failure. Calls report unavailable until it appears.
actiond
Turns what you say or type into one intent and runs the action that serves it. Open and close are built in. Any app can add intents in its manifest.
| Action | Input | Result |
|---|---|---|
action.plan | {text} | {intent, action, by}. Plans without running. |
action.run | {text} | Runs it. A capture intent answers {status:"capture", token, sink, view, ttl}. |
action.invoke | {action, args} | Forwards to a service. With a live capture token it fills in the session. |
action.confirm | {token} | Confirms a pending action, such as closing an app. |
action.cancel | {token, reason: save|discard} | Ends a capture. Save calls the app's end action, discard calls its discard action. |
action.list | - | The registered intents. |
- Captures last 120 seconds, refreshed on each append. One per app, at most 8 at once. Appends are stamped client_id = <token>-<n>, so a retried append is ignored.
- Understood intents with no server answer honestly: status "unsupported".
inferd
The only door to the model. llama.cpp runs behind it on private Unix sockets. With no config it answers with a stub, so everything works before models exist.
| Action | Input | Result |
|---|---|---|
inference.status | {} | Engines and models, and whether they are ready. |
inference.embed | {texts[], model?} | {model, dim, vectors}. The indexer chooses the embedding model, not the caller. |
inference.complete | {prompt, system?, max_tokens?, temperature?, model?, raw?} | {model, text}. By default the prompt is a user message through the model's chat template. |
- Requests name models from the config file, never paths or command-line arguments.
voiced
Speech to text. Live dictation, plus one-shot transcription of a WAV.
| Action | Input | Result |
|---|---|---|
voice.status | {} | Engines, models, whether live dictation is available, and whether a session is active. |
voice.transcribe | {path, language?, keep?} | {engine, model, text}. The WAV must be in the private voice folder. |
voice.dictate.start | {client_id, mode: command|dictation, language?, endpoint_ms?, max_seconds?} | {session, sample_rate: 16000, poll_hint_ms}. One session at a time. |
voice.dictate.poll | {session, since?} | {seq, state, partial, segments[], level[12], text}. Never waits. |
voice.dictate.stop | {session, discard?} | Flushes the tail, closes the microphone, returns the final text. |
listend
Wake word. Your phrase, checked against the model, applied at once.
| Action | Input | Result |
|---|---|---|
listen.status | {} | Engine, whether the microphone is open, keyword, hit count, last error. |
listen.start / listen.stop | {remember?} | Open or close the microphone. remember keeps it across logins. |
listen.set_keyword | {keyword} | One to four plain English words. |
listen.set_sensitivity | {threshold, boost} | Lower threshold hears more and mistakes more. |
listen.set_aliases | {aliases[]} | Extra spellings that wake the same word. |
listen.enroll | {clips[], negatives[], apply?, max_aliases?} | Chooses spellings from your recordings. No training. |
settingsd
One registry of settings and the phrases that change them. The Settings window and the launcher are both clients.
| Action | Input | Result |
|---|---|---|
settings.list | - | {pages:[{id, title, items[]}]} |
settings.get | {id} | One item. |
settings.set | {id, value} | The item after the change. The value is validated first. |
settings.invoke | {id} | Runs an action item. |
settings.match | {text} | Ranked matches for a sentence. Runs nothing. |
- Item kinds: toggle, number, text, choice, action, info. Add a setting and the window renders it without a shell change.
indexd
Three indexes in one service: file contents, file names, and the encrypted activity log. Vector and keyword search fused together.
| Action | Input | Result |
|---|---|---|
index.status | {} | Per-index counts, embedder, whether vectors are pending. |
index.search | {query, scope: files|activity|all, limit} | Ranked hits {scope, uri, ord, text, score, via}. Scope defaults to files, limit to 10. |
index.files.index_file / index_dir / remove | {path} | Keep the content and names indexes in sync. |
index.names.scan | {dir?, full?} | {scanned, added, updated, unchanged, removed, vectors_pending} |
index.names.find | {query, limit ≤ 50} | {exact[], hits[]}. Exact name matches are listed separately. |
index.names.remove | {path} | {removed} |
index.activity.index | {} | Indexes the encrypted log into its own sealed database. |
index.rebuild | {scope: files|names|activity} | Empties an index. Names refill in the background. |
- A scan of 20,000 files took 3.9 s the first time and 0.11 s unchanged, with the lexical embedder. A real model's speed is not measured yet.
Command-line tools
purpose-pkg | Installer for .papp bundles: pack, install, verify, list, uninstall |
purpose-launch | Runs an app or service under Landlock |
purpose-exec | Calls any action: purpose-exec call voice.status |
purpose-index | Local index in one SQLite file: sqlite-vec for vectors, FTS5 for keywords |
purpose-activity | Keygen and reader for the encrypted activity log |
purpose-webhost | Hosts web UIs at purpose-app://<id>/ with the JS bridge |
purpose-models | Fetches and verifies the on-device models |
purpose-wake | Sets your wake word and sensitivity |
purpose-exec call voice.status
purpose-exec call index.names.find '{"query":"tax return","limit":5}' The sandbox these services run in is described on the privacy page.