Tomo is a macOS app for chatting with cloud or local language models and bringing whatever is on your screen into the conversation.
Learn more and download at wvlen.llc/apps/tomo.
Warning
Tomo is an early-stage, independently maintained project built primarily for the author's workflow. It has not received a third-party security audit, provider compatibility can change without notice, and automated coverage does not yet exercise every network, permission, and UI path. Treat releases as beta software and review the code and configuration before using Tomo with sensitive or critical material.
- OpenAI-compatible providers - OpenAI, Anthropic, Google Gemini, OpenRouter, Ollama, and custom endpoints.
- Screen and text capture - Add a screenshot, full-screen capture, or selected text from another app.
- Personas - Save reusable system prompts and switch between them without leaving the conversation.
- Quick selector - Search providers, models, and personas from one switcher.
- Global hotkeys - Show or hide Tomo and capture content from the app you are using.
- Companion CLI - Run model inference, submit text and images to the app, and automate Tomo's providers, models, personas, chat, settings, capture, window, server, update, and hotkey features.
- Images, streaming, and Markdown - Attach images and render streamed responses with highlighted code blocks.
- Local key storage - API keys are stored in the macOS Keychain.
- macOS 14.0 or later
- Xcode 16 or later for source builds and tests
- An API key for a configured cloud provider, or a local OpenAI-compatible server such as Ollama
- Launch Tomo and open Settings > Providers.
- Add a provider, API key, and model. For Ollama, make sure its OpenAI-compatible endpoint is running locally.
- Select the provider and model, then start a conversation.
- Grant Screen Recording or Accessibility access only if you use the corresponding capture feature.
Choose Tomo > Install Tomo CLI... in the app menu, or choose the same item from Tomo's menu bar icon. Confirm ~/.local/bin/tomo in the save panel, then make sure ~/.local/bin is in your shell's PATH.
# Stream a response from the selected model
tomo "Explain why this code is failing"
# Stream thinking summaries to stderr and the answer to stdout
tomo --thinking "Review this approach" > answer.md
# Pipe text into the prompt, following llm-style command conventions
git diff | tomo -s "Review this change" --no-stream
# Send one or more images to a configured model
tomo "Describe this screenshot" -a screenshot.png -m openai/gpt-4o
# Open Tomo, start a new chat, and submit the same text and images through the UI
tomo ui "Compare these" -a before.png -a after.png --newThe default command is headless inference, similar to the prompt interface in Simon Willison's llm. Prompt text can come from arguments, standard input, or both. Each headless invocation is independent; use tomo ui for the persistent in-app conversation. Named headless conversations and --continue are not currently supported. The CLI supports -m/--model, -p/--persona, --no-persona, -s/--system, repeatable -a/--attachment, explicit attachment MIME types with --at PATH MIME, repeatable provider options with -o/--option, streaming by default, --no-stream, and --json. Use tomo ui or --ui to add the request to Tomo's visible conversation instead of returning the response only in the terminal.
Model IDs use <provider-route>/<model>, as shown by tomo models. A bare model name also works when it is unique across configured providers. -m and -p are one-request overrides; they do not change the selected model or persona. Provider options are decoded as JSON values when possible, so -o temperature 0.2, -o seed 7, and -o response_format '{"type":"json_object"}' preserve their types.
Use --thinking to include provider-supplied thinking summaries in headless output. They stream to stderr under a Thinking: label while stdout contains the answer. --thinking --no-stream waits and prints the collected summary before the answer. --thinking --json retains the single final JSON object, with summaries in inference.thinking and answers in inference.text; it does not print thinking to stderr. Unsupported models or responses without summaries return just the answer. The app's collapsed Thinking disclosure uses the same provider adapters. See model effort and thinking support for provider details and supported options.
The rest of Tomo's automatable surface is available as subcommands:
# Providers and models
tomo providers
printf '%s' "$OPENAI_API_KEY" | tomo providers add \
--name OpenAI --base-url https://api.openai.com/v1 \
--api-key-stdin --model gpt-4o-mini --selected-model gpt-4o-mini --select
tomo providers update openai --model gpt-4o-mini --model gpt-4.1
tomo providers remove openai --force
tomo models --query vision
tomo models add llama3.2 --provider ollama
tomo models select openai/gpt-4o-mini
tomo models remove llama3.2 --provider ollama --force
# Personas
tomo personas
tomo personas create Reviewer --prompt "Review for correctness and risk."
tomo personas update Reviewer --prompt-file reviewer.txt
tomo personas show Reviewer
tomo personas select Reviewer
tomo personas clear
tomo personas remove Reviewer --force
# Current in-app conversation
tomo chat status
tomo chat last --role assistant
tomo chat search "rate limit"
tomo chat regenerate [MESSAGE_ID]
tomo chat delete MESSAGE_ID --force
tomo chat delete-last --force
tomo chat export ./exports --force
tomo chat clear --force
# App controls and settings
tomo status
tomo version
tomo show
tomo hide
tomo toggle
tomo capture text
tomo capture window
tomo capture area
tomo capture screen
tomo window move left
tomo settings Providers
tomo about
tomo updates check
tomo config list
tomo config get highlight-theme
tomo config set window-transparency false
tomo config reset window-transparency
tomo hotkeys list
tomo hotkeys set toggle-app control+option+space
# Optional OpenAI-compatible HTTP endpoint
tomo server status
tomo server start --port 8787
tomo server models
tomo server stopRun tomo help or tomo help <command> for the complete grammar. Destructive commands require --force; exports require it only when the destination already exists. Provider listings report whether a key exists but never return the key. Prefer --api-key-stdin over --api-key so secrets do not appear in shell history or process listings.
The CLI is bundled at Tomo.app/Contents/Helpers/tomo and launches Tomo automatically. Headless inference still runs inside the Tomo process, where the selected provider, model, persona, and Keychain credentials are resolved; the CLI never reads application storage or API keys directly. Communication uses a streaming, length-prefixed typed JSON protocol over a Unix socket in Tomo's team-scoped App Group container. The socket is readable and writable only by the current user (0600). tomo server separately controls Tomo's optional OpenAI-compatible loopback HTTP endpoint.
Results go to standard output and diagnostics go to standard error. Exit codes are stable: 0 for success, 1 for an operational failure, and 2 for invalid usage. For automation, prefer --json; Tomo owns all mutable application state and the CLI acts only through the local companion service.
Open Tomo.xcodeproj in Xcode and build the Tomo scheme.
To build and run the unit tests without code signing:
xcodebuild -project Tomo.xcodeproj -scheme TomoTests \
-destination 'platform=macOS' CODE_SIGNING_ALLOWED=NO testTo sign a build, provide your Apple Developer Team ID via a local, gitignored config file:
cp Config/Local.xcconfig.example Config/Local.xcconfig
# then edit Config/Local.xcconfig and set DEVELOPMENT_TEAM to your team IDForks should also replace TOMO_BUNDLE_IDENTIFIER in that file with a unique identifier they control.
- API keys are stored in the macOS Keychain, not in
UserDefaultsor the repository. - Messages, images, screenshots, captured text, and persona prompts are sent to the selected model endpoint. A cloud provider may retain or process that data under its own terms.
- A local endpoint such as Ollama can keep model traffic on your Mac, provided the endpoint itself is local and configured that way.
- Screen Recording and Accessibility access are optional macOS permissions used only by their capture features.
- Tomo has no app analytics. Sparkle checks
tomo.wvlen.llcfor software updates, and normal chat traffic goes to the endpoint you configure. - Tomo uses App Sandbox, Hardened Runtime, code signing, and a team-scoped App Group shared with its bundled CLI.
- The optional OpenAI-compatible server listens only on loopback, but any process on the Mac can call it and use Tomo's configured provider credentials while it is enabled. Leave it disabled when it is not needed.
- Custom endpoints are trusted configuration. Confirm their URL and data-handling policy before sending private content.
- Provider request logging is off by default. Setting
TOMO_DEBUG_API_LOGGING=1in a Debug build may write prompt or response content to logs.
- Tomo targets the OpenAI chat-completions shape. Provider-specific features outside that compatibility layer may not work.
- The Effort picker above the composer remembers a separate setting per provider/model. Supported levels vary by model; Default uses the provider's existing behavior. See model effort support and API research.
- OpenRouter effort levels are discovered from its model catalog and cached locally. Available thinking traces stream into the app's collapsed Thinking disclosure, or into headless CLI output with
--thinking. - All compatible endpoints can display thinking returned in
reasoning_content,reasoning, or typedreasoning_detailsfields. Enabling thinking and discovering accepted effort levels still depend on provider capabilities; see the provider generality review. - Anthropic uses its OpenAI SDK compatibility layer by default. Selecting effort uses native Messages requests with adaptive thinking on supported Claude models. Effort choices and adaptive-thinking support come from cached Models API metadata, with documented fallback when unavailable. This adapter supports text and images; it does not add native tool use or thinking-history persistence.
- Preconfigured model lists are starter defaults, not a live catalog. Providers rename and retire models, so you may need to add a current model manually.
- Conversations are not a durable record and should not be your only copy of important output.
- Screen and text capture depend on macOS permissions and on the source application's accessibility support.
- Unit tests cover core parsing, conversion, search, provider, and keychain-keying logic. UI tests currently provide launch-level coverage rather than full workflow coverage.
Tomo/- application sourceTomoCLI/- shared companion protocol, local socket transport, and command-line entry pointTomoTests/- unit testsTomoUITests/- launch and UI test targetConfig/- shared and developer-local signing configurationrelease-notes/- canonical Markdown release notesscripts/- release packaging, appcast, and license helpers
See CONTRIBUTING.md for development and pull-request guidance and SECURITY.md for reporting vulnerabilities privately.
Release notes are tracked in release-notes/<version>.md, and app version
metadata is defined once in Config/Version.xcconfig. Configure the
Tomo-specific notarization profile once:
scripts/publish-release.sh \
--setup-notary-profile \
--apple-id you@example.comFor each release, update the version and notes and commit them. Then prepare and validate the release locally without publishing:
scripts/publish-release.sh \
--version 0.2.12 \
--build 14 \
--notes release-notes/0.2.12.mdAfter reviewing the generated artifacts, push the release commit and run the
same command with --publish. A matching artifact manifest makes the
publishing run reuse the exact reviewed bytes. The release script tests, signs,
notarizes, packages, and verifies the app before publishing the payloads and
finally the Sparkle appcast. Each successful stable publish also updates the stable
download URL https://tomo.wvlen.llc/Tomo.dmg from the same verified DMG bytes;
public download buttons should use that URL instead of a versioned filename.
Beta releases use a Sparkle channel in the same feed, are marked as GitHub
prereleases, and preserve the stable download URL. Settings > Updates >
Include beta updates is available starting in 0.2.13 and controls opt-in;
manually installing a beta opts in
unless you previously opted out. See beta releases
for versioning, initial installation, and release commands.
Regenerate committed third-party acknowledgements with
scripts/fetch-licenses.sh after changing dependencies. A Sparkle upgrade also
requires reviewing its release tool and updating the pinned checksum in
scripts/update-appcast.sh.
Released under the MIT License.