Track every Mac activity – automatically.
See how you actually work, in detail.
100% open source, privacy-first.
Runs locally, fully transparent.
No black-box, no fees.
Made with <3 in Austria.
Quick Start
Clone
git clone https://github.com/peab-dev/WorkTracker.git
cd WorkTracker
Install
./install.sh
Sets up Python, dependencies, launchd services, shell aliases.
Grant macOS permissions
Open System Settings → Privacy & Security and enable the toggles below for your Terminal app (Terminal.app, iTerm2, Warp, …).
| Toggle | Why it’s needed |
|---|---|
| Accessibility | Keyboard & mouse monitoring (CGEventTap) and true focus detection |
| Screen Recording | Reading window titles of other apps — and capturing screenshots, if enabled |
After toggling either one, macOS will ask you to quit and relaunch your Terminal. Do it — the permission only takes effect after restart.
Full details: Permissions section below.
Verify
source ~/.zshrc # or: wtrl
wt status
The collector is already running. Data appears within seconds.
Plug a local LLM into WorkTracker to enrich every session with a short topic and an optional motivational one-liner. Works out of the box with LM Studio or Ollama on localhost — see Topic LLM and Motivation LLM. Nothing leaves your machine.
LLM-Prompts for scheduled summaries (weekly/daily/monthly) will land in v0.2.1 or above.
Requirements
| Requirement | Details |
|---|---|
| Operating System | macOS only (AppKit, Quartz, launchd) |
| Python | 3.9+ (installer can bootstrap via Homebrew) |
| Xcode CLI Tools | Needed for compiling Python deps |
| Disk |
~140 MB base (mostly the Python venv), plus per day: • ~8–10 MB snapshots (JSONL) • ~0.2 MB sessions + <1 MB reports • ~0.3 MB logs • ~300 MB screenshots (PNG) if screenshot.enabled — roughly ~30–60 MB/day after running wt compress (JPEG q75, typical 5–10× reduction)
|
| Optional | LM Studio or Ollama for local topic & motivation LLMs |
Python Dependencies
| Package | Purpose |
|---|---|
pyobjc-framework-Cocoa | App detection, clipboard, menubar |
pyobjc-framework-Quartz | Input monitoring (CGEventTap), screenshots |
pyobjc-framework-EventKit | Calendar integration |
pandas | Snapshot aggregation |
rapidfuzz | Fuzzy title matching |
pyyaml | Config files |
flask | Web dashboard |
Installation
./install.sh
The installer:
- Checks prerequisites (macOS, Xcode CLI Tools, Python 3).
- Offers to install Homebrew and Python if missing.
- Copies the project to
~/WorkTracker. - Creates
data/,logs/,summaries/. - Builds a Python venv and installs dependencies.
- Generates 4 launchd plists and loads them.
- Adds shell aliases (
wts,wtd,wtx, …) to your shell config.
macOS Permissions
| Permission | Why | Path |
|---|---|---|
| Accessibility | Keyboard/mouse monitoring via CGEventTap | Privacy & Security → Accessibility |
| Screen Recording | Reading window titles — and capturing screenshots, if enabled | Privacy & Security → Screen Recording |
Grant access to your Terminal app (Terminal.app, iTerm2, Warp, …).
Uninstall
./uninstall.sh
- Stops and removes all launchd services.
- Removes shell aliases.
- Optionally deletes collected data.
CLI Commands
All commands run via the wt CLI:
wt <command>
Overview
| Command | Alias | Description |
|---|---|---|
wt status | wt s / wts | Services, data counts, latest reports |
wt tail | wt log / wtl | Follow collector logs live |
wt help | wt h | Interactive help screen |
Collector
| Command | Alias | Description |
|---|---|---|
wt restart | wt r / wtr | Restart collector daemon |
wt start | — | Start collector |
wt stop | — | Stop collector |
Aggregator
| Command | Alias | Description |
|---|---|---|
wt restart-agg | wt ra / wtra | Reload all aggregator launchd schedules |
wt daily | wt d / wtd | Run today’s daily aggregation now |
wt weekly | wt w / wtw | Run weekly aggregation now |
wt monthly | wt m / wtm | Run monthly aggregation now |
wt past | wt p / wtp | Backfill missing daily & weekly reports |
wt reprocess | wt rp / wtrp | Re-run every day with the current patterns and LLM config |
Dashboards
| Command | Alias | Description |
|---|---|---|
wt dash | wtdash | Terminal dashboard (curses UI, 2s refresh) |
wt web | wtweb | Flask web dashboard at http://127.0.0.1:7880 |
wt menubar | wt mb / wtmb | macOS menubar widget (auto-starts the web dashboard) |
wt rhythm | wt rh / wtrh | Weekly activity heatmap in the terminal |
wt docs | wt docu / wtdocs | Open this documentation |
Maintenance
| Command | Alias | Description |
|---|---|---|
wt compress | wt cmp / wtcmp | Compress old PNG screenshots to JPEG q75 using sips |
Shell Aliases
Installed into your shell config by install.sh:
| Alias | Expands to | Description |
|---|---|---|
wts | wt status | Services, data & storage overview |
wtl | wt tail | Follow collector logs live |
wtr | wt restart | Restart collector daemon |
wtra | wt restart-agg | Reload all aggregator schedules |
wtd | wt daily | Run daily aggregation now |
wtw | wt weekly | Run weekly aggregation now |
wtm | wt monthly | Run monthly aggregation now |
wtp | wt past | Backfill missing daily & weekly reports |
wtrp | wt reprocess | Re-run all days with current patterns |
wtx | wt status && wt daily && wt weekly && wt monthly | Run everything in one go |
wtdash | wt dash | Terminal dashboard |
wtweb | wt web | Web dashboard at port 7880 |
wtmb | wt menubar | macOS menubar widget |
wtrh | wt rhythm | Weekly activity heatmap |
wtcmp | wt compress | Compress PNG screenshots to JPEG q75 |
wtdocs | wt docs | Open this documentation |
wtrl | exec $SHELL -l | Reload shell (activate aliases after install) |
Dashboards
Terminal Dashboard
wt dash — curses UI with a 2 s refresh:
- Service status (running / ready / not loaded)
- Current app, window title, live input rates
- Today: active time, sessions, focus count, app switches
- Project breakdown with intensity bars
- Recent session timeline and latest report files
Web Dashboard
wt web starts Flask on http://127.0.0.1:7880. Same data, richer charts: hourly activity, project distribution, top apps, session timeline, service and log monitoring. Override the port via the PORT env var:
PORT=8000 wt web
Menubar Widget
wt menubar attaches a live stats icon to the macOS menu bar. It polls /api/live every 30 s and — if the web dashboard is not already running — spawns it in the background on port 7880. Click the icon to open the full web view.
wtmb as your daily driver. It’s the one command that gives you everything at a glance.
Rhythm Heatmap
wt rhythm [weeks] renders a weekly activity heatmap from summaries/daily/*.md. Each cell is one hour; the “healthy” window is 09:00–22:00. Pass a number to look further back:
wt rhythm # last 1 week
wt rhythm 4 # last 4 weeks
wtrh 8 # last 8 weeks
Statistics & Screenshots Explorer
The web dashboard ships three extra pages on top of the main view:
| Route | Purpose |
|---|---|
/explore/explore/<date> | Drill into a specific day: full session list, topics, timeline, snapshot-level detail. |
/statistics | Cross-day aggregates: project trends, per-hour intensity, weekday patterns — the long-view analytics. |
/screenshots | Visual explorer for captured screenshots, grouped by day. Only populated when screenshot.enabled: true. |
All three pages are reachable once wt web is running.
HTTP API
Everything the dashboard renders is backed by JSON endpoints you can script against:
| Endpoint | Returns |
|---|---|
GET /api/live | Current app, focus, today’s totals — menubar polls this every 30 s |
GET /api/dates | All dates that have snapshots |
GET /api/sessions/<date> | All sessions of a given day |
GET /api/topics/<date> | LLM-generated topics for a given day |
GET /api/snapshots/<date>/range | First/last snapshot timestamps |
GET /api/snapshots/<date>/timeline | Snapshot timeline (for the drill-down view) |
GET /api/rhythm[/<weeks>] | Heatmap data (default 2 weeks) |
GET /api/statistics | Cross-day statistics payload |
GET /api/reports/<rtype> | List all reports of type daily, weekly, or monthly |
GET /api/report/<rtype>/<name> | Raw Markdown of a specific report |
POST /api/open/<rtype>/<name> | Ask the backend to open a report in the system editor |
GET /api/screenshots/dates | Days that have screenshots |
GET /api/screenshots/<date> | Filenames for a given day |
GET /screenshots/file/<date>/<filename> | Serve one screenshot file directly |
127.0.0.1. If you want external access, wrap it in a trusted tunnel (Tailscale, SSH) — never expose it directly.
Reports
Reports are Markdown files generated by the aggregator. They are the structured input Claude Cowork (or any other summarizer) consumes.
Daily
Generated at 22:00 → summaries/daily/YYYY-MM-DD.md
- Overview: active time, focus sessions, app switches, idle ratio.
- Project distribution: time, share, sessions, avg session, intensity.
- App usage: top apps by time.
- Timeline: full session-by-session table with topic and (optional) motivation message.
- Parallel media: music and video playing alongside work.
- Input analysis: keystroke peak hours, hourly intensity bars.
- Git activity: commits per repo (if enabled).
- Calendar & time classification: meetings vs. deep work.
- Previous day comparison: delta metrics.
Weekly
Generated Sunday 23:00 → summaries/weekly/YYYY-Wnn.md: aggregated week (Mon–Sun), daily comparison, project distribution, most/least productive day, previous-week delta.
Monthly
Generated 1st of month 00:30 → summaries/monthly/YYYY-MM.md: full month aggregation, weekly comparison, long-term trend (1st vs. 2nd half), previous-month delta.
Topic & Motivation Enrichment
If you enable the local LLM blocks (see Topic LLM, Motivation LLM), every eligible session in a report carries:
- Topic — a short German phrase (2–20 words) describing what the session was about, produced by
topic_extractor.py. - Topic long — 1–2 sentences, ≤280 chars, same pipeline.
- Motivation message — a one-line German motivational note, produced by
motivation_extractor.pyfrom up to N session screenshots via a local vision LLM.
Maintenance
Backfill Missing Reports
wt past (alias wt p) scans data/snapshots/ for days that have no daily report and for ISO weeks with no weekly report, then runs the aggregator for each. The current (incomplete) week is skipped.
Reprocess
wt reprocess (alias wt rp) re-runs every day in your snapshot history through the aggregator with the current project_patterns.yaml and LLM config. Use this after tweaking patterns, raising LLM quality, or changing session thresholds.
Compress Screenshots
Screenshots pile up fast. wt compress walks data/screenshots/, converts every PNG to JPEG via native sips, and leaves only images younger than 10 s alone so it never races the running collector.
| Flag | Purpose |
|---|---|
--quality N | JPEG quality 1–100 (default 75) |
--date YYYY-MM-DD | Compress only this one day |
--skip-today | Exclude today’s folder (default: included) |
--dry-run | Report estimated savings, touch no files |
--help / -h | Inline usage |
wt compress # everything (incl. today, >10 s old)
wt compress --dry-run # preview savings only
wt compress --date 2026-04-10 # one specific day
wt compress --quality 60 --skip-today # tighter quality, spare today’s folder
Override the source directory with the SCREENSHOTS_DIR env var.
Configuration
Everything lives in one file:
~/WorkTracker/daemon/config.yaml
Collector Settings
| Key | Default | Description |
|---|---|---|
interval_seconds | 5 | Snapshot interval in seconds |
track_clipboard_content | true | Capture full clipboard text |
track_input_counts | true | Count keystrokes, clicks, scrolls |
track_media | true | Detect media playback (Spotify, YouTube, …) |
track_all_windows | true | Monitor all visible windows |
track_keystroke_content | false | Capture typed plaintext (privacy warning!) |
track_browser_urls | true | Read URLs from browser history DBs |
track_git | false | Track git commits and branches |
git_repos | [] | Repo paths to monitor |
git_scan_interval_seconds | 30 | Git rescan interval |
track_calendar | false | Read events from macOS Calendar |
Screenshots
| Key | Default | Description |
|---|---|---|
screenshot.enabled | false | Capture one PNG per interval, union of active displays |
screenshot.interval_seconds | 60 | Capture cadence (independent of snapshot interval) |
screenshot.dir | ~/WorkTracker/data/screenshots | Output directory, one subfolder per day |
screenshot.skip_bundle_ids | [1Password, …] | Bundle IDs whose frontmost focus suppresses capture |
Distraction Notifications
| Key | Default | Description |
|---|---|---|
notifications.enabled | false | Enable macOS distraction warnings |
notifications.threshold_minutes | 15 | Fallback minutes before an alert fires |
notifications.cooldown_minutes | 30 | Cooldown between alerts |
notifications.distraction_categories | {Social Media: 10, Media/Entertainment: 20} | List (shared threshold) or dict (per-category minutes) |
Aggregator Settings
| Key | Default | Description |
|---|---|---|
idle_threshold_seconds | 300 | Gap > 5 min starts a new session |
focus_session_min_seconds | 600 | Sessions > 10 min count as “focus” |
fuzzy_match_threshold | 0.65 | Levenshtein ratio for title matching |
same_app_grace_period_seconds | 20 | Same-app gap < 20 s = merge (tab switching) |
min_session_snapshots | 4 | Sessions with < 4 snapshots get absorbed |
calendar_classification.meeting_keywords | [Meeting, Call, …] | Event title keywords that mark meetings |
calendar_classification.deep_work_min_minutes | 30 | Threshold for deep-work classification |
Topic LLM
Sends session briefs (app, window title, URL host, filename, project, duration, activity counts) to a local OpenAI-compatible chat endpoint and writes a short German topic back into every session. Clipboard samples are never sent.
| Key | Default | Description |
|---|---|---|
topic_llm.enabled | false | Master switch |
topic_llm.endpoint | http://localhost:1234/v1/chat/completions | Local OpenAI-compatible endpoint (LM Studio / Ollama) |
topic_llm.model | google/gemma-3-4b | Model name passed to the endpoint |
topic_llm.timeout_seconds | 30 | Per-batch HTTP timeout |
topic_llm.batch_size | 5 | Sessions per request |
topic_llm.max_sessions_per_day | 500 | Daily budget cap |
topic_llm.min_session_seconds | 15 | Skip sessions shorter than this |
endpoint at a remote host.
Motivation LLM (Vision)
Sends up to N session screenshots to a local vision-capable chat endpoint and writes a one-line German motivation message to session["motivation_message"]. Needs a vision model (e.g. qwen3-vl-8b, llava).
| Key | Default | Description |
|---|---|---|
motivation_llm.enabled | false | Master switch |
motivation_llm.endpoint | http://localhost:1234/v1/chat/completions | Local vision endpoint |
motivation_llm.model | qwen/qwen3-vl-8b | Vision model name |
motivation_llm.timeout_seconds | 120 | Per-request timeout (vision is slow) |
motivation_llm.max_images_per_session | 1 | Screenshots sent per session |
motivation_llm.max_sessions_per_day | 40 | Daily budget cap |
motivation_llm.min_session_seconds | 30 | Skip very short sessions |
motivation_llm.image_max_bytes | 5000000 | Hard limit per image (larger ones skipped) |
Project Patterns
Projects are matched by window title and URL patterns in:
~/WorkTracker/daemon/project_patterns.yaml
Example
projects:
MyProject:
patterns: ["*myproject*", "*MyProject*"]
category: "Development"
AI Research:
patterns: ["*claude*", "*openai*", "*chatgpt*"]
category: "AI/Research"
Crypto:
patterns: ["*binance*", "*bitcoin*"]
url_patterns: ["*binance.com*", "*coingecko.com*"]
category: "Crypto"
default_project: "Other"
Glob syntax (* = wildcard), case-insensitive match against window titles. URL patterns apply when track_browser_urls is on.
Web Categories
daemon/web_categories.py is a stdlib-only knowledge base mapping domains to a two-level hierarchy (main_category, subcategory) — Development / Code Hosting, Development / Q&A, Social Media / …, Media/Entertainment / …. The aggregator uses it to build the “Browsing Analysis” section of each report and to evaluate distraction thresholds against notifications.distraction_categories.
Add new domains by editing the DOMAIN_CATEGORIES dict at the top of the file — no config, no restart.
How It Works
WorkTracker follows a 4-layer architecture: collect → aggregate → enrich → summarize.
Directory Structure
Launchd Services
| Service | Label | Schedule | Behavior |
|---|---|---|---|
| Collector | com.peab.worktracker.collector |
Always on | RunAtLoad + KeepAlive (auto-restarts) |
| Daily Agg | com.peab.worktracker.aggregator.daily |
22:00 daily | One shot, writes daily report |
| Weekly Agg | com.peab.worktracker.aggregator.weekly |
Sunday 23:00 | One shot, writes weekly report |
| Monthly Agg | com.peab.worktracker.aggregator.monthly |
1st of month 00:30 | One shot, writes monthly report |
All services run with Nice: 10 (low priority) and log to ~/WorkTracker/logs/.
Collector Deep Dive
Long-running Python daemon. Every few seconds it writes one JSON snapshot.
What It Captures
| Category | Data |
|---|---|
| Active app | Name, bundle ID, window title — via Accessibility API for true focus |
| Windows | All visible windows (owner, title, position, layer) |
| Input | Keystrokes, left/right clicks, scrolls, mouse distance, idle gaps |
| Clipboard | Full text, change detection |
| Browser | Current URL from Safari / Chrome / Firefox / Arc history DBs |
| Media | Spotify, Apple Music, VLC, IINA, Podcasts, YouTube, Netflix, Twitch, SoundCloud |
| Git | Branch and recent commits per configured repo |
| Calendar | Current/upcoming events via EventKit |
| Screenshots | One PNG per screenshot.interval_seconds, union of active displays |
| System | Sleep/wake events, active space |
Input Monitoring
Uses CGEventTap in listen-only mode on a dedicated CFRunLoop thread — it can observe events but cannot inject them.
Focus Detection
Uses AXUIElementCopyAttributeValue instead of NSWorkspace.frontmostApplication so floating overlays (e.g. the Claude Cowork widget) don’t steal the “frontmost” label.
Aggregator Deep Dive
Execution Modes & Flags
python aggregator.py --mode daily # today
python aggregator.py --mode daily --date 2026-04-01 # specific date
python aggregator.py --mode weekly
python aggregator.py --mode monthly
| Flag | Purpose |
|---|---|
--mode daily|weekly|monthly | Required. Which report to generate. |
--date YYYY-MM-DD | Target date (default: today). For weekly/monthly: any date inside the target period. |
--progress | Emit live phase progress on stderr — used by wt past / wt reprocess for the per-day progress bars. |
--tag LABEL | Prefix every progress line with LABEL (e.g. the date) so batch runs stay readable. |
Pipeline
- Load JSONL snapshots into a Pandas DataFrame.
- Flatten nested fields (
active_app,input,clipboard,system,media,git). - Build sessions via two-tier merging (see below).
- Match each session to a project via pattern rules.
- Classify URLs through
web_categories.py. - Compute stats (time distribution, intensity, focus sessions).
- Enrich via topic LLM and motivation vision LLM (if enabled).
- Render Markdown report with tables.
- Run pattern learning on “Other” sessions.
Session Detection
Sessions group consecutive snapshots that represent one work context.
Tier 1 — same app, small gap
Same app within same_app_grace_period_seconds → always merge. Handles rapid tab switching.
Tier 2 — same app, larger gap
Larger gap but same app → merge only if window titles hit the fuzzy threshold (Levenshtein ratio ≥ fuzzy_match_threshold).
Split triggers
- Different app.
- Idle gap exceeds
idle_threshold_seconds. - Sleep/wake event.
- Title similarity drops below the fuzzy threshold.
Micro-session absorption
Sessions with fewer than min_session_snapshots are absorbed into the preceding session to cut noise.
Pattern Learning
Unclassified (“Other”) sessions are used to suggest new project patterns automatically.
- After daily aggregation, “Other” sessions are analyzed.
- Window titles and URL hosts are grouped by keyword/domain.
- Groups with ≥ 5 min total become pattern suggestions.
- Suggestions are written to
learned_patterns.yaml.
Priority
Static patterns in project_patterns.yaml always win. Hand-curated rules are never overridden.
Learned Pattern Format
projects:
github.com:
patterns: ["*github.com*"]
url_patterns: ["*github.com*"]
category: "auto-learned"
_auto_generated: true
_total_time_seconds: 3240
_first_seen: "2026-04-01"
_sample_titles: ["Issues - anthropics/claude-code"]
Interactive Review
daemon/review_patterns.py is a small TTY tool that walks through every learned_patterns.yaml suggestion and lets you keep, rename, or discard it interactively. Accepted entries are promoted into project_patterns.yaml.
python3 ~/WorkTracker/daemon/review_patterns.py
Run it after a week of data has accumulated — it’s the fastest way to turn noisy auto-learned patterns into clean, curated project rules.
Special Features
Sleep/Wake Detection
- Explicit: NSWorkspace sleep/wake notifications.
- Heuristic: loop iteration > 3× expected interval → gap record inserted.
Midnight Continuity
The daily aggregator loads the previous day’s tail (≥ 23:55) so sessions spanning midnight merge cleanly.
Intensity Scoring
Normalized input activity (keystrokes + clicks + scrolls over session duration) rendered as 10-character bar charts in reports and dashboards.
Focus vs. Deep Work
Sessions longer than focus_session_min_seconds are “focus sessions”; sessions past deep_work_min_minutes are promoted to deep work.
Browser URL Tracking
Reads SQLite history databases directly for Safari, Chrome, Firefox, Brave, Arc and Edge. URLs are matched against url_patterns and fed through web_categories.py.
Media Detection
Native apps (Spotify, Apple Music, VLC, IINA, Podcasts) via their own APIs; browser tabs (YouTube, Netflix, Twitch, SoundCloud) via window-title pattern matching. Parallel media is tracked alongside work sessions and shown in the report.