Metadata-Version: 2.5
Name: campus-agent
Version: 0.1.0
Summary: Local-first student assistant for LPU: timetable and attendance sync, class alerts, mail digest, calendar export. Everything stays on your device.
Project-URL: Homepage, https://github.com/alok024/campus-agent
Author: Alok Kashyap
License: MIT
License-File: LICENSE
Keywords: attendance,local-first,lpu,student,timetable,ums
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Education
Requires-Python: >=3.11
Requires-Dist: keyring>=24
Requires-Dist: platformdirs>=3
Requires-Dist: websocket-client>=1.6
Provides-Extra: dev
Requires-Dist: hypothesis>=6; extra == 'dev'
Requires-Dist: pre-commit>=3; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: filevault
Requires-Dist: cryptography>=42; extra == 'filevault'
Description-Content-Type: text/markdown

# campus-agent

Lovely Professional University's student portal, UMS, is the one place a student's timetable,
attendance, exam schedule and fees live, and it was not built to be read by anything but a human
in a browser. It's a legacy ASP.NET site bolted onto a newer Next.js single-page app, it sits
behind a Cloudflare bot check that silently fails any headless browser, there is no API, and its
attendance page renders a student as attending zero classes if the browser window reading it is
narrower than about 1000 pixels. [`docs/ums-traps.md`](docs/ums-traps.md) records eleven
behaviours like that, each one measured against the live portal because guessing produced a wrong
diagnosis first. Day to day, checking it means opening a browser, logging in by hand, and doing
attendance math in your head against the bar that decides whether you're allowed to sit an exam.

campus-agent turns that into a background job. It drives a real, visible Chrome or Edge window
through the login over the raw DevTools protocol — no Selenium, no Playwright, three runtime
dependencies in total — because a visible window is what the portal's own bot check actually
requires. From the synced data: `campus next` says what class you're in and what's after it,
`campus bunk CAB106` says exactly how many more periods of a course you can miss and stay over
the bar, `campus cal export` puts your timetable on your phone as a real calendar with class
reminders, and an optional mail digest surfaces exam and fee mail out of an unread inbox.

