# Pulse Stack Tracker

Rust daemon for tracking Codex and Claude Code activity from local JSONL files and uploading Pulse Stack heartbeats.

For installer, first-run, service, and diagnostics instructions, see [SETUP.md](SETUP.md).

## Current Status

This tracker is already operational:

- scans `~/.codex/sessions/**/*.jsonl` and `~/.codex/archived_sessions/**/*.jsonl`
- enriches activity with thread names from `~/.codex/session_index.jsonl`
- scans Claude Code sessions and subagents under `~/.claude/projects/**/*.jsonl`
- infers a contextual `primary_file` from observed file paths and separately records files changed by write signals
- builds heartbeat payloads for Pulse Stack
- uploads to `/api/pulse-stack/v1/users/current/heartbeats.bulk` with Bearer authentication when `api_key` is configured
- stores live cursors in `~/.pulse-stack/tracker/state.json`
- stores failed uploads in `~/.pulse-stack/tracker/pending.json`
- supports `configure`, `install-service`, `uninstall-service`, and `doctor` in addition to the tracking commands
- is bundled and supervised by native macOS menu-bar and Windows tray apps, while Linux uses a systemd user service
- ships from one tagged-release workflow as a native macOS app ZIP/PKG, Linux ZIP/DEB/RPM, and Windows ZIP/MSI artifacts
- measures foreground human activity in supported editors, Codex, Claude, and terminals without recording window titles or input content
- uploads closed, replay-safe human activity segments to the native Pulse Stack v2 endpoint

## How It Works

The binary:

1. walks Codex rollout files under `~/.codex` and Claude Code transcripts under `~/.claude/projects`
2. reads only new lines in live mode using per-file cursors
3. routes each file through its source adapter and emits provider-neutral activity records
4. groups activity by `session_id + cwd + thread_name`
5. infers the dominant file for the bucket
6. emits one heartbeat per bucket
7. reads auth and API endpoint from `~/.pulse-stack/config.toml`
8. uploads immediately when backend config is present, or safely queues bounded activity when `api_key` is absent

Alongside transcript metadata, the daemon samples only the identity of the
foreground application and the operating system idle duration. Consecutive
samples are folded into short closed intervals. A gap, idle state, unsupported
surface, sleep, or restart closes the interval instead of extrapolating time.
Codex and Claude transcript metadata attributes those intervals to a provider,
session, task, and project without uploading prompt or transcript content.

Backfill mode:

- replays all matching rollout lines since a given date
- does not mutate live cursor state
- can run with `--dry-run` to preview counts without upload

The heartbeat mapping is fixed for the current collector contract:

- `project` = repository name
- `branch` = Codex thread name or Claude Code session slug
- `entity` = primary file path
- `editor` = `codex` or `claude`
- `git_branch` remains in `codex_context.git_branch`
- `pulse_context.provider` identifies `codex` or `claude`
- `codex_context.touched_files` remains the serialized compatibility field for the complete bounded changed-file list used by backend session/file attribution for both providers
- `codex_context.touched_files_sample` remains a configurable compatibility/debug sample

## Privacy Model

The tracker uploads compact activity metadata, not conversation contents.

Excluded from the normalized activity model and upload payload:

- user prompt text
- assistant message text
- full tool outputs
- full patch content
- encrypted reasoning payloads
- Claude Code prompt and assistant text
- Claude Code tool results and tool output
- Claude Code edit/write content and Bash command text

Supported Claude file tools contribute only their tool name and file path. Bash command strings are inspected in memory to infer file paths and write intent, then discarded before activity records are created.

## Build

```sh
cd codex_tracker
cargo build
```

For a release build:

```sh
cd codex_tracker
cargo build --release
```

The expected stable install path for the binary is:

```sh
~/.pulse-stack/bin/pulse-stack-tracker
```

## Configuration

Primary config path:

```sh
~/.pulse-stack/config.toml
```

Minimal Pulse Stack config:

```toml
api_url = "https://ps.qqmikey.com/api/pulse-stack/v1"
api_key = "replace-me"
```

Generic tracker settings and source-specific settings can live in the same file:

```toml
[tracker]
device_name = "Developer Mac"
poll_interval_seconds = 5
heartbeat_interval_seconds = 120
idle_gap_seconds = 900
sample_size = 5
max_pending_heartbeats = 10000
max_pending_activity_segments = 10000
presence_enabled = true
presence_interval_seconds = 5
presence_maximum_bridge_seconds = 15
presence_idle_threshold_seconds = 60
presence_reading_grace_seconds = 120
presence_flush_interval_seconds = 60

[sources.codex]
home = "/Users/q/.codex"

[sources.claude]
home = "/Users/q/.claude"
```

