hypruse 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. hypruse-0.1.0/.github/workflows/ci.yml +21 -0
  2. hypruse-0.1.0/.github/workflows/release.yml +54 -0
  3. hypruse-0.1.0/.gitignore +9 -0
  4. hypruse-0.1.0/ARCHITECTURE.md +68 -0
  5. hypruse-0.1.0/CHANGELOG.md +38 -0
  6. hypruse-0.1.0/CONTRIBUTING.md +35 -0
  7. hypruse-0.1.0/LICENSE +21 -0
  8. hypruse-0.1.0/PKG-INFO +189 -0
  9. hypruse-0.1.0/README.md +165 -0
  10. hypruse-0.1.0/RELEASING.md +53 -0
  11. hypruse-0.1.0/packaging/aur/hypruse/PKGBUILD +23 -0
  12. hypruse-0.1.0/packaging/aur/hypruse-git/PKGBUILD +36 -0
  13. hypruse-0.1.0/pyproject.toml +52 -0
  14. hypruse-0.1.0/scripts/e2e_input.py +104 -0
  15. hypruse-0.1.0/src/hypruse/__init__.py +3 -0
  16. hypruse-0.1.0/src/hypruse/__main__.py +3 -0
  17. hypruse-0.1.0/src/hypruse/hyprctl.py +125 -0
  18. hypruse-0.1.0/src/hypruse/input.py +199 -0
  19. hypruse-0.1.0/src/hypruse/safety.py +83 -0
  20. hypruse-0.1.0/src/hypruse/screenshot.py +203 -0
  21. hypruse-0.1.0/src/hypruse/server.py +275 -0
  22. hypruse-0.1.0/src/hypruse/session.py +58 -0
  23. hypruse-0.1.0/src/hypruse/wire.py +237 -0
  24. hypruse-0.1.0/tests/fixtures/desktop.json +89 -0
  25. hypruse-0.1.0/tests/test_e2e.py +50 -0
  26. hypruse-0.1.0/tests/test_fit.py +141 -0
  27. hypruse-0.1.0/tests/test_hyprctl.py +69 -0
  28. hypruse-0.1.0/tests/test_input.py +44 -0
  29. hypruse-0.1.0/tests/test_launch.py +83 -0
  30. hypruse-0.1.0/tests/test_mcp_roundtrip.py +79 -0
  31. hypruse-0.1.0/tests/test_prune.py +21 -0
  32. hypruse-0.1.0/tests/test_safety.py +28 -0
  33. hypruse-0.1.0/tests/test_screenshot.py +37 -0
  34. hypruse-0.1.0/tests/test_session.py +63 -0
  35. hypruse-0.1.0/tests/test_wire.py +61 -0
  36. hypruse-0.1.0/uv.lock +822 -0
  37. hypruse-0.1.0/waybar/README.md +46 -0
  38. hypruse-0.1.0/waybar/hypruse-status.sh +15 -0
