@xaverric/activity-tracker
v0.2.1
Published
macOS activity tracking daemon with encrypted local storage and on-demand AI summaries
Downloads
505
Maintainers
Readme
Activity Tracker Agent
macOS only. A daemon that passively tracks computer activity throughout the day, stores it locally in an encrypted SQLite database, and uses the Claude Agent SDK to generate on-demand summaries for worklogs.
Architecture
The system has two completely independent parts that share only the SQLite database:
graph TB
subgraph DAEMON["Daemon (runs all day, zero AI cost)"]
D[daemon.js]
D --> C1[active-window<br/>10s poll]
D --> C2[browser-url<br/>30s poll]
D --> C3[git-activity<br/>5min poll]
D --> C4[shell-history<br/>5min poll]
D --> C5[keyboard<br/>event-driven]
D --> C6[clipboard<br/>3s poll]
end
subgraph DB["~/.activity-tracker/activity.db"]
E[(events table)]
S[(summaries table)]
end
subgraph AGENT["Agent (on-demand, costs ~$0.01/run)"]
A[agent/<br/>Claude Agent SDK]
MCP[MCP Tools<br/>read-only DB access]
A --> MCP
end
C1 & C2 & C3 & C4 & C5 & C6 -->|write| E
MCP -->|read| E
A -->|write| S
CLI[cli.js] -->|start/stop| D
CLI -->|summary/ask| A
CLI -->|events/export/search| EWhy the agent?
The daemon collects raw events — hundreds per day. Window switches, URLs, keystrokes, file edits, git commits. Raw data is noisy and hard to read.
The agent (Claude) is what turns that raw data into useful answers. It does two things:
summary — Generates a structured worklog: queries all event types, groups continuous activity into time ranges, detects task switches and breaks, extracts JIRA tickets from window titles and commit messages, and produces a formatted timeline with hour estimates.
ask — Answers any freeform question about your activity: "what was I working on this morning?", "how much time in WebStorm today?", "what files did I edit related to auth?". The agent picks the right DB queries to answer your question.
Both commands use the same read-only MCP tools (get_events, get_app_summary, get_git_commits, search_events, get_events_range) — the agent can query the DB but never modify your events.
The daemon is dumb storage. The agent is the intelligence layer. You only pay for Claude API when you explicitly run summary or ask.
How the daemon works
sequenceDiagram
participant U as You (CLI)
participant DC as daemon-control.js
participant D as daemon.js
participant DB as SQLite
U->>DC: activity-tracker start
DC->>DC: Check PID file (already running?)
DC->>D: spawn(node daemon.js, {detached: true})
DC-->>U: "Daemon started (pid: 12345)"
Note over DC,D: Parent detaches, daemon runs independently
loop Every 10s / 30s / 5min (per collector)
D->>D: Poll active window / browser / git / etc.
D->>DB: INSERT INTO events
end
U->>DC: activity-tracker stop
DC->>DC: Read PID from ~/.activity-tracker/daemon.pid
DC->>D: kill(pid, SIGTERM)
D->>D: Clear intervals, close DB
D->>D: Remove PID file
D-->>DC: Process exitsThe daemon is a regular Node.js process spawned as a detached child (like a background service). It:
- Writes its PID to
~/.activity-tracker/daemon.pid - Runs all collectors on their intervals
- Stays alive after the CLI exits (detached)
- Cleans up on SIGTERM (stop command) or SIGINT (Ctrl+C)
- Cleans up old events on startup (90 day retention)
- Logs to
~/.activity-tracker/daemon.log
Collectors
active-window
How: Uses AppleScript via osascript to query System Events for the frontmost application and window title.
Interval: Every 10 seconds.
Deduplication: Only records a new event when app::title changes. Computes duration between window switches (stored in duration_seconds).
Editor file paths: When the active app is a known editor (VS Code, WebStorm, IntelliJ, Xcode, Cursor, Sublime), it also:
- Queries the macOS
AXDocumentaccessibility attribute via osascript to get the full absolute file path (e.g.,/Users/you/projects/app/src/index.js) - Parses the window title as a fallback to extract project name and file name
Stored as:
source: "active-window"
app: "WebStorm"
title: "agent-poc – keyboard-activity.js"
metadata: { bundleId, pid, filePath: "/Users/.../keyboard-activity.js", project: "agent-poc" }
duration_seconds: 45Requires: Screen Recording + Accessibility permissions.
browser-url
How: Runs AppleScript via osascript to ask Chrome, Arc, or Safari for the URL and title of the active tab in the frontmost window.
Interval: Every 30 seconds.
Deduplication: Only records when the URL changes from the last seen URL.
Stored as:
source: "browser-url"
app: "chrome"
title: "GitHub - anthropics/claude-code"
url: "https://github.com/anthropics/claude-code"Requires: Automation permission (auto-prompted when osascript first talks to Chrome/Arc).
keyboard
How: Uses node-global-key-listener which spawns a native macOS binary (MacKeyServer) that creates a CGEventTap to intercept all keyboard events system-wide. The collector reconstructs typed text from key names (handles letters, numbers, punctuation, shift, backspace).
Interval: Event-driven with a 5-second idle flush. Every keypress resets a timer. When you stop typing for 5 seconds, the accumulated text is saved as one event (a "typing burst").
Example timeline:
You type "const x = 42" in VS Code, then pause 5 seconds
-> one event: typed="const x = 42", keystrokeCount=14, app="Code"
You type "hello world" in Slack, pause 5 seconds
-> one event: typed="hello world", keystrokeCount=11, app="Slack"Stored as:
source: "keyboard"
app: "WebStorm" (active window at time of first keystroke in burst)
title: "const x = 42" (the reconstructed text)
metadata: { keystrokeCount: 14, durationSeconds: 3, typed: "const x = 42" }Requires: Accessibility + Input Monitoring permissions.
clipboard
How: Runs pbpaste (macOS built-in) to read the current clipboard content, compares a hash against the last known value.
Interval: Every 3 seconds.
Deduplication: Only records when clipboard content actually changes (hash comparison).
Stored as:
source: "clipboard"
app: "Chrome" (active window when copy happened)
title: "the copied text, truncated to 500 chars..."
metadata: { contentLength: 1234, windowTitle: "GitHub - some page" }Configurable: clipboardPreviewLength controls how much text is stored (default 500 chars).
Requires: No special permissions.
git-activity
How: Uses simple-git to run git log --since=today --all in each discovered or configured repository.
Interval: Every 5 minutes.
Deduplication: Tracks seen commit hashes. Only inserts new commits.
Repos: Discovered by scanning gitScanDirs (Documents, Desktop, Downloads by default) or listed explicitly in gitRepos in ~/.activity-tracker/config.json.
Stored as:
source: "git"
app: "my-repo" (repository directory name)
title: "fix: upgrade test config"
timestamp: "2026-02-12 10:30:00" (actual commit time)
metadata: { hash: "abc1234", author: "You", branch: "main", fullHash: "abc1234..." }Requires: No special permissions.
shell-history
How: Reads ~/.zsh_history by tracking the file offset. On each poll, reads only the bytes added since last check.
Interval: Every 5 minutes.
First run: Records the current file size as the offset and skips (so it doesn't import your entire history). Only captures commands entered after the daemon starts.
Handles: Both zsh extended format (: timestamp:0;command) and plain format.
Stored as:
source: "shell"
title: "git push origin main"
timestamp: "2026-02-12 11:00:00" (from zsh timestamp if available)Requires: No special permissions.
Setup
npm install -g @xaverric/activity-trackerThis installs the tool globally so you can run it via the activity-tracker command.
Then run first-time encryption setup:
activity-tracker setupEvery 5 minutes the CLI session expires. Re-authenticate with:
activity-tracker unlockMost commands that read the DB will prompt again when this session expires.
macOS Permissions
The daemon needs these permissions (System Settings > Privacy & Security):
| Permission | What needs it | How to grant | |---|---|---| | Accessibility | active-window, keyboard | Add your terminal app (Terminal, iTerm2, etc.) | | Screen Recording | active-window (window titles) | Add your terminal app | | Input Monitoring | keyboard | Add your terminal app | | Automation > Chrome/Arc | browser-url | Auto-prompted on first run |
After granting Screen Recording, you may need to restart the terminal app.
Usage
# Security / session
activity-tracker setup # one-time encryption setup
activity-tracker unlock # unlock DB for 5 minutes
activity-tracker lock # clear active session
# Daemon
activity-tracker start # start collector daemon
activity-tracker stop # stop daemon
activity-tracker status # daemon status + today's event count
# View & export data (no AI, reads DB directly)
activity-tracker events [date] # show raw events (default: today)
activity-tracker events [date] --source <name> # filter by source
activity-tracker export [date] # export today's events as JSON
activity-tracker export 2026-02-10 2026-02-12 # export date range
activity-tracker export 2026-02-12 -o day.json # export to file
activity-tracker export 2026-02-12 --csv # export as CSV
activity-tracker search <query> # search event history
activity-tracker browse # interactive history browser (TUI)
activity-tracker settings # interactive settings editor (TUI)
activity-tracker repos # list discovered git repos
activity-tracker rescan # trigger git repo rescan in running daemon
# AI-powered
activity-tracker summary [date] # generate structured worklog summary
activity-tracker ask "what was I doing at 2pm?" # ask any question about your activityConfiguration
Create ~/.activity-tracker/config.json to override defaults:
{
"keyboardLayout": "cs",
"excludedUrlPatterns": ["localhost", "internal.example.com"],
"browsers": ["chrome", "arc"],
"gitScanDirs": ["/Users/you/Documents", "/Users/you/Desktop"],
"gitRepos": ["/Users/you/projects/my-repo"],
"excludedApps": ["LoginWindow", "ScreenSaverEngine"],
"aiExcludePatterns": ["private-client"],
"retentionDays": 90,
"keyboardIdleMs": 5000,
"clipboardPreviewLength": 500,
"intervals": {
"activeWindow": 10000,
"browserUrl": 30000,
"gitActivity": 300000,
"shellHistory": 300000,
"clipboard": 3000
}
}All values are optional — only specify what you want to override.
Database
All events go into ~/.activity-tracker/activity.db (SQLite, WAL mode).
events table:
| Column | Type | Description |
|---|---|---|
| id | INTEGER | Auto-increment primary key |
| timestamp | TEXT | YYYY-MM-DD HH:MM:SS in local time |
| source | TEXT | Collector name (active-window, browser-url, git, shell, keyboard, clipboard) |
| app | TEXT | Application name (nullable) |
| title | TEXT | Window title, typed text, command, file path, etc. (nullable) |
| url | TEXT | URL for browser events (nullable) |
| metadata | TEXT | JSON blob with collector-specific extra data (nullable) |
| duration_seconds | REAL | Time spent (only for active-window events) |
summaries table: Stores AI-generated summaries keyed by date.
Encryption & Session Management
The database is encrypted at rest using SQLCipher (AES-256). Even with direct access to the .db file, data cannot be read without the passphrase. The passphrase is stored in the macOS Keychain and CLI access requires re-authentication every 5 minutes.
sequenceDiagram
participant User
participant CLI
participant SessionFile as Session File
participant Keychain as macOS Keychain
participant DB as Encrypted DB
Note over User,DB: Setup (one-time)
User->>CLI: activity-tracker setup
CLI->>User: Prompt for passphrase
User->>CLI: types passphrase
CLI->>Keychain: Store passphrase
CLI->>DB: Create encrypted DB with PRAGMA key
Note over User,DB: CLI command (session expired)
User->>CLI: activity-tracker events
CLI->>SessionFile: Check session timestamp
SessionFile-->>CLI: expired / missing
CLI->>User: Prompt for passphrase
User->>CLI: types passphrase
CLI->>Keychain: Read stored passphrase
CLI-->>CLI: Verify match
CLI->>SessionFile: Write new timestamp
CLI->>DB: Open with PRAGMA key, execute query
Note over User,DB: CLI command (session valid)
User->>CLI: activity-tracker events
CLI->>SessionFile: Check session timestamp
SessionFile-->>CLI: valid (less than 5 min)
CLI->>Keychain: Read passphrase
CLI->>DB: Open with PRAGMA key, execute query
Note over User,DB: Daemon (long-running)
CLI->>Keychain: Read passphrase at startup
CLI->>DB: Open with PRAGMA key, write events continuouslyPrivacy & Password Redaction
All data stays local. Passwords and secrets are automatically redacted before storage.
Redaction layers (implemented in src/redact.js, applied to both keyboard and clipboard):
| Layer | How it works | Example |
|---|---|---|
| Password manager detection | If the active app is 1Password, Bitwarden, LastPass, KeePassXC, Dashlane, NordPass, or macOS Keychain Access — redact everything | Copy anything from 1Password → [REDACTED] |
| Login context detection | If the browser URL matches login/auth/SSO patterns, or the window title says "Sign in", "Password", "2FA", etc. — redact keyboard input | Typing on accounts.google.com/login → [REDACTED] |
| Entropy analysis | Shannon entropy + character class counting. High entropy (>4.0 bits/char) with 3+ char classes (upper, lower, digit, special) and no spaces → redact | P@ssw0rd!2024 → [REDACTED], hello world → passes through |
| Token/key detection | Long alphanumeric strings without spaces (API keys, JWTs, tokens) with high entropy → redact | ghp_1234567890abcdef... → [REDACTED] |
Redacted events still show up in the timeline (so the AI knows something happened), but the sensitive content is replaced:
source: "clipboard", title: "[REDACTED]", metadata: { redacted: true, redactReason: "password_manager" }
source: "keyboard", title: "[REDACTED]", metadata: { redacted: true, redactReason: "high_entropy", keystrokeCount: 16 }Other privacy measures:
- Claude API only sees event data when you explicitly run
summaryorask - Clipboard content truncated to 500 chars by default
- Configurable app/URL exclusions and retention policy (default 90 days auto-cleanup)
Known limitation: Keyboard reconstruction is best-effort. keyboardLayout supports cs mapping today; unsupported layouts may produce imperfect non-ASCII output.
