Metadata-Version: 2.4
Name: aegis-focus
Version: 1.1.0
Summary: macOS focus tool: LLM-judged page blocking during study sessions
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/BitPeppr/Aegis
Project-URL: Source, https://github.com/BitPeppr/Aegis
Keywords: focus,macos,chrome-extension,llm
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Desktop Environment
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1,>=0.27
Requires-Dist: rumps>=0.4
Dynamic: license-file

# Aegis

A macOS background focus tool. Start a focus session with a topic and duration;
the Chrome extension reports pages you visit to a local daemon, which uses an
LLM (any OpenAI-compatible endpoint) to judge whether the page supports your
study topic. Off-task pages are redirected to a local "keep focus" page.

- **No browsing latency.** Pages load normally; the check happens in the
  background and a caught distraction is redirected within ~1s of being
  judged.
- **Locked sessions.** While a session is active it cannot be stopped, reset,
  or quit through the app — the daemon runs as a `launchd` KeepAlive agent and
  session state persists atomically across restarts (crash/restart/corruption
  cannot silently void the lock). Be honest with yourself: this is a
  self-imposed commitment aid, not tamper-proof confinement — anyone who owns
  the machine can still unload the agent, disable the extension, use another
  browser, or edit the session file. It guarantees the lock is not lost
  *accidentally*; it does not prevent leaving *intentionally*.

## Architecture

```
Chrome (extension: background.js)
  │  tabs.onUpdated / onActivated
  ▼  POST /evaluate  (async, non-blocking)
macOS daemon (aegis/)  ──►  SQLite cache ──► LLM (Groq/NIM/Zen/…)
  │  whitelist  →  cache  →  LLM verdict
  ▼  {allowed: false, confidence >= 0.8}
redirect tab ──►  http://127.0.0.1:8123/blocked  ("Aegis" page)
```

## Install

Aegis is a Python package installed with pipx; the daemon runs as a launchd
LaunchAgent managed by the `aegis` CLI.

```bash
brew install pipx
pipx install aegis-focus         # from PyPI
aegis install                    # render the LaunchAgent plist and start the daemon
```

The package is published on PyPI as **`aegis-focus`** (the plain `aegis` name
was already taken by another project); the installed commands are still
`aegis` and `aegis-daemon`.

The plist points at the pipx venv's python, so the daemon always runs exactly
the code the CLI manages. After a new release, upgrade and reload:

```bash
pipx upgrade aegis-focus
aegis restart                    # refuses while a focus session is active (--force overrides)
```

To install from this repository instead (development builds):

```bash
pipx install .                   # from the repo (or pipx install /path/to/aegis)
```