Everything above runs and stores its state entirely on your own device: no server, no account, no
telemetry, no cost. Credentials go to your operating system's own secret store, never to a file in
this repo or anywhere else — see [Security design](#security-design) below for exactly how, and
how the optional AI feature is kept from being steered by a hostile email.

## Security design

campus-agent ends up holding a UMS password and, if you turn the modules on, a Gmail app password
and an AI API key. This is precisely what happens to them, so you can check it against
[`vault.py`](src/campus_agent/vault.py) and [`ai/guard.py`](src/campus_agent/ai/guard.py) instead
of taking the description's word for it.

**Credentials never touch a file.** By default every secret goes to your OS's own secret store —
Windows Credential Manager, macOS Keychain, or the Linux Secret Service, via the `keyring`
library — the same layer a password manager uses, protected by the OS rather than by
campus-agent. There is no `.env`, no config key, and no plaintext copy anywhere on disk.

**Where no OS secret store is available** (a Linux box with no keyring daemon running),
campus-agent falls back to an encrypted file rather than to plaintext. Each secret is encrypted
with AES-256-GCM under a key derived from a passphrase you choose via scrypt (`n=2^17, r=8, p=1`,
a ~128MB working set) — deliberately expensive, to make brute-forcing the passphrase costly. The
entry's own name (`ums-password`, `mail-app-password`, ...) is bound into the encryption as the
AEAD's associated data, so a ciphertext copied under a different name fails to decrypt instead of
silently decrypting into the wrong secret. If neither a keyring nor the encrypted file is usable,
the last resort asks for the secret every time and stores nothing — never a plaintext fallback.

**Prompt injection is the default assumption, not an edge case.** `campus ask` hands the model
text that other people wrote — some of it mail you didn't choose to receive. That data is wrapped
between a fence line carrying a fresh random token generated on every call, and the system prompt
states plainly that nothing between the fences is an instruction. Any line inside the data that
happens to match the fence's own pattern is stripped before wrapping, so injected text cannot
forge a closing fence and talk its way out of the untrusted block. The model is never given
tools; its output is only ever displayed. The worst a malicious email can do is make one answer
on screen wrong — it cannot touch your accounts, your files, or your synced data.

**What's on disk and what leaves the device.** Everything lives in per-user app directories with
owner-only permissions on Linux/macOS (`campus doctor` prints the exact paths). The only network
connections campus-agent makes on its own are to the UMS portal (`ums.lpu.in` and
`studentums.lpu.in`) and your IMAP server; turning on the optional modules adds ntfy.sh (phone
push), your chosen AI endpoint, and a Google secret-iCal URL if you connect one. During a sync
the real browser it drives may also make Chrome's own background requests, and nothing else talks
to the network. Attendance percentages never leave the device; phone-push messages carry course
codes, rooms and times, never percentages. Teachers' names are read so a course section can be
identified, but are never repeated into a notification or handed to the AI — a faculty change is
detected by comparing a fingerprint of the raw text and reported only as a count
("3 listed -> 2"), never as names (`ums/model.py`). The local dashboard
(`campus serve`) binds to 127.0.0.1 only and rejects DNS-rebinding and cross-site requests.

## Install

You need Python 3.11 or newer and Google Chrome or Microsoft Edge.

Once the project is published, install straight from GitHub:

```
pip install pipx
pipx install git+https://github.com/alok024/campus-agent
campus setup
```

Before then (or offline), install the release wheel someone shares with you:

```
pip install pipx
pipx install ./campus_agent-0.1.0-py3-none-any.whl
campus setup
```

`campus setup` asks which modules you want and stores your credentials in the OS keyring
(Windows Credential Manager, macOS Keychain, or the Linux secret service). Every module is
opt-in; skip anything you do not want.

## Commands

| Command | What it does |
|---|---|
| `campus sync` | Log into UMS and refresh timetable, attendance, exams, marked dates |
| `campus next` | The class happening now and the next one coming up |
| `campus today` | Today's classes |
| `campus bunk CAP7001` | How many periods you can skip and stay over the 75% bar |
| `campus cal export` | Write the timetable + exam dates as an `.ics` calendar file |
| `campus mail digest` | Unread mail, with exam/fee/placement keywords flagged first |
| `campus serve` | Local dashboard at http://127.0.0.1:8765 with an auto-sync loop |
| `campus schedule install` | Sync automatically in the background (systemd on Linux, Task Scheduler on Windows) |
| `campus paste timetable page.html` | Ingest a saved UMS page when live sync will not work |
| `campus ask "..."` | Optional: ask an AI about your synced data (bring your own key) |
| `campus doctor` | Check every module and report what works |

## Your timetable on your phone

Run `campus cal export` and import the `.ics` file into Google Calendar (or any calendar app).
Your classes appear as recurring events with rooms, exams and holidays as all-day events, and
your phone gives you native class notifications. Events carry stable ids, so re-importing after
a timetable change updates them in place instead of duplicating.

For push notifications ("room changed", "class in 10 minutes"), enable the ntfy option in
`campus setup` and subscribe to your private topic in the ntfy app.

## How the UMS sync works

`campus sync` drives a real Chrome or Edge window (your normal browser stays untouched; the tool
uses its own browser profile). The window closes when the read finishes. On a headless Linux box
it uses Xvfb instead. If automation ever breaks on your machine, paste mode
([docs/paste-mode.md](docs/paste-mode.md)) keeps everything working: save the UMS page with
Ctrl+S and feed it to `campus paste`.

The scraper is deliberately paranoid: every value read from the portal must match a strict
pattern (a course code, a room, a time, a date, a percentage) before it is stored, portal prose
is never copied into anything a notification or an AI later reads, and a page that only half
rendered is refused rather than stored. [docs/ums-traps.md](docs/ums-traps.md) documents the
portal's sharp edges for contributors.

## Mail

Read-only, over IMAP with a Gmail app password ([docs/gmail-app-password.md](docs/gmail-app-password.md)).
The tool cannot send, delete, or mark anything. Keywords you care about (exam, datesheet, fee,
attendance, placement) are flagged first; edit them in the config file.

## AI (optional)

Everything else works without this. If you add a key (Groq and OpenRouter both have free tiers,
or point it at a local Ollama), `campus ask` answers questions over your synced data. See
[Security design](#security-design) above for exactly how untrusted portal and mail text is kept
from being read as instructions.

## Config

`campus doctor` prints where your `config.toml` lives; [config.example.toml](config.example.toml)
documents every knob. Environment variables like `CAMPUS_UMS_SYNC_INTERVAL_MINUTES` override the
file.

## Development

```
git clone https://github.com/alok024/campus-agent
cd campus-agent
uv venv && uv pip install -e ".[dev,filevault]"
pytest
scripts/check.sh
```

132 tests, run in CI on Linux, Windows and macOS against Python 3.11 and 3.12, plus a full-history
gitleaks scan. Windows testers are especially welcome: see [docs/windows.md](docs/windows.md).

Distributed under the MIT license — see [LICENSE](LICENSE).
