linux observer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Makefile, AGENTS.md, and CLAUDE.md for agent infrastructure

Makefile provides install (uv + dev tools), test, format, ci, and clean
targets. AGENTS.md documents project architecture, source layout, commands,
conventions, and file header requirements. CLAUDE.md symlinks to AGENTS.md.

Also adds pytest and ruff as dev dependencies in pyproject.toml and
extends .gitignore for venv/uv artifacts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+218
+4
.gitignore
··· 4 4 dist/ 5 5 build/ 6 6 .pytest_cache/ 7 + .mypy_cache/ 8 + .venv/ 9 + .installed 10 + uv.lock
+115
AGENTS.md
··· 1 + # AGENTS.md 2 + 3 + Development guidelines for solstone-linux, a standalone Linux desktop observer. 4 + 5 + ## Project Overview 6 + 7 + solstone-linux is a companion app that runs alongside the main [solstone](https://solstone.app) journal. It captures screen recordings and audio from a Linux desktop using PipeWire and GStreamer, stores segments locally, and syncs them to a solstone server. It runs as a systemd user service on GNOME Wayland sessions. 8 + 9 + This is **not** part of the solstone monorepo. It is a standalone package with its own release lifecycle, installed via pipx alongside system-provided PyGObject/GStreamer bindings. 10 + 11 + ## Source Layout 12 + 13 + ``` 14 + src/solstone_linux/ 15 + __init__.py Package version 16 + cli.py CLI entry point (run, setup, install-service, status) 17 + config.py Config loading/persistence (~/.local/share/solstone-linux/) 18 + observer.py Main capture loop — state machine (idle/screencast), audio + video 19 + screencast.py Portal-based multi-monitor recording (xdg-desktop-portal + GStreamer) 20 + audio_recorder.py Stereo audio recording (mic + system via soundcard) 21 + audio_detect.py Audio device detection via ultrasonic tone 22 + audio_mute.py PulseAudio mute state detection 23 + activity.py GNOME-specific activity detection (idle, screen lock, power save) 24 + monitor_positions.py Monitor position assignment from geometry 25 + session_env.py Desktop session environment checks and recovery 26 + streams.py Stream name derivation (hostname-based) 27 + sync.py Background sync service — uploads completed segments to server 28 + upload.py HTTP upload client for solstone ingest server 29 + recovery.py Crash recovery for orphaned .incomplete segments 30 + 31 + tests/ pytest test suite 32 + contrib/ Reference systemd service unit 33 + ``` 34 + 35 + ## Architecture 36 + 37 + The observer runs a single asyncio event loop with two concurrent concerns: 38 + 39 + 1. **Capture loop** (`observer.py`) — Checks activity status every 5 seconds, records audio continuously, manages screencast recording via GStreamer. Creates 5-minute segments in `~/.local/share/solstone-linux/captures/YYYYMMDD/stream/HHMMSS_DDD/`. Segment directories start as `.incomplete` and are renamed on finalization. 40 + 41 + 2. **Sync service** (`sync.py`) — Background asyncio task that walks the captures directory, queries the server for existing segments, and uploads missing ones. Circuit breaker pattern with error-type-aware thresholds. 42 + 43 + State machine has two modes: `screencast` (screen active, recording video) and `idle` (screen inactive). Mode transitions, mute state changes, and 5-minute intervals all trigger segment boundaries. 44 + 45 + The capture loop never makes network calls. It writes locally; sync handles all uploads. 46 + 47 + ## Commands 48 + 49 + ```bash 50 + make install # Create venv, install package + dev tools (pytest, ruff) via uv 51 + make test # Run all tests 52 + make test-only TEST=tests/test_config.py # Run specific test 53 + make format # Auto-format with ruff 54 + make ci # Lint + format check + tests 55 + make clean # Remove build artifacts and caches 56 + make versions # Show installed package versions 57 + ``` 58 + 59 + ## Development Principles 60 + 61 + - **Simple code.** Prefer plain functions over classes. Use dataclasses for structured data. Only use classes when managing stateful lifecycle (Observer, Screencaster, SyncService, AudioRecorder). 62 + - **Async by default.** The main loop is asyncio. DBus calls, subprocess management, and sync all use async. Audio recording uses a dedicated thread because soundcard is blocking. 63 + - **No network in the capture loop.** The observer writes segments locally. The sync service uploads asynchronously. This keeps capture reliable even when the server is down. 64 + - **Atomic directory operations.** Segments start as `HHMMSS.incomplete/`, are renamed to `HHMMSS_DDD/` on completion, or `HHMMSS.failed/` on recovery failure. 65 + - **System site-packages required.** PyGObject and GStreamer bindings come from system packages. The venv (and pipx) must use `--system-site-packages`. 66 + 67 + ## File Headers 68 + 69 + All `.py` source files must include this header as the first two lines: 70 + 71 + ```python 72 + # SPDX-License-Identifier: AGPL-3.0-only 73 + # Copyright (c) 2026 sol pbc 74 + ``` 75 + 76 + Add this header to new `.py` files in `src/solstone_linux/` and `tests/`. Do not add headers to markdown, TOML, or config files. 77 + 78 + ## Runtime Dependencies 79 + 80 + System packages (not pip-installable): 81 + - `python3-gobject` / `python3-gi` — PyGObject for GTK4 and GDK 82 + - GStreamer with PipeWire plugin (`gst-launch-1.0 pipewiresrc`) 83 + - PipeWire running 84 + - `pactl` (PulseAudio utils) for mute detection 85 + - xdg-desktop-portal with ScreenCast support 86 + 87 + Python packages (in pyproject.toml): 88 + - `requests` — HTTP upload client 89 + - `numpy` — Audio buffer manipulation and RMS computation 90 + - `soundfile` — FLAC encoding 91 + - `soundcard` — Audio device enumeration and recording 92 + - `dbus-next` — Async DBus client for portal and activity detection 93 + - `PyGObject` — GDK monitor geometry (installed from system) 94 + 95 + ## Data Paths 96 + 97 + - Config: `~/.local/share/solstone-linux/config/config.json` 98 + - Captures: `~/.local/share/solstone-linux/captures/` 99 + - State: `~/.local/share/solstone-linux/state/` 100 + - Restore token: `~/.local/share/solstone-linux/config/restore_token` 101 + 102 + ## Key Patterns 103 + 104 + - **Activity detection is GNOME-specific.** Uses Mutter IdleMonitor, GNOME ScreenSaver, and Mutter DisplayConfig DBus interfaces. Other desktops capture screen and audio but won't get activity-based segment boundaries. 105 + - **Audio is stereo-interleaved.** Left channel = microphone, right channel = system audio. When muted, channels are split into separate mono FLAC files. 106 + - **Screencast uses xdg-desktop-portal.** Session persistence via restore tokens avoids re-prompting the user. GStreamer subprocess (`gst-launch-1.0`) handles the actual PipeWire recording. 107 + - **Crash recovery runs on startup.** `recovery.py` scans for orphaned `.incomplete` directories older than 2 minutes and finalizes or marks them as failed. 108 + 109 + ## Testing 110 + 111 + Tests use pytest with standard mocking. No system dependencies required for tests — audio devices, DBus, and GStreamer are mocked. Run `make test` to execute the full suite. 112 + 113 + ## License 114 + 115 + AGPL-3.0-only -- Copyright (c) 2026 sol pbc
+1
CLAUDE.md
··· 1 + AGENTS.md
+92
Makefile
··· 1 + # solstone-linux Makefile 2 + # Standalone Linux desktop observer for solstone 3 + 4 + .PHONY: install test test-only format ci clean clean-install versions all 5 + 6 + # Default target 7 + all: install 8 + 9 + # Virtual environment directory 10 + VENV := .venv 11 + VENV_BIN := $(VENV)/bin 12 + PYTHON := $(VENV_BIN)/python 13 + 14 + # Require uv 15 + UV := $(shell command -v uv 2>/dev/null) 16 + ifndef UV 17 + $(error uv is not installed. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh) 18 + endif 19 + 20 + # Marker file to track installation 21 + .installed: pyproject.toml 22 + @echo "Installing package with uv (including dev tools)..." 23 + $(UV) sync --group dev 24 + @touch .installed 25 + 26 + # Install package in editable mode with isolated venv 27 + install: .installed 28 + 29 + # Venv tool shortcuts 30 + PYTEST := $(VENV_BIN)/pytest 31 + RUFF := $(VENV_BIN)/ruff 32 + 33 + # Run all tests 34 + test: .installed 35 + @echo "Running tests..." 36 + $(PYTEST) tests/ -q 37 + 38 + # Run specific test file or pattern 39 + test-only: .installed 40 + @if [ -z "$(TEST)" ]; then \ 41 + echo "Usage: make test-only TEST=<test_file_or_pattern>"; \ 42 + echo "Example: make test-only TEST=tests/test_config.py"; \ 43 + echo "Example: make test-only TEST=\"-k test_function_name\""; \ 44 + exit 1; \ 45 + fi 46 + $(PYTEST) $(TEST) 47 + 48 + # Auto-format and fix code, then report remaining issues 49 + format: .installed 50 + @echo "Formatting and fixing code with ruff..." 51 + @$(RUFF) format . 52 + @$(RUFF) check --fix . 53 + @echo "" 54 + @echo "Checking for remaining issues..." 55 + @$(RUFF) check . || { echo ""; echo "Issues above need manual fixes."; exit 1; } 56 + @echo "" 57 + @echo "All clean!" 58 + 59 + # Run CI checks (what CI would run) 60 + ci: .installed 61 + @echo "Running CI checks..." 62 + @echo "=== Checking formatting ===" 63 + @$(RUFF) format --check . || { echo "Run 'make format' to fix formatting"; exit 1; } 64 + @echo "" 65 + @echo "=== Running ruff ===" 66 + @$(RUFF) check . || { echo "Run 'make format' to auto-fix"; exit 1; } 67 + @echo "" 68 + @echo "=== Running tests ===" 69 + @$(MAKE) test 70 + @echo "" 71 + @echo "All CI checks passed!" 72 + 73 + # Clean build artifacts and cache files 74 + clean: 75 + @echo "Cleaning build artifacts and cache files..." 76 + rm -rf build/ dist/ *.egg-info/ 77 + rm -rf .pytest_cache/ .mypy_cache/ 78 + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true 79 + find . -type f -name "*.pyc" -delete 80 + find . -type f -name "*.pyo" -delete 81 + rm -f .installed 82 + 83 + # Clean everything and reinstall 84 + clean-install: clean install 85 + 86 + # Show installed package versions 87 + versions: .installed 88 + @echo "=== Python version ===" 89 + $(PYTHON) --version 90 + @echo "" 91 + @echo "=== Installed packages ===" 92 + @$(UV) pip list | grep -E "^(pytest|ruff|requests|numpy|soundfile|soundcard|dbus-next|PyGObject)" || true
+6
pyproject.toml
··· 17 17 [project.scripts] 18 18 solstone-linux = "solstone_linux.cli:main" 19 19 20 + [dependency-groups] 21 + dev = [ 22 + "pytest", 23 + "ruff", 24 + ] 25 + 20 26 [build-system] 21 27 requires = ["hatchling"] 22 28 build-backend = "hatchling.build"