@@ -0,0 +1,21 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.11", "3.14"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ - run: uv sync --group dev
20
+ - run: uv run ruff check .
21
+ - run: uv run pytest -q
@@ -0,0 +1,54 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI on a version tag via Trusted Publishing (OIDC) — no
4
+ # API token stored anywhere. One-time PyPI setup is documented in
5
+ # RELEASING.md (add a pending publisher for this repo + workflow).
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - run: uv build
18
+ - uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+
23
+ publish:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment: pypi
27
+ permissions:
28
+ id-token: write # OIDC token for Trusted Publishing
29
+ steps:
30
+ - uses: actions/download-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+ - uses: pypa/gh-action-pypi-publish@release/v1
35
+
36
+ github-release:
37
+ needs: publish
38
+ runs-on: ubuntu-latest
39
+ permissions:
40
+ contents: write
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: actions/download-artifact@v4
44
+ with:
45
+ name: dist
46
+ path: dist/
47
+ - name: Create GitHub release
48
+ env:
49
+ GH_TOKEN: ${{ github.token }}
50
+ run: >
51
+ gh release create "${GITHUB_REF_NAME}"
52
+ --title "${GITHUB_REF_NAME}"
53
+ --notes "See [CHANGELOG.md](CHANGELOG.md)."
54
+ dist/*
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
@@ -0,0 +1,68 @@
1
+ # Architecture
2
+
3
+ Ten-minute orientation for contributors.
4
+
5
+ ## Module map
6
+
7
+ ```
8
+ src/hypruse/
9
+ server.py MCP wiring: 6 tools, docstrings = the agent-facing API
10
+ hyprctl.py all Hyprland IPC (queries + dispatchers), state trimming
11
+ wire.py raw Wayland client for zwlr_virtual_pointer_v1
12
+ input.py pointer orchestration (movecursor + wire) and wtype keyboard
13
+ screenshot.py grim capture: monitor / window / region + coord metadata
14
+ session.py discovers HYPRLAND_INSTANCE_SIGNATURE / WAYLAND_DISPLAY
15
+ from runtime-dir sockets when the host stripped the env
16
+ safety.py activity beacon + kill-switch semantics
17
+ ```
18
+
19
+ Rule of thumb: `server.py` validates and narrates; everything real happens
20
+ in the leaf modules, which stay importable and testable without MCP.
21
+
22
+ ## The coordinate contract
23
+
24
+ One space rules everything: **Hyprland global logical coordinates** (what
25
+ `hyprctl cursorpos`, client `at`, and `dispatch movecursor` speak).
26
+
27
+ - `desktop` reports window geometry in it.
28
+ - `pointer` accepts it.
29
+ - `screenshot` captures *pixels* and returns `geometry` + `scale` per
30
+ capture so callers map back: `global = origin + pixel / scale`.
31
+
32
+ If you touch anything coordinate-adjacent, preserve this contract; it is
33
+ what keeps multi-monitor and fractional scaling tractable.
34
+
35
+ ## Why input works the way it does
36
+
37
+ - **Position** via `hyprctl dispatch movecursor x y` — authoritative,
38
+ global, no per-monitor extent math, immune to
39
+ [hyprwm/Hyprland#6749](https://github.com/hyprwm/Hyprland/issues/6749).
40
+ - **Buttons/axis** via a virtual pointer created over the raw wire
41
+ (`wire.py` is ~250 lines: registry scan, bind, button/axis/frame, sync
42
+ barrier, wl_display.error surfacing). No daemon, no uinput, no root.
43
+ - **Keyboard** via `wtype`: it uploads its own XKB keymap through
44
+ `zwp_virtual_keyboard_v1`, which is why unicode and non-US layouts work.
45
+ We shell out instead of reimplementing keymap upload — that wheel is
46
+ round already.
47
+
48
+ A press and its release always happen inside one tool call, which is what
49
+ makes `pkill -f hypruse` a safe panic action at any moment.
50
+
51
+ ## Sequence of a typical agent step
52
+
53
+ 1. `desktop` → find `firefox` at `0x…`, workspace 3, geometry.
54
+ 2. `hypr focus_window 0x…` (IPC, ~ms) — no vision spent.
55
+ 3. `screenshot window=0x…` → crop + `geometry`/`scale`.
56
+ 4. `pointer click x y` — computed from image pixel via the contract.
57
+ 5. `keyboard type "…"`.
58
+ 6. `desktop` again to verify the world changed as expected.
59
+
60
+ ## Testing tiers
61
+
62
+ 1. **Unit** (CI): pure functions — wire encoders/parsers, combo parsing,
63
+ region parsing, state trimming against fixtures.
64
+ 2. **Live seat-safe** (`pytest -m e2e`): real session, zero input events —
65
+ including a virtual-pointer create/destroy handshake.
66
+ 3. **Supervised** (`scripts/e2e_input.py`): the only tier that clicks and
67
+ types; countdown, self-verifying via kitty remote control, restores
68
+ focus. Never wire this into anything automatic.
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
+ [SemVer](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] — 2026-07-16
10
+
11
+ Initial release.
12
+
13
+ ### Added
14
+ - MCP stdio server with six tools: `desktop`, `screenshot`, `pointer`,
15
+ `keyboard`, `hypr`, `launch`, plus cross-tool guidance in the server
16
+ `instructions` and terse per-tool schemas for eager client loading.
17
+ - Semantic desktop snapshot over hyprctl IPC (token-lean, fixture-tested).
18
+ - Screenshots via grim: focused monitor, exact window crop, region zoom,
19
+ with true output dimensions and scale for exact pixel→global mapping.
20
+ Default returns a saved PNG path (works with hosts that mangle inline
21
+ image blocks); `HYPRUSE_SCREENSHOT_MODE=image` returns wire-level image
22
+ content auto-fit to a transport byte budget and long-edge cap
23
+ (`HYPRUSE_MAX_IMAGE_BYTES`, `HYPRUSE_MAX_IMAGE_EDGE`) by degrading format
24
+ before resolution, so the API never silently downscales under the model.
25
+ Coarse-to-fine clicking guidance to counter pixel-estimation error.
26
+ - Pointer input over a raw `zwlr_virtual_pointer_v1` wire client — no
27
+ ydotool, no uinput, no daemon; positioning via `hyprctl movecursor`.
28
+ - Keyboard input via wtype (XKB keymap upload; unicode/layout-correct),
29
+ with combo parsing (`ctrl+shift+t`, `super+enter`, bare-mod taps).
30
+ - Workspace/window dispatchers and app launch with new-window detection,
31
+ including single-instance apps (browsers) whose window is relocated to
32
+ the requested workspace.
33
+ - Session discovery: finds `HYPRLAND_INSTANCE_SIGNATURE` / `WAYLAND_DISPLAY`
34
+ from runtime sockets when the host launches the server with a stripped
35
+ environment (dbus/systemd-activated desktop apps).
36
+ - Activity beacon + Waybar kill-switch indicator module.
37
+ - Unit suite, seat-safe live e2e, MCP stdio round-trip tests, supervised
38
+ input verification script, CI.
@@ -0,0 +1,35 @@
1
+ # Contributing
2
+
3
+ Contributions are welcome — especially the roadmap items marked
4
+ `help wanted` (sway/niri support is the big one: `wire.py` already speaks
5
+ the wlr protocols; what's missing is an IPC layer equivalent to
6
+ `hyprctl.py` for those compositors).
7
+
8
+ ## Setup
9
+
10
+ ```sh
11
+ git clone https://github.com/IlyasKhallouki/hypruse
12
+ cd hypruse
13
+ uv sync --group dev
14
+ ```
15
+
16
+ ## Before you push
17
+
18
+ ```sh
19
+ uv run ruff check .
20
+ uv run pytest # must stay green without a compositor
21
+ ```
22
+
23
+ If your change touches live behaviour, also run `uv run pytest -m e2e
24
+ --override-ini addopts=` inside a Hyprland session, and
25
+ `scripts/e2e_input.py` if it touches input (supervised — it takes the
26
+ seat for ~10 seconds).
27
+
28
+ ## Ground rules
29
+
30
+ - Conventional commits (`feat:`, `fix:`, `docs:`, `test:`, `ci:`, `chore:`).
31
+ - Unit tests accompany features; anything pure gets tested pure.
32
+ - Never make input tests automatic. The supervised script stays supervised.
33
+ - Keep the coordinate contract (see ARCHITECTURE.md) intact.
34
+ - No new runtime dependencies without an issue discussion first — the
35
+ zero-daemon, near-zero-dep footprint is a feature.
hypruse-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilyas Khallouki
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
hypruse-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: hypruse
3
+ Version: 0.1.0
4
+ Summary: Computer use for Hyprland — an MCP server that gives AI agents native hands on your Wayland desktop
5
+ Project-URL: Homepage, https://github.com/IlyasKhallouki/hypruse
6
+ Project-URL: Issues, https://github.com/IlyasKhallouki/hypruse/issues
7
+ Author-email: Ilyas Khallouki <khalloukielias@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai-agents,automation,claude,computer-use,hyprland,mcp,wayland
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: X11 Applications
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Desktop Environment :: Window Managers
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: mcp>=1.2
23
+ Description-Content-Type: text/markdown
24
+
25
+ # hypruse
26
+
27
+ **Computer use for [Hyprland](https://hypr.land).** An [MCP](https://modelcontextprotocol.io) server that gives AI agents native hands on your Wayland desktop — workspaces, windows, mouse, keyboard, screenshots.
28
+
29
+ No ydotool daemon. No root. No portals. No X11.
30
+
31
+ <!-- demo: assets/demo.gif goes here before launch -->
32
+
33
+ ## Why
34
+
35
+ Computer use exists on macOS and Windows. On Linux there is effectively nothing: the Claude Desktop Linux beta explicitly ships **without** screen control, Anthropic's reference implementation is an X11 container, and the existing Wayland attempts lean on setuid uinput hacks or GNOME-only portals.
36
+
37
+ Meanwhile Hyprland already exposes everything an agent needs, better than any accessibility bridge: a complete IPC surface for state and window management, and first-class Wayland protocols for input. hypruse just wires them to MCP:
38
+
39
+ - **Semantic first.** `desktop` returns the real window/workspace tree — addresses, classes, titles, geometry — in one call. The agent switches workspaces and focuses windows the way you do (instantly, over IPC), not by squinting at pixels.
40
+ - **Vision when it matters.** Screenshots of a monitor, an exact window crop, or a zoomed region, with the geometry/scale metadata to map any pixel back to a clickable coordinate.
41
+ - **Native input.** Clicks and scrolls are spoken directly over the Wayland wire (`zwlr_virtual_pointer_v1`); typing goes through `wtype`'s virtual keyboard with a proper XKB keymap — unicode-safe on any layout.
42
+
43
+ ## How it works
44
+
45
+ ```
46
+ agent (Claude Code, or any MCP client)
47
+ │ stdio
48
+
49
+ hypruse
50
+ ├── hyprctl -j ········▶ desktop state: monitors, workspaces, windows
51
+ ├── hyprctl dispatch ··▶ focus / move / close / launch / movecursor
52
+ ├── grim ··············▶ screenshots: monitor, window crop, region
53
+ ├── wtype ·············▶ keyboard (zwp_virtual_keyboard_v1, real XKB keymap)
54
+ └── raw Wayland wire ··▶ click & scroll (zwlr_virtual_pointer_v1)
55
+ ```
56
+
57
+ Design choices, defended:
58
+
59
+ - **No ydotool / uinput.** That path needs a daemon, udev rules or root, and types US scancodes that break on other layouts. hypruse is just another Wayland client of your compositor — same standing as `wlrctl`.
60
+ - **No portals.** `xdg-desktop-portal-hyprland` does not implement the RemoteDesktop portal (InputCapture is capture, not injection), so anything built on libei/portals silently degrades on Hyprland. hypruse doesn't try.
61
+ - **Cursor positioning via `hyprctl dispatch movecursor`** (global logical coordinates, exact on any monitor layout), with only button/axis events on the virtual pointer — sidestepping the known multi-monitor bugs of absolute virtual-pointer motion ([hyprwm/Hyprland#6749](https://github.com/hyprwm/Hyprland/issues/6749)).
62
+
63
+ ## Tools
64
+
65
+ | tool | what it does |
66
+ |---|---|
67
+ | `desktop` | One-call semantic snapshot: monitors, workspaces, windows (address/class/title/geometry), active window, cursor |
68
+ | `screenshot` | Focused monitor, exact window crop by address, or `x,y,WxH` region — returns image + coordinate-mapping metadata |
69
+ | `pointer` | move / click / drag / scroll in global coordinates |
70
+ | `keyboard` | Type literal text (unicode-safe) or press combos: `ctrl+shift+t`, `super+enter`, `F5` |
71
+ | `hypr` | Switch workspace, focus/move/close windows, fullscreen, floating — pure IPC, milliseconds |
72
+ | `launch` | Start an app (optionally silent on another workspace), wait for its window, return its address — detects single-instance apps (browsers) whose window ignores exec rules, and moves it to the requested workspace |
73
+
74
+ ## Install
75
+
76
+ Requirements: Hyprland, `grim`, `wtype` (most Hyprland setups already have both), and [uv](https://docs.astral.sh/uv/).
77
+
78
+ Claude Code:
79
+
80
+ ```sh
81
+ claude mcp add -s user hypruse -- uvx hypruse
82
+ ```
83
+
84
+ From a source checkout:
85
+
86
+ ```sh
87
+ claude mcp add -s user hypruse -- uv run --directory /path/to/hypruse hypruse
88
+ ```
89
+
90
+ Any other MCP client: run `uvx hypruse` as a stdio server.
91
+
92
+ ### Claude Desktop (Linux beta)
93
+
94
+ The Linux beta ships **without** Anthropic's first-party computer use — but stdio MCP servers work in chat, which makes hypruse the workaround. In `~/.config/Claude/claude_desktop_config.json`:
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "hypruse": {
100
+ "command": "uvx",
101
+ "args": ["hypruse"],
102
+ "env": { "HYPRUSE_SCREENSHOT_MODE": "image" }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ Two Desktop-specific notes: use `image` mode (Desktop renders inline MCP images and has no file-read tool), and the app must run natively inside your Hyprland session so the server inherits `WAYLAND_DISPLAY`/`HYPRLAND_INSTANCE_SIGNATURE` — from a VM or container it cannot reach your compositor. If your Desktop install bypasses tool-approval prompts, treat the [Waybar indicator + panic keybind](waybar/) as mandatory, not optional.
109
+
110
+ ## Security model
111
+
112
+ Read this section before installing. **hypruse hands an agent your mouse, your keyboard, your screen contents, and an app launcher.** The layers that keep that sane:
113
+
114
+ 1. **Approval** — MCP clients gate tool calls. In Claude Code, allowlist the read-only tools (`desktop`, `screenshot`) and leave `pointer`/`keyboard`/`hypr`/`launch` on ask-first until you trust a workflow.
115
+ 2. **Visibility** — the server maintains an activity beacon (`$XDG_RUNTIME_DIR/hypruse/state.json`); the shipped [Waybar module](waybar/) is invisible when idle and shows 󰚩 while an agent has hands on your desktop.
116
+ 3. **Interruption** — click the indicator, or bind a panic key: `bind = SUPER SHIFT, BackSpace, exec, pkill -f hypruse`. Killing it mid-action is safe: button press/release pairs never span tool calls, so it cannot die holding a button.
117
+ 4. **The seat is shared.** There is one cursor and one keyboard focus, and Hyprland's focus-follows-mouse means a cursor move alone can retarget keystrokes. Don't type while an agent is driving — watch the indicator.
118
+ 5. **Scope** — stdio only (no network listener), no clipboard access, nothing persisted except the beacon. A screenshot sees everything visible: treat an agent session like screen sharing.
119
+
120
+ ## Performance
121
+
122
+ Measured on a live session (Hyprland 0.55, 1080p, 20 windows): `desktop`
123
+ ~30 ms, workspace/window dispatch ~10–20 ms, screenshots ~0.5 s. If tool
124
+ calls *feel* slow, it is almost certainly the MCP **approval prompt** in
125
+ front of each call, not the server — allowlist the tools you trust and the
126
+ latency disappears. Claude Code (`.claude/settings.json`):
127
+
128
+ ```jsonc
129
+ {
130
+ "permissions": {
131
+ "allow": [
132
+ "mcp__hypruse__desktop",
133
+ "mcp__hypruse__screenshot",
134
+ "mcp__hypruse__hypr"
135
+ // add pointer/keyboard/launch once you trust your workflows
136
+ ]
137
+ }
138
+ }
139
+ ```
140
+
141
+ ## Coordinates
142
+
143
+ Everything speaks Hyprland's global logical coordinates — the space `hyprctl cursorpos` and window `at` use. Screenshots are pixel-space; each capture returns `geometry` and `scale` so `global = origin + pixel / scale`. On scale 1.0 monitors (most setups) image pixels *are* global coordinates.
144
+
145
+ In image mode, captures automatically fit the host's result-size limit (Claude Desktop caps tool results at 1 MB): format degrades before resolution — native PNG, then full-res JPEG, then stepped downscale — because full-res JPEG reads UI text better than half-res PNG. The applied scale is folded into the returned metadata, so coordinate mapping stays exact; tune with `HYPRUSE_MAX_IMAGE_BYTES`, or pass `scale` for a deliberate zoom-out.
146
+
147
+ By default the screenshot tool writes a PNG under `$XDG_RUNTIME_DIR/hypruse/` and returns its path — MCP hosts with a file reader (Claude Code's `Read`) render it natively. This default exists because some hosts (Claude Code ≤ 2.1.x among them) serialize inline MCP image blocks to base64 *text* the model can't see; we verified this empirically rather than trusting the spec. `HYPRUSE_SCREENSHOT_MODE=image` switches to inline image content blocks for hosts that render them correctly.
148
+
149
+ ## Development
150
+
151
+ ```sh
152
+ uv sync --group dev
153
+ uv run pytest # unit tests, no compositor needed
154
+ uv run pytest -m e2e --override-ini addopts= # live seat-safe checks
155
+ uv run python scripts/e2e_input.py # supervised: takes the seat ~10s
156
+ ```
157
+
158
+ The input e2e is deliberately manual — it borrows your cursor and keyboard, counts down, proves click/scroll/type delivery by reading the target terminal's screen back over kitty remote control, and restores your focus.
159
+
160
+ ## Roadmap
161
+
162
+ **Now — distribution**
163
+ - [ ] PyPI (`uvx hypruse`) and AUR (`hypruse`, `hypruse-git`)
164
+ - [ ] Demo GIF, then `awesome-mcp-servers` / `awesome-hyprland` listings
165
+
166
+ **Next — trust & accuracy**
167
+ - [ ] `hypruse doctor` — first-run diagnostics (deps, session reachability, virtual-pointer handshake)
168
+ - [ ] Click-by-text via OCR (Tesseract) — click labels, not estimated pixels; works on any app
169
+ - [ ] Read-only mode — disable input tools for a safe first run
170
+ - [ ] Headless-Hyprland CI — real end-to-end tests in GitHub Actions
171
+
172
+ **Then — breadth & depth**
173
+ - [ ] sway / niri support — the wire client already speaks the wlr protocols; needs an IPC layer alongside `hyprctl.py` (PRs very welcome, `help wanted`)
174
+ - [ ] AT-SPI element tree — click by accessible name, read GTK/Qt UIs without vision
175
+ - [ ] Clipboard (`wl-clipboard`), settle / wait-for-stable, discrete-axis scroll
176
+ - [ ] Multi-monitor and fractional-scaling hardening
177
+
178
+ ## Alternatives, honestly
179
+
180
+ | project | approach | where it falls short on Hyprland |
181
+ |---|---|---|
182
+ | computer-use-linux | AT-SPI + portals, ydotool fallback | GNOME-first; RemoteDesktop portal is dead on Hyprland, screenshots portal-first |
183
+ | hyprmcp | hyprctl wrapper | no screenshots, no input |
184
+ | wayland-mcp (×2) | setuid evemu + bundled VLM | archived / stale; evemu needs elevated setup |
185
+ | Anthropic computer-use-demo | X11 + xdotool in Docker | a sandboxed demo, not your desktop |
186
+
187
+ ## License
188
+
189
+ [MIT](LICENSE)
@@ -0,0 +1,165 @@
1
+ # hypruse
2
+
3
+ **Computer use for [Hyprland](https://hypr.land).** An [MCP](https://modelcontextprotocol.io) server that gives AI agents native hands on your Wayland desktop — workspaces, windows, mouse, keyboard, screenshots.
4
+
5
+ No ydotool daemon. No root. No portals. No X11.
6
+
7
+ <!-- demo: assets/demo.gif goes here before launch -->
8
+
9
+ ## Why
10
+
11
+ Computer use exists on macOS and Windows. On Linux there is effectively nothing: the Claude Desktop Linux beta explicitly ships **without** screen control, Anthropic's reference implementation is an X11 container, and the existing Wayland attempts lean on setuid uinput hacks or GNOME-only portals.
12
+
13
+ Meanwhile Hyprland already exposes everything an agent needs, better than any accessibility bridge: a complete IPC surface for state and window management, and first-class Wayland protocols for input. hypruse just wires them to MCP:
14
+
15
+ - **Semantic first.** `desktop` returns the real window/workspace tree — addresses, classes, titles, geometry — in one call. The agent switches workspaces and focuses windows the way you do (instantly, over IPC), not by squinting at pixels.
16
+ - **Vision when it matters.** Screenshots of a monitor, an exact window crop, or a zoomed region, with the geometry/scale metadata to map any pixel back to a clickable coordinate.
17
+ - **Native input.** Clicks and scrolls are spoken directly over the Wayland wire (`zwlr_virtual_pointer_v1`); typing goes through `wtype`'s virtual keyboard with a proper XKB keymap — unicode-safe on any layout.
18
+
19
+ ## How it works
20
+
21
+ ```
22
+ agent (Claude Code, or any MCP client)
23
+ │ stdio
24
+
25
+ hypruse
26
+ ├── hyprctl -j ········▶ desktop state: monitors, workspaces, windows
27
+ ├── hyprctl dispatch ··▶ focus / move / close / launch / movecursor
28
+ ├── grim ··············▶ screenshots: monitor, window crop, region
29
+ ├── wtype ·············▶ keyboard (zwp_virtual_keyboard_v1, real XKB keymap)
30
+ └── raw Wayland wire ··▶ click & scroll (zwlr_virtual_pointer_v1)
31
+ ```
32
+
33
+ Design choices, defended:
34
+
35
+ - **No ydotool / uinput.** That path needs a daemon, udev rules or root, and types US scancodes that break on other layouts. hypruse is just another Wayland client of your compositor — same standing as `wlrctl`.
36
+ - **No portals.** `xdg-desktop-portal-hyprland` does not implement the RemoteDesktop portal (InputCapture is capture, not injection), so anything built on libei/portals silently degrades on Hyprland. hypruse doesn't try.
37
+ - **Cursor positioning via `hyprctl dispatch movecursor`** (global logical coordinates, exact on any monitor layout), with only button/axis events on the virtual pointer — sidestepping the known multi-monitor bugs of absolute virtual-pointer motion ([hyprwm/Hyprland#6749](https://github.com/hyprwm/Hyprland/issues/6749)).
38
+
39
+ ## Tools
40
+
41
+ | tool | what it does |
42
+ |---|---|
43
+ | `desktop` | One-call semantic snapshot: monitors, workspaces, windows (address/class/title/geometry), active window, cursor |
44
+ | `screenshot` | Focused monitor, exact window crop by address, or `x,y,WxH` region — returns image + coordinate-mapping metadata |
45
+ | `pointer` | move / click / drag / scroll in global coordinates |
46
+ | `keyboard` | Type literal text (unicode-safe) or press combos: `ctrl+shift+t`, `super+enter`, `F5` |
47
+ | `hypr` | Switch workspace, focus/move/close windows, fullscreen, floating — pure IPC, milliseconds |
48
+ | `launch` | Start an app (optionally silent on another workspace), wait for its window, return its address — detects single-instance apps (browsers) whose window ignores exec rules, and moves it to the requested workspace |
49
+
50
+ ## Install
51
+
52
+ Requirements: Hyprland, `grim`, `wtype` (most Hyprland setups already have both), and [uv](https://docs.astral.sh/uv/).
53
+
54
+ Claude Code:
55
+
56
+ ```sh
57
+ claude mcp add -s user hypruse -- uvx hypruse
58
+ ```
59
+
60
+ From a source checkout:
61
+
62
+ ```sh
63
+ claude mcp add -s user hypruse -- uv run --directory /path/to/hypruse hypruse
64
+ ```
65
+
66
+ Any other MCP client: run `uvx hypruse` as a stdio server.
67
+
68
+ ### Claude Desktop (Linux beta)
69
+
70
+ The Linux beta ships **without** Anthropic's first-party computer use — but stdio MCP servers work in chat, which makes hypruse the workaround. In `~/.config/Claude/claude_desktop_config.json`:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "hypruse": {
76
+ "command": "uvx",
77
+ "args": ["hypruse"],
78
+ "env": { "HYPRUSE_SCREENSHOT_MODE": "image" }
79
+ }
80
+ }
81
+ }
82
+ ```
83
+
84
+ Two Desktop-specific notes: use `image` mode (Desktop renders inline MCP images and has no file-read tool), and the app must run natively inside your Hyprland session so the server inherits `WAYLAND_DISPLAY`/`HYPRLAND_INSTANCE_SIGNATURE` — from a VM or container it cannot reach your compositor. If your Desktop install bypasses tool-approval prompts, treat the [Waybar indicator + panic keybind](waybar/) as mandatory, not optional.
85
+
86
+ ## Security model
87
+
88
+ Read this section before installing. **hypruse hands an agent your mouse, your keyboard, your screen contents, and an app launcher.** The layers that keep that sane:
89
+
90
+ 1. **Approval** — MCP clients gate tool calls. In Claude Code, allowlist the read-only tools (`desktop`, `screenshot`) and leave `pointer`/`keyboard`/`hypr`/`launch` on ask-first until you trust a workflow.
91
+ 2. **Visibility** — the server maintains an activity beacon (`$XDG_RUNTIME_DIR/hypruse/state.json`); the shipped [Waybar module](waybar/) is invisible when idle and shows 󰚩 while an agent has hands on your desktop.
92
+ 3. **Interruption** — click the indicator, or bind a panic key: `bind = SUPER SHIFT, BackSpace, exec, pkill -f hypruse`. Killing it mid-action is safe: button press/release pairs never span tool calls, so it cannot die holding a button.
93
+ 4. **The seat is shared.** There is one cursor and one keyboard focus, and Hyprland's focus-follows-mouse means a cursor move alone can retarget keystrokes. Don't type while an agent is driving — watch the indicator.
94
+ 5. **Scope** — stdio only (no network listener), no clipboard access, nothing persisted except the beacon. A screenshot sees everything visible: treat an agent session like screen sharing.
95
+
96
+ ## Performance
97
+
98
+ Measured on a live session (Hyprland 0.55, 1080p, 20 windows): `desktop`
99
+ ~30 ms, workspace/window dispatch ~10–20 ms, screenshots ~0.5 s. If tool
100
+ calls *feel* slow, it is almost certainly the MCP **approval prompt** in
101
+ front of each call, not the server — allowlist the tools you trust and the
102
+ latency disappears. Claude Code (`.claude/settings.json`):
103
+
104
+ ```jsonc
105
+ {
106
+ "permissions": {
107
+ "allow": [
108
+ "mcp__hypruse__desktop",
109
+ "mcp__hypruse__screenshot",
110
+ "mcp__hypruse__hypr"
111
+ // add pointer/keyboard/launch once you trust your workflows
112
+ ]
113
+ }
114
+ }
115
+ ```
116
+
117
+ ## Coordinates
118
+
119
+ Everything speaks Hyprland's global logical coordinates — the space `hyprctl cursorpos` and window `at` use. Screenshots are pixel-space; each capture returns `geometry` and `scale` so `global = origin + pixel / scale`. On scale 1.0 monitors (most setups) image pixels *are* global coordinates.
120
+
121
+ In image mode, captures automatically fit the host's result-size limit (Claude Desktop caps tool results at 1 MB): format degrades before resolution — native PNG, then full-res JPEG, then stepped downscale — because full-res JPEG reads UI text better than half-res PNG. The applied scale is folded into the returned metadata, so coordinate mapping stays exact; tune with `HYPRUSE_MAX_IMAGE_BYTES`, or pass `scale` for a deliberate zoom-out.
122
+
123
+ By default the screenshot tool writes a PNG under `$XDG_RUNTIME_DIR/hypruse/` and returns its path — MCP hosts with a file reader (Claude Code's `Read`) render it natively. This default exists because some hosts (Claude Code ≤ 2.1.x among them) serialize inline MCP image blocks to base64 *text* the model can't see; we verified this empirically rather than trusting the spec. `HYPRUSE_SCREENSHOT_MODE=image` switches to inline image content blocks for hosts that render them correctly.
124
+
125
+ ## Development
126
+
127
+ ```sh
128
+ uv sync --group dev
129
+ uv run pytest # unit tests, no compositor needed
130
+ uv run pytest -m e2e --override-ini addopts= # live seat-safe checks
131
+ uv run python scripts/e2e_input.py # supervised: takes the seat ~10s
132
+ ```
133
+
134
+ The input e2e is deliberately manual — it borrows your cursor and keyboard, counts down, proves click/scroll/type delivery by reading the target terminal's screen back over kitty remote control, and restores your focus.
135
+
136
+ ## Roadmap
137
+
138
+ **Now — distribution**
139
+ - [ ] PyPI (`uvx hypruse`) and AUR (`hypruse`, `hypruse-git`)
140
+ - [ ] Demo GIF, then `awesome-mcp-servers` / `awesome-hyprland` listings
141
+
142
+ **Next — trust & accuracy**
143
+ - [ ] `hypruse doctor` — first-run diagnostics (deps, session reachability, virtual-pointer handshake)
144
+ - [ ] Click-by-text via OCR (Tesseract) — click labels, not estimated pixels; works on any app
145
+ - [ ] Read-only mode — disable input tools for a safe first run
146
+ - [ ] Headless-Hyprland CI — real end-to-end tests in GitHub Actions
147
+
148
+ **Then — breadth & depth**
149
+ - [ ] sway / niri support — the wire client already speaks the wlr protocols; needs an IPC layer alongside `hyprctl.py` (PRs very welcome, `help wanted`)
150
+ - [ ] AT-SPI element tree — click by accessible name, read GTK/Qt UIs without vision
151
+ - [ ] Clipboard (`wl-clipboard`), settle / wait-for-stable, discrete-axis scroll
152
+ - [ ] Multi-monitor and fractional-scaling hardening
153
+
154
+ ## Alternatives, honestly
155
+
156
+ | project | approach | where it falls short on Hyprland |
157
+ |---|---|---|
158
+ | computer-use-linux | AT-SPI + portals, ydotool fallback | GNOME-first; RemoteDesktop portal is dead on Hyprland, screenshots portal-first |
159
+ | hyprmcp | hyprctl wrapper | no screenshots, no input |
160
+ | wayland-mcp (×2) | setuid evemu + bundled VLM | archived / stale; evemu needs elevated setup |
161
+ | Anthropic computer-use-demo | X11 + xdotool in Docker | a sandboxed demo, not your desktop |
162
+
163
+ ## License
164
+
165
+ [MIT](LICENSE)
@@ -0,0 +1,53 @@
1
+ # Releasing
2
+
3
+ The launch and every release after it. Steps marked **[you]** need your
4
+ accounts/credentials; the rest is automated.
5
+
6
+ ## One-time setup
7
+
8
+ 1. **[you] Make the repo public** on GitHub (Settings → General → Danger
9
+ Zone). The awesome-list PRs and AUR source tarballs depend on this.
10
+ 2. **[you] PyPI Trusted Publisher** — on PyPI, add a *pending* publisher so
11
+ the first `release` workflow run can publish without a token
12
+ (https://pypi.org/manage/account/publishing/):
13
+ - Project: `hypruse`
14
+ - Owner: `IlyasKhallouki` · Repo: `hypruse`
15
+ - Workflow: `release.yml` · Environment: `pypi`
16
+ 3. **[you] AUR SSH key** — add your public key at
17
+ https://aur.archlinux.org/account, if not already done.
18
+
19
+ ## Cutting a release
20
+
21
+ 1. Bump `version` in `pyproject.toml` and move the CHANGELOG `[Unreleased]`
22
+ entries under the new version.
23
+ 2. Tag and push:
24
+ ```sh
25
+ git tag -a v0.1.0 -m "v0.1.0"
26
+ git push origin v0.1.0
27
+ ```
28
+ The `release` workflow builds, publishes to PyPI via OIDC, and cuts a
29
+ GitHub release with the artifacts.
30
+ 3. Verify: `uvx hypruse --version`.
31
+
32
+ ## AUR
33
+
34
+ Stable (`packaging/aur/hypruse/`), after the tag exists:
35
+
36
+ ```sh
37
+ cd packaging/aur/hypruse
38
+ updpkgsums # fills the real sha256 for the tag tarball
39
+ makepkg --printsrcinfo > .SRCINFO
40
+ # push to the AUR remote:
41
+ # git clone ssh://aur@aur.archlinux.org/hypruse.git
42
+ # copy PKGBUILD + .SRCINFO in, commit, push
43
+ ```
44
+
45
+ `hypruse-git` never needs `updpkgsums` (VCS source); regenerate its
46
+ `.SRCINFO` the same way and push to `ssh://aur@aur.archlinux.org/hypruse-git.git`.
47
+
48
+ ## Listings (after public + first release)
49
+
50
+ - `awesome-mcp-servers` and `awesome-hyprland` — PR one entry each.
51
+ - The MCP registry.
52
+
53
+ Test a PKGBUILD locally before pushing: `makepkg -si` in its directory.