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.

Runs Locally Open Source No Blackbox App Tracking Local LLM Flow Insights

Quick Start

1

Clone

git clone https://github.com/peab-dev/WorkTracker.git
cd WorkTracker
2

Install

./install.sh

Sets up Python, dependencies, launchd services, shell aliases.

3

Grant macOS permissions

Required — without these, tracking silently fails.
Open System Settings → Privacy & Security and enable the toggles below for your Terminal app (Terminal.app, iTerm2, Warp, …).
ToggleWhy 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.

4

Verify

source ~/.zshrc   # or: wtrl
wt status

The collector is already running. Data appears within seconds.

Optional Power Up

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

RequirementDetails
Operating SystemmacOS only (AppKit, Quartz, launchd)
Python3.9+ (installer can bootstrap via Homebrew)
Xcode CLI ToolsNeeded 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)
OptionalLM Studio or Ollama for local topic & motivation LLMs

Python Dependencies

PackagePurpose
pyobjc-framework-CocoaApp detection, clipboard, menubar
pyobjc-framework-QuartzInput monitoring (CGEventTap), screenshots
pyobjc-framework-EventKitCalendar integration
pandasSnapshot aggregation
rapidfuzzFuzzy title matching
pyyamlConfig files
flaskWeb dashboard

Installation

./install.sh

The installer:

  1. Checks prerequisites (macOS, Xcode CLI Tools, Python 3).
  2. Offers to install Homebrew and Python if missing.
  3. Copies the project to ~/WorkTracker.
  4. Creates data/, logs/, summaries/.
  5. Builds a Python venv and installs dependencies.
  6. Generates 4 launchd plists and loads them.
  7. Adds shell aliases (wts, wtd, wtx, …) to your shell config.

macOS Permissions

Required: without these, input tracking and window titles fail silently.
PermissionWhyPath
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

CLI Commands

All commands run via the wt CLI:

wt <command>

Overview

CommandAliasDescription
wt statuswt s / wtsServices, data counts, latest reports
wt tailwt log / wtlFollow collector logs live
wt helpwt hInteractive help screen

Collector

CommandAliasDescription
wt restartwt r / wtrRestart collector daemon
wt startStart collector
wt stopStop collector

Aggregator

CommandAliasDescription
wt restart-aggwt ra / wtraReload all aggregator launchd schedules
wt dailywt d / wtdRun today’s daily aggregation now
wt weeklywt w / wtwRun weekly aggregation now
wt monthlywt m / wtmRun monthly aggregation now
wt pastwt p / wtpBackfill missing daily & weekly reports
wt reprocesswt rp / wtrpRe-run every day with the current patterns and LLM config

Dashboards

CommandAliasDescription
wt dashwtdashTerminal dashboard (curses UI, 2s refresh)
wt webwtwebFlask web dashboard at http://127.0.0.1:7880
wt menubarwt mb / wtmbmacOS menubar widget (auto-starts the web dashboard)
wt rhythmwt rh / wtrhWeekly activity heatmap in the terminal
wt docswt docu / wtdocsOpen this documentation

Maintenance

CommandAliasDescription
wt compresswt cmp / wtcmpCompress old PNG screenshots to JPEG q75 using sips

Shell Aliases

Installed into your shell config by install.sh:

AliasExpands toDescription
wtswt statusServices, data & storage overview
wtlwt tailFollow collector logs live
wtrwt restartRestart collector daemon
wtrawt restart-aggReload all aggregator schedules
wtdwt dailyRun daily aggregation now
wtwwt weeklyRun weekly aggregation now
wtmwt monthlyRun monthly aggregation now
wtpwt pastBackfill missing daily & weekly reports
wtrpwt reprocessRe-run all days with current patterns
wtxwt status && wt daily && wt weekly && wt monthlyRun everything in one go
wtdashwt dashTerminal dashboard
wtwebwt webWeb dashboard at port 7880
wtmbwt menubarmacOS menubar widget
wtrhwt rhythmWeekly activity heatmap
wtcmpwt compressCompress PNG screenshots to JPEG q75
wtdocswt docsOpen this documentation
wtrlexec $SHELL -lReload shell (activate aliases after install)

Dashboards

Terminal Dashboard

wt dash — curses UI with a 2 s refresh:

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

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.

Tip: Use 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:

