Skip to main content
Glama
Jashan-Mshadow

sommus-laptop

README.md
# Sommus

A personal assistant that controls my devices. Named after Somnus, the Roman god of sleep.

**Phase 1:** type a command in the terminal, and Sommus controls my MacBook.
Next up: Bluetooth speakers, then voice, then phone and custom hardware.

What a session looks like (illustrative):

```
you › study mode: close messages, open obsidian, volume 10
  → quit_app(name="Messages")
    ✓ Asked Messages to quit.
  → open_app(name="Obsidian")
    ✓ Opened Obsidian.
  → set_volume(level=10)
    ✓ Volume 10%.
sommus › Study mode is on: Messages closed, Obsidian open, volume at 10.
  4 steps · 9,812 in (7,904 cached) · 212 out · $0.0141
```

## What it can do

**59 tools across three nodes**, plus live web search:

| Area | Tools |
|---|---|
| Sound | volume, mute, play/pause/skip, what's playing |
| Display | brightness, screen off, lock |
| Apps | list open apps, open, quit, focus, any keyboard shortcut |
| Web & files | open a URL, Spotlight search, read a file or folder, append a line, open a file |
| Clipboard | read, write |
| Reminders | create (syncs to iPhone), list |
| Notes vault | search, read, append, add to the to-do list, commit |
| Email | search, read, send, reply, draft over IMAP/SMTP |
| System | battery, Wi-Fi status and toggle, sleep, notifications |
| Browser | list Chrome tabs, read a tab's text, list its links, click a link or button, switch tabs |
| Seeing & typing | **screenshot** (an image the model looks at), click at its coordinates, type any text, any keystroke, wait |
| Documents | read a PDF as text — scanned pages go through macOS Vision OCR |
| Contacts | look up anyone's number or email from the Contacts app |
| Downloads | fetch a file straight to disk, or save a logged-in page with Cmd+S |
| Messaging | send an iMessage (always confirms first), write or send a Gmail |
| Escape hatch | run any of the user's macOS **Shortcuts** — Focus modes, Home devices, anything macOS won't script |
| Knowledge | **web search** for weather, news, prices, anything after the model's cutoff |

It answers questions as readily as it acts, and when there's no exact tool it tries the nearest route
(a Shortcut, a keystroke, opening the right settings pane) before saying it can't.

## Nodes

| Node | Tools | Setup |
|---|---|---|
| **laptop** | 46 — sound, display, apps, browser, screen, shell, PDFs, contacts, files, clipboard, reminders, shortcuts | macOS permissions (below) |
| **vault** | 6 — search, read, list, append, add a to-do, commit the notes repo | none |
| **gmail** | 6 — search, read, send, reply, draft, mark read | app password in `.env` |

A node that isn't set up reports as unreachable; everything else keeps working.

## Sommus on your phone (Telegram)

The brain doesn't change — Telegram is a second interface over the same event stream.

