npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xaverric/activity-tracker

v0.2.1

Published

macOS activity tracking daemon with encrypted local storage and on-demand AI summaries

Downloads

505

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| E

Why 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 exits

The 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 AXDocument accessibility 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: 45

Requires: 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-tracker

This installs the tool globally so you can run it via the activity-tracker command.

Then run first-time encryption setup:

activity-tracker setup

Every 5 minutes the CLI session expires. Re-authenticate with:

activity-tracker unlock

Most 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 activity

Configuration

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 continuously

Privacy & 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 summary or ask
  • 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.