The Chrome extension is **not** part of the Python package: load it from the
`extension/` folder of the [repository](https://github.com/BitPeppr/Aegis)
(that folder is also bundled in the PyPI source download).

### CLI reference

```
aegis install     install/repair the daemon agent (re-renders the plist, reloads)
aegis restart     reload the agent; refuses during an active session unless --force
aegis status      agent loaded? daemon healthy? session state?
aegis cleanup     remove logs and the verdict cache (keeps config and the session lock)
aegis uninstall   stop the daemon, remove the agent + logs + ALL state (confirms unless -y)
```

There is deliberately **no `aegis stop`**: the daemon is a KeepAlive agent —
you cannot stop it without removing it. `aegis uninstall` is the removal path.
A deliberate user can always escape (see "Locked sessions" above); the
commands are honest about it.

### Configure the LLM

Create `~/Library/Application Support/Aegis/config.json`:

```json
{
  "LLM_BASE_URL": "https://api.groq.com/openai/v1",
  "LLM_API_KEY": "gsk_...",
  "LLM_MODEL": "llama-3.3-70b-versatile",
  "LLM_TIMEOUT": "60",
  "BLOCK_CONFIDENCE": "0.8"
}
```

or export the same names as environment variables *in the terminal you run the
daemon from* (a launchd agent does not inherit your shell env; the rendered
plist only sets the `PATH` captured at `aegis install` time). After a
`pipx ensurepath` or a shell change, run `aegis restart` so the plist is
re-rendered with the new `PATH` (an unchanged plist is hot-restarted with
`kickstart`; a changed one is booted out and re-bootstrapped so launchd reads
the new program path). `config.json` is the reliable route for secrets under
launchd. If
no key is set the daemon still runs and blocks nothing (every verdict falls
back to allow). Invalid values (e.g. a bad `AEGIS_PORT`) fall back to defaults
instead of crashing.

> Note: the Chrome extension hardcodes `127.0.0.1:8123` (see
> `extension/background.js`), so `AEGIS_PORT`/`AEGIS_HOST` should be left at
> their defaults for the extension to work.

Extra always-allowed domains can be added without touching code:

```json
{ "EXTRA_WHITELIST": "khanacademy.org, brilliant.org" }
```

Whitelisted entries match subdomains too (e.g. `en.wikipedia.org`) and are
IDNA-normalized, so Unicode domains work. Public-suffix entries (e.g. `co.uk`,
`com.au` — domains that are really TLDs) are rejected with a warning: you
cannot whitelist an entire country's domains, and the rejection keeps a typo
from silently widening the allowlist.

Then load the extension:

1. Open Chrome → `chrome://extensions`
2. Enable **Developer mode** → **Load unpacked**
3. Select the `extension/` folder

## Usage

- Click the **A** menu bar icon → **Start Focus Session…** → enter topic and
  duration (1–600 min). The session cannot be stopped early through the app (a
  deliberate user can always remove it — see "Locked sessions" above).
- When you finish (or the daemon restarts mid-session), the lock is restored
  automatically from
  `~/Library/Application Support/Aegis/session.json` (a backup copy
  `.bak` is kept and used if the lock file is ever corrupt).

## Verdict pipeline

1. No active session → allow.
2. Whitelisted domains (`github.com`, `wikipedia.org`, …) and browser-internal
   pages (`chrome://`, `about:`, `edge://`, Chrome Web Store, the Aegis daemon
   itself, …) → allow, no LLM call.
3. SQLite verdict cache (per URL per session) → instant repeat answers. LLM
   failures (timeout, transport error, unparseable reply) are **never** cached —
   an outage cannot seal a page open (or lock one shut).
4. LLM call with page title, URL, meta description, and top/middle/bottom text
   samples (or YouTube title/channel/description for videos) → JSON verdict
   `{"allowed": bool, "confidence": 0-1, "reason": str}`. Verdicts are
   rate-limited per session (up to 30 per 5 s) so a tab-storm cannot burn the
   LLM budget; a burst of navigations simply waits its turn and gets slower
   answers, it never makes more calls.
5. Any LLM failure → allow (never block browsing due to API issues) — meaning
   a slow or stalled evaluator intentionally lets an off-task page through. The
   API is loopback-only and rejects every non-extension Origin, so no website
   can read your session or burn your LLM quota.

## The "Aegis" page

Off-task pages are redirected to `http://127.0.0.1:8123/blocked`, which:

- Shows the block reason, session topic, and a live countdown.
- **Auto-returns** to the original page 5 seconds after the session ends
  (no input needed).
- **Appeals**: a text box + "Appeal" button. The appeal is sent to the LLM,
  which decides whether your justification warrants access to that page
  *during this session*. The page's link is attached automatically — nothing
  to copy.
  - Granted → the page opens immediately and the allow verdict is cached for
    the session, so it is not instantly re-blocked.
  - Denied → the reason is shown; you may appeal again (rate-limited to 1 per
    5 s to protect the LLM budget).
  - LLM outage → **denied** (fail-closed — an appeal is not an unlock bypass).
    Appeals are retried up to 6 times before giving up.
  - Grants are per-session: a new session judges the page fresh.
  - The page also shows an "Aegis daemon unreachable" hint after several
    failed /status polls, so a dead daemon is visible instead of a silently
    frozen countdown.

## Uninstall

```bash
aegis uninstall
```

Removes the LaunchAgent, `~/Library/Logs/Aegis/`, and the entire
`~/Library/Application Support/Aegis/` state directory (session lock,
cache, config). The pipx package itself is left installed
(`pipx uninstall aegis` removes that too). The project directory is left in
place.

## Development

```bash
python3 -m venv venv
./venv/bin/pip install -e .
python -m aegis.main          # run daemon in foreground (menu bar app)
python3 -m tests.test_evaluator   # offline daemon+CLI unit tests (no LLM needed)
node aegis/static/test_blocked.mjs      # blocked-page UI harness
node extension/test_background.mjs      # extension harness
```

Logs: `~/Library/Logs/Aegis/` — `daemon.log` (auto-rotating, capped at
~1.5 MB total) plus unbounded `daemon.out.log` / `daemon.err.log` (launchd's
stdout/stderr redirects). The daemon is crash-proof by design: corrupt lock
files fall back to the backup copy, a corrupt SQLite cache degrades to an
in-memory cache, and any LLM/cache error always degrades to *allow*.

## License

GNU General Public License v3.0 or later — see [LICENSE](LICENSE).