Existing `[codex_tracker]` configuration is still read during upgrades and is rewritten to the generic structure by `configure`.

Notes:

- `api_key` is required for upload and `test-connection`
- `api_url` and `api_key` are read from `~/.pulse-stack/config.toml`
- `configure --import-wakatime` copies compatible endpoint/key values into the private Pulse Stack config without changing `~/.wakatime.cfg`
- `~/.wakatime.cfg` remains a compatibility fallback when Pulse Stack config is absent
- if `api_key` is empty, `run` and `run-once` still parse activity and persist it in bounded queues for upload after authentication
- the tracker sends credentials in the `Authorization: Bearer` header, never in request URLs
- `max_pending_heartbeats` and `max_pending_activity_segments` bound local disk queues; the oldest records are dropped only after the configured limit and the dropped count is reported in status
- the Codex source home defaults to `~/.codex` and the Claude source home defaults to `~/.claude`
- `heartbeat_interval_seconds` controls synthetic heartbeat spacing inside an active agent span
- `idle_gap_seconds` splits long activity into separate spans; keep it aligned with the dashboard session gap if you want totals to match exactly
- presence tracking is enabled by default and requires no Accessibility, Input Monitoring, screen-capture, administrator, or root permission
- macOS and Windows provide foreground application plus idle-time coverage through standard system APIs
- Linux X11 uses `xprop` plus `xprintidle` or `xssstate`; missing tools are reported as partial/unavailable instead of fabricated time
- Wayland environments without a supported foreground-application protocol report editor-only or unavailable coverage honestly
- `status` and `doctor` show presence capability, queue depth, the latest sample/closed segment, and degraded reason

## Local Usage

Inspect resolved paths and installation state:

```sh
cd codex_tracker
cargo run -- status
```

Render the same local state as versioned JSON for a supervising application:

```sh
cd codex_tracker
cargo run -- status --json
```

Run the daemon loop in foreground:

```sh
cd codex_tracker
cargo run -- run
```

A native desktop application can supervise the same tracker without writing the
API key to disk or process arguments:

```sh
printf 'YOUR_API_KEY\n' | cargo run -- run --api-key-stdin --device-name "Developer Mac"
```

In supervised mode the first stdin line supplies the key. The parent keeps stdin
open for the lifetime of the helper; closing it stops the helper. This mode is for
the native Pulse Stack apps, while the config-based flow remains supported for
Linux, diagnostics, and headless compatibility installations.

Run a single live pass:

```sh
cd codex_tracker
cargo run -- run-once
```

Replay historical activity since a date:

```sh
cd codex_tracker
cargo run -- backfill --since 2026-03-18 --dry-run
```

Verify backend credentials:

```sh
cd codex_tracker
cargo run -- test-connection
```

Use `--config /path/to/config.toml` only when you want to test a non-default Pulse Stack config file.

Important live behavior:

- the first live scan of each source starts its existing Codex or Claude files at EOF, so enabling a new source does not silently import history
- after a source is initialized, newly created session and subagent files are read from the beginning so short live sessions are not missed
- cursor state is written to `~/.pulse-stack/tracker/state.json`
- LaunchAgent and CLI use `~/.pulse-stack/config.toml` automatically
- `~/.wakatime.cfg` remains a fallback for Waka-compatible setups
- failed uploads are queued in `~/.pulse-stack/tracker/pending.json`
- failed human-activity uploads are queued separately in `~/.pulse-stack/tracker/pending-segments.json`
- the next successful upload flushes both new heartbeats and the pending queue
- `backfill --dry-run` computes Codex and Claude heartbeat counts without touching live state or uploading

Claude Code subagent transcripts under a session's `subagents/` directory receive a distinct session identity so concurrent agents remain separate on the timeline. The collector uses the Claude session slug as the task/session label and never derives a label from prompt text.

## Install And Connect

On macOS or Windows, install Pulse Stack, open its menu-bar/tray icon, and choose
**Sign in**. Browser approval creates a dedicated device key, stores it in the
platform credential store, and starts the bundled tracker without terminal
commands or config editing. Legacy config keys are removed only after a verified
credential-store write.

Linux continues to use service packages. Its ZIP distribution contains the
binary and installer:

```sh
./install.sh
```

The installer:

