Skip to main content

A local-first knowledge base for your AI coding sessions

Project description

StackUnderflow

StackUnderflow: the local observability for your coding agents. Search, replay, and analyse every session, all offline. Starts with Claude Code.

Quickstart | Features | Configuration | Architecture | Contributing

StackUnderflow Dashboard

Quickstart

Requirements: Python 3.10+ and an existing ~/.claude/ directory from using Claude Code. Adapters for more coding agents are on the way.

pip install stackunderflow
stackunderflow init

Your browser opens to http://localhost:8081 with every project under ~/.claude/projects/ indexed and ready to browse.

Common knobs:

stackunderflow init --no-browser      # don't auto-open the browser
stackunderflow cfg set port 8090      # change the port
stackunderflow backup create          # snapshot ~/.claude/ before risky changes
stackunderflow --help                 # everything else

If port 8081 is taken: stackunderflow cfg set port <free-port> then re-run init.

Nix (reproducible)

A flake is provided for Nix users — no Python or Node setup required:

nix run github:0bserver07/StackUnderflow      # launch the dashboard
nix build github:0bserver07/StackUnderflow    # build, output at ./result
nix develop                                   # dev shell with python + node

See flake.nix for the full derivation.

Development setup

If you want to hack on StackUnderflow (or install from source), you'll also need Node 18+ to build the React UI:

git clone https://github.com/0bserver07/StackUnderflow.git
cd StackUnderflow

# 1. Build the React UI (one-time)
cd stackunderflow-ui && npm install && npm run build && cd ..

# 2. Install the Python package in editable mode
pip install -e .

# 3. Launch the dashboard
stackunderflow init