RoutePurpose
/explore
/explore/<date>
Drill into a specific day: full session list, topics, timeline, snapshot-level detail.
/statisticsCross-day aggregates: project trends, per-hour intensity, weekday patterns — the long-view analytics.
/screenshotsVisual 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:

EndpointReturns
GET /api/liveCurrent app, focus, today’s totals — menubar polls this every 30 s
GET /api/datesAll 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>/rangeFirst/last snapshot timestamps
GET /api/snapshots/<date>/timelineSnapshot timeline (for the drill-down view)
GET /api/rhythm[/<weeks>]Heatmap data (default 2 weeks)
GET /api/statisticsCross-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/datesDays that have screenshots
GET /api/screenshots/<date>Filenames for a given day
GET /screenshots/file/<date>/<filename>Serve one screenshot file directly
Local only. Flask binds to 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:00summaries/daily/YYYY-MM-DD.md

Weekly

Generated Sunday 23:00summaries/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:30summaries/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:

Privacy: the topic brief contains window titles and URL hosts; the motivation brief contains full screenshots. Both endpoints must stay local. Never point them at a remote host.

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.

FlagPurpose
--quality NJPEG quality 1–100 (default 75)
--date YYYY-MM-DDCompress only this one day
--skip-todayExclude today’s folder (default: included)
--dry-runReport estimated savings, touch no files
--help / -hInline 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

KeyDefaultDescription
interval_seconds5Snapshot interval in seconds
track_clipboard_contenttrueCapture full clipboard text
track_input_countstrueCount keystrokes, clicks, scrolls
track_mediatrueDetect media playback (Spotify, YouTube, …)
track_all_windowstrueMonitor all visible windows
track_keystroke_contentfalseCapture typed plaintext (privacy warning!)
track_browser_urlstrueRead URLs from browser history DBs
track_gitfalseTrack git commits and branches
git_repos[]Repo paths to monitor
git_scan_interval_seconds30Git rescan interval
track_calendarfalseRead events from macOS Calendar

Screenshots

KeyDefaultDescription
screenshot.enabledfalseCapture one PNG per interval, union of active displays
screenshot.interval_seconds60Capture cadence (independent of snapshot interval)
screenshot.dir~/WorkTracker/data/screenshotsOutput directory, one subfolder per day
screenshot.skip_bundle_ids[1Password, …]Bundle IDs whose frontmost focus suppresses capture

Distraction Notifications

KeyDefaultDescription
notifications.enabledfalseEnable macOS distraction warnings
notifications.threshold_minutes15Fallback minutes before an alert fires
notifications.cooldown_minutes30Cooldown between alerts
notifications.distraction_categories{Social Media: 10, Media/Entertainment: 20}List (shared threshold) or dict (per-category minutes)

Aggregator Settings

KeyDefaultDescription
idle_threshold_seconds300Gap > 5 min starts a new session
focus_session_min_seconds600Sessions > 10 min count as “focus”
fuzzy_match_threshold0.65Levenshtein ratio for title matching
same_app_grace_period_seconds20Same-app gap < 20 s = merge (tab switching)
min_session_snapshots4Sessions with < 4 snapshots get absorbed
calendar_classification.meeting_keywords[Meeting, Call, …]Event title keywords that mark meetings
calendar_classification.deep_work_min_minutes30Threshold 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.

KeyDefaultDescription
topic_llm.enabledfalseMaster switch
topic_llm.endpointhttp://localhost:1234/v1/chat/completionsLocal OpenAI-compatible endpoint (LM Studio / Ollama)
topic_llm.modelgoogle/gemma-3-4bModel name passed to the endpoint
topic_llm.timeout_seconds30Per-batch HTTP timeout
topic_llm.batch_size5Sessions per request
topic_llm.max_sessions_per_day500Daily budget cap
topic_llm.min_session_seconds15Skip sessions shorter than this
Keep it local. The brief may contain window titles and URLs. Never point 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).