1. Message [@BotFather](https://t.me/botfather) on Telegram, send `/newbot`, pick a name, copy the token.
2. Put it in `.env` as `TELEGRAM_BOT_TOKEN=...` and leave `TELEGRAM_ALLOWED_IDS` empty for now.
3. Run it, message your bot once, and it prints your chat id:

```bash
uv run sommus telegram
```

4. Put that id in `TELEGRAM_ALLOWED_IDS` and restart. Anyone not on that list is ignored and logged —
   without it, whoever finds the bot could drive the laptop.

`/new` starts a fresh conversation, `/cost` reports the day's spend.

## Connecting Gmail

Sommus talks to Gmail over IMAP and SMTP with an **app password**, not the Gmail API.
Gmail's read scopes are "restricted", so a personal OAuth app can't leave Google's Testing
mode without a security assessment — and tokens in Testing expire every 7 days. An app
password never expires and needs no cloud project.

1. Turn on **2-Step Verification** on the Google account (required for app passwords).
2. Go to [myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords), name it
   "Sommus", and copy the 16-character password.
3. Put both in `.env` (gitignored):

```
GMAIL_ADDRESS=you@gmail.com
GMAIL_APP_PASSWORD=abcd efgh ijkl mnop
```

`uv run sommus check` verifies the login. Without it, email still works through the browser
(`compose_email`) and the Gmail tools just explain the setup.

## Architecture

```
  INTERFACES                      BRAIN                         NODES
  ┌──────────────┐          ┌──────────────────────┐   MCP    ┌──────────────────┐
  │ terminal     │─ text ─► │ agent loop           │ ───────► │ laptop (macOS)   │
  │ telegram     │ ◄ events │ Claude API           │ ───────► │ vault (notes)    │
  │ voice (next) │          │ permission tiers     │ ───────► │ gmail            │
  └──────────────┘          │ audit log + cost     │          └──────────────────┘
                            └──────────────────────┘
```

- **Interfaces** only exchange text and events with the brain. Voice will be a new interface, not a rewrite.
- **Nodes** are [MCP](https://modelcontextprotocol.io) servers. Each device lists its tools; the brain routes calls.
  A new device is a new node.
- **The agent loop** is hand-written on the Claude Messages API (`src/sommus/brain/loop.py`): streaming,
  adaptive thinking, prompt caching, refusal fallback, and a step limit.

### Permission tiers

Sommus runs with full permission by default: it acts without asking. Every tool still declares a tier
through MCP annotations, and every call is logged with its tier, so the gate can be switched back on
(`[safety] ask_before_destructive = true`) when riskier tools arrive — email, files, voice.

| Tier | Full permission (default) | `ask_before_destructive = true` | Examples |
|---|---|---|---|
| read | runs | runs | `get_battery`, `list_apps`, `read_file` |
| reversible | runs | runs | `set_volume`, `open_app`, `press_keys`, `create_reminder` |
| destructive | runs | asks y/N first | `quit_app`, `sleep_computer`, `set_wifi` |
| always_ask | **asks every time** | asks every time | `send_message` — it reaches another person |
| blocked | never runs, hidden from the model | same | set per tool in `config.toml` |

A tool with no annotations counts as destructive.

`run_shell` exists after an experiment in going without it: with no shell tool, Sommus typed a command
into Terminal with `type_text` instead — same power, no output, no exit code, no log, and it reported
success for something that never ran. A real tool is the safer of the two. AppleScript still receives
user text through `argv`, never string formatting, and every call is logged.

## Setup

Needs macOS, Python 3.12, and [uv](https://docs.astral.sh/uv/) (`brew install uv`).

```bash
uv sync
cp .env.example .env        # then paste your Anthropic API key into .env
uv run sommus check         # verifies the key, nodes, and macOS permissions
uv run sommus
```

**macOS Accessibility permission** (for `media_control`, `press_keys` and `lock_screen`): System Settings →
Privacy & Security → Accessibility → enable the terminal app you run Sommus from, then restart it.
Reminders, Messages and Spotify/Music prompt separately the first time they're used (Privacy & Security →
Automation). **Screen Recording** is needed for `screenshot`, and reading a Chrome tab's *text* needs
Chrome's View → Developer → Allow JavaScript from Apple Events (titles and URLs work without it).

In the chat: `/tools`, `/cost`, `/new`, `/quit`. Ctrl+C cancels a reply.

Test a tool directly, no AI or API key needed:

```bash
uv run sommus tool                      # list tools
uv run sommus tool set_volume level=20
uv run sommus tool notify title=Hi message="From Sommus"
```

### Use the laptop node from Claude Desktop or Claude Code

The node is a standard MCP server, so it works without the brain:

```bash
claude mcp add sommus-laptop -- "$(pwd)/.venv/bin/python" -m sommus.nodes.laptop.server
```

## Scoring it

`evals/commands.toml` holds the 20 commands Phase 1 has to handle. The runner replays each one in a
fresh conversation and checks which tools were called:

```bash
uv run sommus eval                  # read tools run for real, the rest are simulated
uv run sommus eval --only volume    # just the commands mentioning "volume"
uv run sommus eval --live           # really run every tool (it will sleep the laptop)
```

Currently **46/46** on Sonnet 5 at low effort, ~$0.0095 per command. Target: never below 90%. Each run is saved to `data/evals/` with the tools called, replies, latency and cost,
so model and effort changes can be compared.

## Layout

```
src/sommus/
├── brain/        loop.py · nodes.py · permissions.py · prompt.py · store.py
├── interfaces/   cli.py
├── evals/        runner.py
└── nodes/laptop/ server.py (MCP tools) · macos.py (system) · apps.py (music, Shortcuts, Reminders) · files.py
evals/commands.toml   the 20 commands Phase 1 must handle
tests/                agent loop + permission gate against a fake Claude and a real in-process node
```

## Development

```bash
uv run pytest
uv run ruff check src tests && uv run ruff format src tests
```

Every turn and tool call is logged to `data/sommus.db` (SQLite) with tokens and cost.