Features

  • Analytics dashboard — token usage, cost breakdown, model distribution, error patterns, hourly activity
  • Cost tab — token-burn attribution: top sessions by cost, most expensive commands (click → Messages tab), tool-cost ranking, token composition (donut + stacked daily), cache ROI, outliers, retry-loop signals, week-over-week trends, and an error-cost estimate. Filter state (range / session / tool) is URL-encoded so views are shareable. Implementation: stackunderflow-ui/src/pages/cost/ + stackunderflow/routes/cost.py + stackunderflow/routes/commands.py.
  • Session viewer — browse individual JSONL session files with conversation replay, sub-agent grouping, per-session cost. Deep-linked detail views show a breadcrumb + back button.
  • Light / dark theme — toggle in the header (sun/moon). Persists to localStorage['suf:theme'].
  • Full-text search — across all sessions, with filters for date, model, and role
  • Q&A pair detection — heuristic extraction of question-answer pairs based on text patterns and follow-up cues
  • Auto-tagging — tags sessions by language, framework, topic, and intent (build, fix, explore, refactor, test, ops) using keyword and pattern matching
  • Resolution status — flags Q&A pairs as resolved, looped, or abandoned based on follow-up patterns, with loop counts surfaced in the dashboard
  • Bookmarks — save and organise important conversations
  • Incremental backupsstackunderflow backup create snapshots ~/.claude/ with hard-linked rsync --link-dest (use backup auto on macOS for daily scheduling)
  • Multi-project — switch between projects, view cross-project statistics
  • Legacy project recovery — pre-January 2026 Claude Code stored prompts in ~/.claude/history.jsonl instead of per-project JSONL files. StackUnderflow auto-detects these old projects and surfaces them from that file (prompts and timestamps only — token/model data wasn't stored locally in the old format).

Using as a Library

StackUnderflow also works as a Python package for scripting and automation:

import stackunderflow

# List all Claude Code projects on your machine
projects = stackunderflow.list_projects()
# [{"dir_name": "...", "log_path": "...", ...}, ...]

# Query the session store directly
from stackunderflow.store import db, queries

from pathlib import Path
store = Path.home() / ".stackunderflow" / "store.db"
conn = db.connect(store)
project = queries.get_project(conn, slug=projects[0]["dir_name"])
messages, stats = queries.get_project_stats(conn, project_id=project.id)
conn.close()

tokens = stats["overview"]["total_tokens"]
print(f"Sessions: {stats['overview']['sessions']}")
print(f"Tokens: {tokens['input']:,} in / {tokens['output']:,} out")
print(f"Total cost: ${stats['overview']['total_cost']:.2f}")

The stats modules are also importable for custom workflows:

from stackunderflow.stats import classifier, enricher, aggregator, formatter
from stackunderflow.infra.discovery import locate_logs

Configuration

# Change port (default: 8081)
stackunderflow cfg set port 8090

# Disable auto-opening browser
stackunderflow cfg set auto_browser false

# Show current settings
stackunderflow cfg ls

# Reset a setting to default
stackunderflow cfg rm port

# Sync the session store with the latest JSONL files (incremental)
stackunderflow reindex
Key Default Description
port 8081 Server port
host 127.0.0.1 Server host
auto_browser true Auto-open browser on start
max_date_range_days 30 Default date range for analytics

See CLI reference for all commands.

Architecture

stackunderflow/
  adapters/       # source-adapter layer (one adapter per AI tool)
    base.py       #   LogAdapter ABC — discover() + stream_messages() protocol
    claude.py     #   Claude Code adapter — reads ~/.claude/projects/ + history.jsonl
  ingest/         # mtime-gated incremental import into the store
    enumerate.py  #   fan all adapters' SessionRefs into one iterable
    writer.py     #   transactional writer — one file → one transaction → one ingest_log row
  store/          # SQLite session store (~/.stackunderflow/store.db)
    db.py         #   connection factory (WAL mode, row_factory)
    schema.py     #   CREATE TABLE migrations
    queries.py    #   typed read helpers (list_projects, get_project_stats, …)
    types.py      #   frozen dataclasses returned by query helpers
  stats/          # message classification + analytics (no I/O — pure transforms)
    classifier.py #   tag message types and error patterns
    enricher.py   #   build dataset with interaction chains
    aggregator.py #   compute statistics (one pass, collector-based)
    formatter.py  #   shape messages for the REST API
  infra/
    discovery.py  # find and enumerate Claude log directories
    costs.py      # per-model cost estimation
  routes/         # FastAPI route modules
    projects.py   #   project selection and listing
    data.py       #   stats, dashboard-data, messages, refresh (store-backed)
    sessions.py   #   session browsing (store-backed)
    search.py     #   full-text search
    qa.py         #   Q&A pair browsing
    tags.py       #   auto-tags and manual tagging
    bookmarks.py  #   bookmark CRUD + session metadata enrichment
    misc.py       #   pricing, health, static
  services/       # search, Q&A, tags, bookmarks, pricing
  deps.py         # shared state (config, services, store_path)
  server.py       # thin shell — app creation, middleware, lifespan
  settings.py     # env → file → default config resolution (descriptor-based)
  cli.py          # click CLI (init, start, cfg, backup, clear-cache, reindex)

stackunderflow-ui/  # React + TypeScript + Tailwind frontend

Data flow

~/.claude/projects/<slug>/*.jsonl
         │
         ▼
  ClaudeAdapter.enumerate() + read()
         │  (incremental: skip unchanged mtime/offset)
         ▼
  ingest/writer.py  →  SQLite store (~/.stackunderflow/store.db)
         │
         ▼
  queries.get_project_stats(conn, project_id)
         │  (reconstructs messages, feeds stats/ chain)
         ▼
  stats/{classifier → enricher → aggregator → formatter}
         │
         ▼
  /api/stats, /api/dashboard-data, /api/messages

How refresh works

stackunderflow reindex (or /api/refresh) fans every registered adapter's discovered files through ingest/enumerate.py. For each file the runner compares the stored last_mtime and last_offset and only reads newly-appended bytes. New messages are written transactionally to the store. The store is then the source of truth for all API endpoints — session browsing, cross-project aggregation (reports/aggregate.py), and the waste-finding heuristic (reports/optimize.py).

On first successful ingest the legacy ~/.stackunderflow/cache/ directory is removed if present.

Source adapters

Currently supports Claude Code only — JSONL logs under ~/.claude/projects/ plus the legacy ~/.claude/history.jsonl for pre-January-2026 sessions. A proposal sketch for adding OpenAI Codex (~/.codex/) lives at docs/codex-adapter-spec.md; it is an unimplemented RFC, not a roadmap commitment.

Privacy

StackUnderflow processes all your Claude Code logs locally.

What it reads on your machine:

  • ~/.claude/projects/<slug>/*.jsonl — per-project conversation logs (new format)
  • ~/.claude/history.jsonl — centralized prompt history (legacy format, pre-January 2026)
  • ~/.claude/settings*.json — only via the backup command, for snapshots

What it does with that data:

  • Parsing, search indexing, and analytics run locally — nothing is uploaded
  • The session store is written to ~/.stackunderflow/store.db (SQLite, WAL mode)
  • Backups (opt-in) are written to ~/.stackunderflow/backups/ unencrypted — protect this directory like you would your ~/.claude/

What leaves your machine (only if you enable it):

  • Pricing — fetches model cost data from a public GitHub source (no user data sent)

No telemetry, no tracking, no crash reports. No sharing.

Contributing

# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
python -m pytest tests/stackunderflow/ -v

# Lint
bash lint.sh

See docs/README-DEV.md for architecture details.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stackunderflow-0.3.1.tar.gz (7.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stackunderflow-0.3.1-py3-none-any.whl (2.1 MB view details)

Uploaded Python 3

File details

Details for the file stackunderflow-0.3.1.tar.gz.

File metadata

  • Download URL: stackunderflow-0.3.1.tar.gz
  • Upload date:
  • Size: 7.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for stackunderflow-0.3.1.tar.gz
Algorithm Hash digest
SHA256 818b0234917561039cc5172714c19b92f4f9436178aa05901df9599da7dbb14f
MD5 97fd4327fd03694c5054ddecc8abe366
BLAKE2b-256 f6246e28214cb6365b46d044ad6a2a641f2959b97be15b7f408795a6446da0b5

See more details on using hashes here.

File details

Details for the file stackunderflow-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: stackunderflow-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 2.1 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for stackunderflow-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 cded7c3d5659e74c629490e554e78460f4155c9b7447c1eb0787e1cd6230607a
MD5 9df6ffc0699da775a59c49ed6c4b2c1d
BLAKE2b-256 9c03f88515641ed7d4579338af0a96410be32d58e680f517d1cc7952cd932bf9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page