KeyDefaultDescription
motivation_llm.enabledfalseMaster switch
motivation_llm.endpointhttp://localhost:1234/v1/chat/completionsLocal vision endpoint
motivation_llm.modelqwen/qwen3-vl-8bVision model name
motivation_llm.timeout_seconds120Per-request timeout (vision is slow)
motivation_llm.max_images_per_session1Screenshots sent per session
motivation_llm.max_sessions_per_day40Daily budget cap
motivation_llm.min_session_seconds30Skip very short sessions
motivation_llm.image_max_bytes5000000Hard limit per image (larger ones skipped)
Strict local-only. Screenshots can contain anything: passwords, mail, private chats. The endpoint must stay on localhost. Failure modes (timeout, bad JSON, endpoint down, image too big) are silent — reports keep working with empty motivation messages.

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.

macOS (focus, windows, input, clipboard, screenshots) | v [Collector] every 5–10 s | v data/snapshots/YYYY-MM-DD.jsonl | v [Aggregator] daily / weekly / monthly | v [Enrichment] topic LLM + vision LLM (local) | | v v data/sessions/ summaries/*.md | v [AI Summary] via Cowork

Directory Structure

~/WorkTracker/ ├── wt # CLI tool ├── install.sh # Installer ├── uninstall.sh # Uninstaller │ ├── daemon/ │ ├── collector.py # Snapshot collector (every 5–10 s) │ ├── aggregator.py # Session aggregator + report writer │ ├── dashboard.py # Terminal dashboard (curses) │ ├── web_dashboard.py # Flask web dashboard │ ├── menubar.py # macOS menubar widget │ ├── rhythm_heatmap.py # Weekly activity heatmap │ ├── topic_extractor.py # Local LLM topic enrichment │ ├── motivation_extractor.py # Local vision-LLM motivation enrichment │ ├── web_categories.py # Domain → category knowledge base │ ├── review_patterns.py # Interactive pattern review (TTY) │ ├── compress_screenshots.sh # PNG → JPEG q75 via sips │ ├── ctl.sh # Alternative control script │ ├── config.yaml # Main configuration │ ├── project_patterns.yaml # Project matching rules │ ├── learned_patterns.yaml # Auto-learned patterns │ ├── requirements.txt # Python deps │ └── .venv/ # Python virtual environment │ ├── data/ │ ├── snapshots/ # YYYY-MM-DD.jsonl (1 per day) │ ├── sessions/ # YYYY-MM-DD.json (1 per day) │ └── screenshots/ # YYYY-MM-DD/*.png|jpg │ ├── logs/ # Collector/aggregator log files │ ├── summaries/ │ ├── daily/ # YYYY-MM-DD.md │ ├── weekly/ # YYYY-Wnn.md │ └── monthly/ # YYYY-MM.md │ ├── launchd/ # Generated plist files └── docs/ # This documentation

Launchd Services

ServiceLabelScheduleBehavior
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

CategoryData
Active appName, bundle ID, window title — via Accessibility API for true focus
WindowsAll visible windows (owner, title, position, layer)
InputKeystrokes, left/right clicks, scrolls, mouse distance, idle gaps
ClipboardFull text, change detection
BrowserCurrent URL from Safari / Chrome / Firefox / Arc history DBs
MediaSpotify, Apple Music, VLC, IINA, Podcasts, YouTube, Netflix, Twitch, SoundCloud
GitBranch and recent commits per configured repo
CalendarCurrent/upcoming events via EventKit
ScreenshotsOne PNG per screenshot.interval_seconds, union of active displays
SystemSleep/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
FlagPurpose
--mode daily|weekly|monthlyRequired. Which report to generate.
--date YYYY-MM-DDTarget date (default: today). For weekly/monthly: any date inside the target period.
--progressEmit live phase progress on stderr — used by wt past / wt reprocess for the per-day progress bars.
--tag LABELPrefix every progress line with LABEL (e.g. the date) so batch runs stay readable.

Pipeline

  1. Load JSONL snapshots into a Pandas DataFrame.
  2. Flatten nested fields (active_app, input, clipboard, system, media, git).
  3. Build sessions via two-tier merging (see below).
  4. Match each session to a project via pattern rules.
  5. Classify URLs through web_categories.py.
  6. Compute stats (time distribution, intensity, focus sessions).
  7. Enrich via topic LLM and motivation vision LLM (if enabled).
  8. Render Markdown report with tables.
  9. 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

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.

  1. After daily aggregation, “Other” sessions are analyzed.
  2. Window titles and URL hosts are grouped by keyword/domain.
  3. Groups with ≥ 5 min total become pattern suggestions.
  4. 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

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.

Copied