diff --git a/.gitignore b/.gitignore index 0d8b737..d29d61a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __pycache__/ dist/ build/ .pytest_cache/ +.installed +.mypy_cache/ +.ruff_cache/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bd4808b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +# AGENTS.md + +Development guidelines for solstone-tmux, a standalone tmux terminal observer for solstone. + +## Project Overview + +solstone-tmux is a companion app that runs alongside the main solstone journal. It captures all active tmux terminal sessions every 5 seconds, accumulates captures into 5-minute segments in a local cache, and syncs completed segments to the solstone ingest API. Pure Python, no system dependencies beyond tmux itself. Works offline -- segments sync when the server becomes available. Recovers incomplete segments on startup after crashes. + +This is a **solstone observer** -- a standalone capture agent that feeds data into a solstone journal. It follows the same patterns as solstone-macos (the macOS screen/audio observer) but captures terminal content instead of screen recordings. + +## Source Layout + +``` +src/solstone_tmux/ + __init__.py Package init, version + cli.py CLI entry point (run, setup, install-service, status) + config.py Config loading/persistence (~/.local/share/solstone-tmux/) + capture.py Tmux capture library (polls sessions, panes, deduplication) + observer.py Main capture loop with segment rotation + streams.py Stream name derivation (hostname.tmux convention) + sync.py Background sync service (uploads segments to server) + upload.py HTTP upload client for solstone ingest API + recovery.py Crash recovery for orphaned .incomplete segments +tests/ + test_capture.py Capture result serialization, hashing, JSONL writing + test_config.py Config round-trip, defaults, permissions + test_streams.py Stream name derivation and hostname stripping + test_sync.py Recovery and segment collection logic +contrib/ + solstone-tmux.service Reference systemd unit file +``` + +## Build and Test Commands + +```bash +make install # Create venv, install package in editable mode with dev deps +make test # Run all tests with pytest +make test-only TEST=tests/test_capture.py # Run a specific test file +make test-only TEST="-k test_function_name" # Run tests matching a pattern +make format # Auto-format and lint with ruff +make ci # Full CI: format check + lint + tests +make clean # Remove build artifacts and caches +make uninstall # Remove venv and all artifacts +make clean-install # Clean everything and reinstall from scratch +``` + +## Development Principles + +- **Pure Python, minimal dependencies.** Runtime dependency is `requests` only. No frameworks, no heavy libraries. Keep it lean. +- **Stdlib over libraries.** Use `subprocess` for tmux interaction, `asyncio` for the event loop, `dataclasses` for data structures. +- **Atomic writes.** Write to `.tmp` then `os.rename()` for config and state persistence. +- **Offline-first.** Captures always write to local cache. Sync is best-effort with retry and circuit breaker. +- **Crash recovery.** `.incomplete` segment directories get recovered on startup. `.failed` directories are quarantined. +- **Test everything, mock external state.** Tests must never call real tmux or real HTTP endpoints. Use `tmp_path`, monkeypatch, and fixtures to isolate completely. + +## File Headers + +All Python source files must include this header as the first two lines: + +```python +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright (c) 2026 sol pbc +``` + +Add this to new `.py` files in `src/solstone_tmux/` and `tests/`. Do not add headers to TOML, Makefile, or markdown files. + +## Architecture Notes + +### Capture Loop + +The observer (`observer.py`) runs a single `asyncio` event loop. Every 5 seconds it polls tmux for active sessions, captures changed panes, and accumulates captures in memory. Every 5 minutes (configurable), it finalizes the segment: writes JSONL files to disk and triggers the sync service. + +### Segment Format + +Segments live under `~/.local/share/solstone-tmux/captures/YYYYMMDD/stream/HHMMSS_DDD/` where `DDD` is duration in seconds. During recording, the directory has a `.incomplete` suffix. Each segment contains one JSONL file per tmux session (`tmux_{session}_screen.jsonl`). + +### Sync Service + +The `SyncService` runs as a background `asyncio` task. It walks cached days newest-to-oldest, queries the server for existing segments, and uploads missing ones. A circuit breaker opens after 3 consecutive failures. + +### Registration + +Observer registration tries `sol observer create` via CLI first (works without a running server if sol is on PATH), falling back to HTTP registration at the server's `/app/observer/api/create` endpoint. + +## Config + +Config file: `~/.local/share/solstone-tmux/config/config.json` + +```json +{ + "server_url": "http://localhost:5015", + "key": "", + "stream": ".tmux", + "capture_interval": 5, + "segment_interval": 300 +} +``` + +## License + +AGPL-3.0-only. Copyright (c) 2026 sol pbc. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dcd2c62 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +# solstone-tmux Makefile +# Standalone tmux terminal observer for solstone + +.PHONY: install test test-only format ci clean clean-install uninstall + +# Default target +all: install + +# Virtual environment directory +VENV := .venv +VENV_BIN := $(VENV)/bin +PYTHON := $(VENV_BIN)/python + +# Require uv +UV := $(shell command -v uv 2>/dev/null) +ifndef UV +$(error uv is not installed. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh) +endif + +# Venv tool shortcuts +PYTEST := $(VENV_BIN)/pytest +RUFF := $(VENV_BIN)/ruff + +# Marker file to track installation +.installed: pyproject.toml + @echo "Installing solstone-tmux with uv..." + $(UV) venv --quiet --allow-existing $(VENV) + $(UV) pip install --quiet -e ".[dev]" --python $(PYTHON) + @touch .installed + +# Install package in editable mode with dev dependencies +install: .installed + +# Run all tests +test: .installed + @echo "Running tests..." + $(PYTEST) tests/ -q + +# Run a specific test file or pattern +test-only: .installed + @if [ -z "$(TEST)" ]; then \ + echo "Usage: make test-only TEST="; \ + echo "Example: make test-only TEST=tests/test_capture.py"; \ + echo "Example: make test-only TEST=\"-k test_function_name\""; \ + exit 1; \ + fi + $(PYTEST) $(TEST) + +# Auto-format and fix code, then report remaining issues +format: .installed + @echo "Formatting with ruff..." + @$(RUFF) format . + @$(RUFF) check --fix . + @echo "" + @echo "Checking for remaining issues..." + @$(RUFF) check . || { echo ""; echo "Issues above need manual fixes."; exit 1; } + @echo "" + @echo "All clean!" + +# Run CI checks (what CI would run) +ci: .installed + @echo "Running CI checks..." + @echo "=== Checking formatting ===" + @$(RUFF) format --check . || { echo "Run 'make format' to fix formatting"; exit 1; } + @echo "" + @echo "=== Running ruff ===" + @$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; } + @echo "" + @echo "=== Running tests ===" + @$(MAKE) test + @echo "" + @echo "All CI checks passed!" + +# Clean build artifacts and caches +clean: + @echo "Cleaning build artifacts..." + rm -rf build/ dist/ *.egg-info/ src/*.egg-info/ + rm -rf .pytest_cache/ .mypy_cache/ .ruff_cache/ + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete 2>/dev/null || true + rm -f .installed + +# Remove venv and all artifacts +uninstall: clean + @echo "Removing virtual environment..." + rm -rf $(VENV) + +# Clean everything and reinstall +clean-install: uninstall install diff --git a/pyproject.toml b/pyproject.toml index 6df6511..9e16d64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,12 @@ dependencies = [ "requests", ] +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", +] + [project.scripts] solstone-tmux = "solstone_tmux.cli:main"