- copies the binary to `~/.pulse-stack/bin/`
- creates or updates `~/.pulse-stack/config.toml` with user-only permissions on Unix
- imports an existing Waka-compatible key when requested by the installer
- registers and starts the correct current-user background service
- runs `doctor`

For a new account, pass the API key through standard input so it is not stored in shell history:

```sh
printf '%s' 'YOUR_API_KEY' | ~/.pulse-stack/bin/pulse-stack-tracker configure --api-key-stdin
~/.pulse-stack/bin/pulse-stack-tracker install-service
~/.pulse-stack/bin/pulse-stack-tracker doctor
```

The service adapter remains available to headless/compatibility CLI builds:

- macOS CLI compatibility: `~/Library/LaunchAgents/com.pulsestack.tracker.plist`
- Linux: `~/.config/systemd/user/pulse-stack-tracker.service`
- Windows compatibility CLI: `PulseStackTracker` Task Scheduler task; the public MSI never creates it

The native service runs with the current user's ordinary permissions. It never
requests admin/root rights and does not use accessibility hooks, global keyboard
hooks, screenshots, or window titles.

## Public Release Files

The public product does not link customers to the repository or GitHub Actions. The
current installers are checked into
`django_backend/static/downloads/tracker/v<version>/` and described by
`django_backend/static/downloads/tracker/release-manifest.json`. Django uses that
manifest to render the version, platform, architecture, package size, download URL,
and checksum links on `/download/`.

When publishing a tracker version:

1. build all three platform artifact sets with the release workflow;
2. copy the tested files into a new versioned static directory;
3. update the release manifest with the exact filenames, byte sizes, and SHA-256 hashes;
4. run the Django marketing tests, which verify every manifest entry against the file
   stored in the project;
5. deploy the Django application so the files are collected and served by Pulse Stack.

Keep only intentionally supported public versions in this directory. Never point the
marketing page back to a private repository release.

The macOS release job is fail-closed. It imports separate SISHOR Developer ID
Application and Developer ID Installer certificates into an ephemeral keychain,
signs the universal Rust helper before the universal Swift application, notarizes
and staples the application, builds the signed PKG, then notarizes and staples the
PKG. `codesign`, `pkgutil`, `stapler`, and `spctl` must all accept the artifacts
before upload. The required GitHub Actions secrets are:

- `MACOS_APPLICATION_CERTIFICATE_BASE64`
- `MACOS_APPLICATION_CERTIFICATE_PASSWORD`
- `MACOS_INSTALLER_CERTIFICATE_BASE64`
- `MACOS_INSTALLER_CERTIFICATE_PASSWORD`
- `APPLE_NOTARY_KEY_BASE64`
- `APPLE_NOTARY_KEY_ID`
- `APPLE_NOTARY_ISSUER_ID`

The Windows release job is also fail-closed. It signs the native app, Rust
helper, and MSI with Authenticode, verifies every signature, and only then
uploads the artifacts. Configure:

- `WINDOWS_SIGNING_CERTIFICATE_BASE64`
- `WINDOWS_SIGNING_CERTIFICATE_PASSWORD`

## Logs And Status

Expected log files:

```sh
~/.pulse-stack/tracker/stdout.log
~/.pulse-stack/tracker/stderr.log
```

Check current paths and installation state:

```sh
cd codex_tracker
cargo run -- status
```

Validate the complete local setup:

```sh
~/.pulse-stack/bin/pulse-stack-tracker doctor
```

## Uninstall

On macOS, turn off **Launch at login** in Settings, quit Pulse Stack, and move
`Pulse Stack.app` from Applications to the Trash. On Windows, uninstall Pulse
Stack from Installed apps. Local queues, logs, and tracker state remain
available for a later reinstall on both platforms.

For Linux or a legacy/headless CLI installation, stop and unregister only the
Pulse Stack-owned background service:

```sh
~/.pulse-stack/bin/pulse-stack-tracker uninstall-service
```

Configuration, queue, logs, and activity state are deliberately retained. Data deletion is a separate explicit operation.

## Troubleshooting

- On macOS or Windows, use the menu-bar/tray status and Diagnostics panel first. Manual config is an Advanced option only.
- For Linux/headless CLI installs, if `cargo run -- status` shows `pulse stack config` as missing, create `~/.pulse-stack/config.toml`.
- If `test-connection` fails with `api_key is required`, add `api_key` to the config.
- If upload fails, inspect `~/.pulse-stack/tracker/pending.json` and `stderr.log`.
- If the service does not start, run `doctor`, then inspect the platform service definition and tracker logs.
- Re-running the platform installer is the supported upgrade path; service registration is refreshed idempotently.
