deskpilot 0.2.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 (36) hide show
  1. deskpilot-0.2.0/.claude-plugin/marketplace.json +17 -0
  2. deskpilot-0.2.0/.claude-plugin/plugin.json +13 -0
  3. deskpilot-0.2.0/.github/ISSUE_TEMPLATE/bug.yml +29 -0
  4. deskpilot-0.2.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  5. deskpilot-0.2.0/.github/ISSUE_TEMPLATE/feature.yml +16 -0
  6. deskpilot-0.2.0/.github/dependabot.yml +20 -0
  7. deskpilot-0.2.0/.github/pull_request_template.md +13 -0
  8. deskpilot-0.2.0/.github/workflows/ci.yml +99 -0
  9. deskpilot-0.2.0/.github/workflows/release.yml +75 -0
  10. deskpilot-0.2.0/.github/zizmor.yml +5 -0
  11. deskpilot-0.2.0/.gitignore +5 -0
  12. deskpilot-0.2.0/.mergepatrol/mergepatrol.yml +20 -0
  13. deskpilot-0.2.0/AGENTS.md +50 -0
  14. deskpilot-0.2.0/CHANGELOG.md +58 -0
  15. deskpilot-0.2.0/CONTRIBUTING.md +33 -0
  16. deskpilot-0.2.0/LICENSE +21 -0
  17. deskpilot-0.2.0/PKG-INFO +131 -0
  18. deskpilot-0.2.0/README.md +114 -0
  19. deskpilot-0.2.0/SECURITY.md +21 -0
  20. deskpilot-0.2.0/pyproject.toml +47 -0
  21. deskpilot-0.2.0/scripts/changelog.py +53 -0
  22. deskpilot-0.2.0/skills/deskpilot/SKILL.md +56 -0
  23. deskpilot-0.2.0/src/deskpilot/__init__.py +1 -0
  24. deskpilot-0.2.0/src/deskpilot/cli.py +173 -0
  25. deskpilot-0.2.0/src/deskpilot/desk.py +221 -0
  26. deskpilot-0.2.0/src/deskpilot/hypr.py +222 -0
  27. deskpilot-0.2.0/src/deskpilot/keys.py +76 -0
  28. deskpilot-0.2.0/src/deskpilot/mcp_server.py +119 -0
  29. deskpilot-0.2.0/src/deskpilot/overlay.py +105 -0
  30. deskpilot-0.2.0/src/deskpilot/plugin/Makefile +14 -0
  31. deskpilot-0.2.0/src/deskpilot/plugin/deskpilot.cpp +1115 -0
  32. deskpilot-0.2.0/src/deskpilot/plugin.py +181 -0
  33. deskpilot-0.2.0/src/deskpilot/screen.py +138 -0
  34. deskpilot-0.2.0/src/deskpilot/uinput.py +96 -0
  35. deskpilot-0.2.0/tests/test_core.py +447 -0
  36. deskpilot-0.2.0/uv.lock +919 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "deskpilot",
4
+ "description": "deskpilot: desktop control for agents on Hyprland, without taking the user's pointer.",
5
+ "owner": {
6
+ "name": "Cillian Berragan",
7
+ "url": "https://github.com/cjber"
8
+ },
9
+ "plugins": [
10
+ {
11
+ "name": "deskpilot",
12
+ "description": "Click, scroll and type in Hyprland windows from a screenshot, in Wayland, XWayland and Wine apps, while the user keeps their pointer and focus.",
13
+ "source": "./",
14
+ "category": "productivity"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "deskpilot",
3
+ "version": "0.2.0",
4
+ "description": "Desktop control for agents on Hyprland: screenshot a window, then click, scroll and type at points in it while the user keeps their own pointer and keyboard focus. Works in native Wayland, XWayland and Wine apps, games included.",
5
+ "author": {
6
+ "name": "Cillian Berragan",
7
+ "url": "https://github.com/cjber"
8
+ },
9
+ "homepage": "https://github.com/cjber/deskpilot",
10
+ "repository": "https://github.com/cjber/deskpilot",
11
+ "license": "MIT",
12
+ "keywords": ["agent-skills", "hyprland", "wayland", "desktop-automation", "computer-use", "mcp"]
13
+ }
@@ -0,0 +1,29 @@
1
+ name: Bug
2
+ description: deskpilot did the wrong thing
3
+ labels: [bug]
4
+ body:
5
+ - type: textarea
6
+ id: what
7
+ attributes:
8
+ label: What happened
9
+ description: The command you ran, what you expected, and what happened instead. A click or key that reached the wrong window is the most serious kind of bug, so say so.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: window
14
+ attributes:
15
+ label: Target window
16
+ description: The window's entry from `deskpilot --json windows`, and whether it is native Wayland, XWayland or Wine.
17
+ render: json
18
+ - type: input
19
+ id: hyprland
20
+ attributes:
21
+ label: Hyprland version
22
+ description: Output of `hyprctl version | head -1`.
23
+ validations:
24
+ required: true
25
+ - type: input
26
+ id: plugin
27
+ attributes:
28
+ label: Plugin status
29
+ description: Output of `deskpilot plugin status`.
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Security issue
4
+ url: https://github.com/cjber/deskpilot/security/advisories/new
5
+ about: Report vulnerabilities privately.
@@ -0,0 +1,16 @@
1
+ name: Feature
2
+ description: Suggest an action or a change
3
+ labels: [enhancement]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem
9
+ description: What an agent cannot do today, and in which app.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: user
14
+ attributes:
15
+ label: What the user must keep
16
+ description: How the user's own pointer, focus and typing should be affected while it runs.
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: uv
4
+ directory: /
5
+ schedule:
6
+ interval: daily
7
+ cooldown:
8
+ default-days: 1
9
+ groups:
10
+ dependencies:
11
+ patterns: ["*"]
12
+ - package-ecosystem: github-actions
13
+ directory: /
14
+ schedule:
15
+ interval: daily
16
+ cooldown:
17
+ default-days: 1
18
+ groups:
19
+ actions:
20
+ patterns: ["*"]
@@ -0,0 +1,13 @@
1
+ ## What changed and why
2
+
3
+ <!-- One or two sentences. Link the issue it fixes: "Fixes #123". -->
4
+
5
+ ## Evidence
6
+
7
+ <!-- How you checked it. For a change to clicks or keys, name the clients you tried (Wayland, XWayland, Wine). -->
8
+
9
+ ## Checklist
10
+
11
+ - [ ] The checks in `AGENTS.md` pass
12
+ - [ ] `CHANGELOG.md` has a line under Unreleased, if a user would notice the change
13
+ - [ ] `README.md` and `skills/deskpilot/SKILL.md` match any command or flag change
@@ -0,0 +1,99 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ lint:
17
+ name: Lint and types
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ fetch-depth: 0
24
+ persist-credentials: false
25
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
26
+ with:
27
+ version: "0.12.18"
28
+ python-version: "3.13"
29
+ enable-cache: false
30
+ - run: uv sync --locked
31
+ - run: uv run --no-sync ruff check .
32
+ - run: uv run --no-sync ruff format --check .
33
+ - run: uv run --no-sync ty check
34
+ - name: Check CHANGELOG.md
35
+ run: python3 scripts/changelog.py --check
36
+
37
+ test:
38
+ name: Test Python ${{ matrix.python }}
39
+ runs-on: ubuntu-latest
40
+ timeout-minutes: 10
41
+ strategy:
42
+ matrix:
43
+ python: ["3.12", "3.14"]
44
+ steps:
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ with:
47
+ persist-credentials: false
48
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
49
+ with:
50
+ version: "0.12.18"
51
+ python-version: ${{ matrix.python }}
52
+ enable-cache: false
53
+ - run: uv sync --locked
54
+ - run: uv run --no-sync pytest -q
55
+
56
+ workflows:
57
+ name: Workflow checks
58
+ runs-on: ubuntu-latest
59
+ timeout-minutes: 10
60
+ steps:
61
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
62
+ with:
63
+ persist-credentials: false
64
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
65
+ with:
66
+ version: "0.12.18"
67
+ enable-cache: false
68
+ - name: Check workflow syntax
69
+ env:
70
+ ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
71
+ run: |
72
+ curl --fail --silent --show-error --location \
73
+ https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz \
74
+ --output "$RUNNER_TEMP/actionlint.tar.gz"
75
+ printf '%s %s\n' "$ACTIONLINT_SHA256" "$RUNNER_TEMP/actionlint.tar.gz" | sha256sum --check
76
+ tar -xzf "$RUNNER_TEMP/actionlint.tar.gz" -C "$RUNNER_TEMP" actionlint
77
+ "$RUNNER_TEMP/actionlint" -color
78
+ - name: Check workflow security
79
+ run: uvx zizmor==1.30.1 --offline --pedantic .github/workflows
80
+
81
+ secrets:
82
+ name: Secret scanning
83
+ runs-on: ubuntu-latest
84
+ timeout-minutes: 10
85
+ steps:
86
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
87
+ with:
88
+ persist-credentials: false
89
+ fetch-depth: 0
90
+ - name: Scan repository history
91
+ env:
92
+ GITLEAKS_SHA256: 551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
93
+ run: |
94
+ curl --fail --silent --show-error --location \
95
+ https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \
96
+ --output "$RUNNER_TEMP/gitleaks.tar.gz"
97
+ printf '%s %s\n' "$GITLEAKS_SHA256" "$RUNNER_TEMP/gitleaks.tar.gz" | sha256sum --check
98
+ tar -xzf "$RUNNER_TEMP/gitleaks.tar.gz" -C "$RUNNER_TEMP" gitleaks
99
+ "$RUNNER_TEMP/gitleaks" git --redact --no-banner --log-opts="--all" .
@@ -0,0 +1,75 @@
1
+ name: Release
2
+
3
+ # Pushing a vX.Y.Z tag publishes a GitHub release whose notes are that version's CHANGELOG.md entry, then uploads
4
+ # the package to PyPI through trusted publishing. The tag must be on main, where CI has already checked the commit.
5
+ on:
6
+ push:
7
+ tags: ["v*.*.*"]
8
+
9
+ permissions: {}
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ jobs:
16
+ release:
17
+ name: GitHub release
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ permissions:
21
+ contents: write # create the release
22
+ steps:
23
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
24
+ with:
25
+ fetch-depth: 0
26
+ persist-credentials: false
27
+
28
+ - name: Check the tag is on main and matches both versions
29
+ run: |
30
+ set -euo pipefail
31
+ git merge-base --is-ancestor "$GITHUB_SHA" origin/main
32
+ version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
33
+ plugin=$(jq -r .version .claude-plugin/plugin.json)
34
+ if [ "v$version" != "$GITHUB_REF_NAME" ] || [ "$plugin" != "$version" ]; then
35
+ echo "::error::tag $GITHUB_REF_NAME, pyproject.toml $version and plugin.json $plugin must match"
36
+ exit 1
37
+ fi
38
+ echo "VERSION=$version" >> "$GITHUB_ENV"
39
+
40
+ - name: GitHub release
41
+ env:
42
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43
+ run: |
44
+ set -euo pipefail
45
+ python3 scripts/changelog.py "$VERSION" > "$RUNNER_TEMP/notes.md"
46
+ # A rerun finds the release already there and leaves it.
47
+ if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
48
+ gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md"
49
+ fi
50
+
51
+ pypi:
52
+ name: PyPI
53
+ needs: release
54
+ runs-on: ubuntu-latest
55
+ timeout-minutes: 10
56
+ environment:
57
+ name: pypi
58
+ url: https://pypi.org/project/deskpilot/
59
+ permissions:
60
+ id-token: write # trusted publishing
61
+ steps:
62
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
63
+ with:
64
+ persist-credentials: false
65
+
66
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
67
+ with:
68
+ version: "0.12.18"
69
+ enable-cache: false
70
+
71
+ - name: Build
72
+ run: uv build
73
+
74
+ - name: Publish
75
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,5 @@
1
+ rules:
2
+ # Dependabot waits one day before proposing a release; zizmor's default floor is seven.
3
+ dependabot-cooldown:
4
+ config:
5
+ days: 1
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .ruff_cache/
@@ -0,0 +1,20 @@
1
+ # Mergepatrol reads this file from the default branch. The dashboard opens a PR to change it.
2
+ version: 1
3
+ label: mergepatrol
4
+ batch_size: 4
5
+ resolver:
6
+ provider: anthropic
7
+ model: claude-opus-5-5
8
+ effort: medium
9
+ # Run in Mergepatrol's sandbox after an agent resolves a conflict or commits a fix, before anything is pushed.
10
+ # They mirror CI's lint and test jobs. The job image has no uv, so the first command installs the version CI uses.
11
+ verify:
12
+ - curl -LsSf https://astral.sh/uv/0.12.18/install.sh | sh
13
+ - ~/.local/bin/uv sync --locked
14
+ - ~/.local/bin/uv run --no-sync ruff check .
15
+ - ~/.local/bin/uv run --no-sync ruff format --check .
16
+ - ~/.local/bin/uv run --no-sync ty check
17
+ - ~/.local/bin/uv run --no-sync pytest -q
18
+ fix_ci:
19
+ enabled: true
20
+ attempts: 2
@@ -0,0 +1,50 @@
1
+ # deskpilot
2
+
3
+ Desktop control for agents on Hyprland: screenshot a window, click or type at a point in it, and leave the user's
4
+ own pointer and keyboard focus where they were. The CLI and the MCP server are thin layers over the same `Desk`.
5
+
6
+ ## Checks
7
+
8
+ ```sh
9
+ uv sync --locked
10
+ uv run ruff check . && uv run ruff format --check .
11
+ uv run ty check
12
+ uv run pytest
13
+ python3 scripts/changelog.py --check
14
+ claude plugin validate --strict . # if the Claude Code CLI is installed
15
+ ```
16
+
17
+ CI also runs actionlint, zizmor and gitleaks. The plugin is not built in CI: it needs the exact Hyprland it loads
18
+ into, so check a change to `plugin/deskpilot.cpp` with `deskpilot plugin load` on a real session.
19
+
20
+ ## Layout
21
+
22
+ - `src/deskpilot/cli.py`: argument parsing and output; every command calls `Desk`
23
+ - `src/deskpilot/mcp_server.py`: the same actions as MCP tools over stdio, one `Desk` for the session
24
+ - `src/deskpilot/desk.py`: the actions an agent takes, shared by the CLI and the MCP server
25
+ - `src/deskpilot/hypr.py`: hyprctl: windows, monitors, focus, the cursor, and picking the live instance
26
+ - `src/deskpilot/plugin.py`: builds, loads and talks to the compositor plugin
27
+ - `src/deskpilot/plugin/deskpilot.cpp`: the Hyprland plugin that runs each click, scroll and keystroke inside the
28
+ compositor and puts the user's pointer and focus back
29
+ - `src/deskpilot/screen.py`: grim toplevel screenshots, shot ids, and the mapping from shot pixels back to the screen
30
+ - `src/deskpilot/overlay.py`: the agent cursor, drawn by `cua-driver`
31
+ - `src/deskpilot/uinput.py`: the virtual mouse and keyboard behind `--take-pointer` and `--take-focus`
32
+ - `src/deskpilot/keys.py`: key names, chords and US-layout text to Linux key codes
33
+
34
+ ## Rules
35
+
36
+ - The user's pointer and focus are theirs. Only `--take-pointer`, `--take-focus` and `--show` may move them; every
37
+ other action goes through the plugin, which must restore both and replay input the user made meanwhile. A window
38
+ that is not on screen is an error, never a reason to focus it.
39
+ - A point is resolved against a recorded shot. A shot is of one window at one size; any other use is refused.
40
+ - A plugin build loads only into the Hyprland it was built against. Builds are named after the Hyprland commit and
41
+ the plugin source hash, so a changed source or an upgrade rebuilds rather than reusing a stale build.
42
+ - Each dispatcher tries Hyprland 0.57's Lua form first and falls back to the string form. Keep both working.
43
+ - A window name that matches several windows is an error listing them, and a point outside the window or under
44
+ another window is refused. Never guess a target.
45
+ - The agent cursor is feedback, never input: any overlay failure turns it off and the action goes ahead.
46
+ - Text is typed on a US layout. A character it cannot type is an error, never skipped.
47
+ - Tests never touch the real compositor, `/dev/uinput`, `grim` or `cua-driver`. Pass fakes into `Desk` and
48
+ `Hyprland`, as `tests/test_core.py` does.
49
+ - Every user-visible change gets a line under `## [Unreleased]` in `CHANGELOG.md`, and a command or flag change
50
+ updates `README.md` and `skills/deskpilot/SKILL.md` too.
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ What changed in each release, in the terms someone using deskpilot would notice. Dates are UTC.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions follow
6
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Each version's entry is also its GitHub release notes.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-11-10
11
+
12
+ - **Install from PyPI.** Each release is published to PyPI, so `uv tool install deskpilot` installs it.
13
+ - **Windows off screen are refused, not focused.** `shot`, `click`, `move` and `scroll` on a window on a hidden
14
+ workspace or scrolled off its monitor used to focus it, switching your workspace. They now refuse it, and
15
+ `--show` brings it into view on request. `windows` reports `visible` for each window.
16
+ - **Screenshots of the window itself.** `shot` captures the window by its toplevel, so a window on top no longer
17
+ shows in the image, and reads its scale from the image, so scaled outputs map clicks correctly.
18
+ - **Shot ids.** Each `shot` has an id; `--shot ID` (or `shot` over MCP) ties a click to that image, and a window
19
+ resized since is refused instead of clicked in the wrong place. The MCP `shot` tool returns the id with the image.
20
+ - **MCP errors say why.** A refused or failed action over MCP returned only `Error executing tool <name>`; it now
21
+ returns the reason, such as the window covering the point or the names an ambiguous window matched.
22
+ - **The plugin loads itself.** The first action after Hyprland restarts or upgrades builds and loads the plugin.
23
+ - **Clicks keep your keyboard focus.** A click or scroll no longer focuses or raises the target window while it runs,
24
+ so keys you type meanwhile stay in your own window. An XWayland app that asks to be activated when clicked is not
25
+ given focus while deskpilot acts on it.
26
+ - **Agents can act at the same time.** Clicks, scrolls and keys from several callers queue and run in turn instead of
27
+ failing with `busy`. Up to 32 wait at once; past that an action is refused with a clear error.
28
+ - **Typed text arrives whole.** `deskpilot type` and the MCP `type_text` send the text as one action, so another
29
+ agent's keys never land in the middle of it, and long text works. `--delay` now sets how long each key is held as
30
+ well as the gap before the next.
31
+ - **Typing leaves your window alone.** Keys go only to the target app, so the window you are using no longer sees
32
+ its keyboard focus leave and return, and its pointer no longer flickers while an agent types.
33
+ - **Scrolling ignores your compositor settings.** A scroll reaches the app as plain wheel notches, without your
34
+ Hyprland mouse binds or scroll factors acting on it.
35
+ - **Clicks check what is on top before every press.** Each press and wheel notch asks Hyprland what is at the point,
36
+ so a window, bar or popup that appears mid-click is refused instead of clicked.
37
+
38
+ ## [0.1.0] - 2026-11-09
39
+
40
+ - **Clicks that leave your pointer alone.** `deskpilot click`, `scroll` and keys sent with `--window` run inside
41
+ Hyprland through the deskpilot plugin: your pointer and keyboard focus go back where they were, and any mouse
42
+ motion you made meanwhile is applied. Native Wayland, XWayland and Wine windows all receive a real click.
43
+ - **Screenshot coordinates.** `deskpilot shot` saves a window's screenshot and remembers its scale, so `click`,
44
+ `move` and `scroll` take a point straight from the image. A point outside the window, or under another window, is
45
+ refused.
46
+ - **No guessed windows.** A window is named by address, class, or part of its class or title, and a name that
47
+ matches several windows is an error listing them.
48
+ - **Typing.** `deskpilot type` and `deskpilot key` type US-layout text and chords. `--delay` spaces keys for games,
49
+ which read input once per frame.
50
+ - **Taking over when an app needs it.** `--take-pointer` and `--take-focus` move your own pointer or focus and press
51
+ through a virtual input device, for a hover the app must see or an app that ignores keys while unfocused.
52
+ - **An agent cursor.** With `cua-driver` installed, an overlay arrow glides to each point deskpilot acts on.
53
+ - **Plugin management.** `deskpilot plugin load` builds the plugin for the running Hyprland and loads it; run it
54
+ again after each Hyprland upgrade.
55
+ - **MCP server.** `deskpilot mcp` serves the same actions over stdio, keeping one input device and one agent cursor
56
+ open for the session.
57
+ - **Works with Hyprland 0.57 and older.** Each dispatcher tries the Lua form first and falls back to the string
58
+ form, and a shell with a stale `HYPRLAND_INSTANCE_SIGNATURE` reaches the one live Hyprland.
@@ -0,0 +1,33 @@
1
+ # Contributing
2
+
3
+ Bug reports and pull requests are welcome. [AGENTS.md](AGENTS.md) has the layout and the rules a change must keep.
4
+
5
+ ## Before you start
6
+
7
+ - **Bugs:** open an issue with the command you ran, the target window from `deskpilot --json windows`, your
8
+ Hyprland version and `deskpilot plugin status`. A click or key that reached the wrong window is the most serious
9
+ kind of bug, so say so in the title.
10
+ - **New actions and larger changes:** open an issue first, so we can agree how the user's pointer and focus are
11
+ kept.
12
+ - **Security issues:** report them privately, as [SECURITY.md](SECURITY.md) describes.
13
+
14
+ ## Making a change
15
+
16
+ Run the checks in [AGENTS.md](AGENTS.md#checks). A change needs:
17
+
18
+ - passing checks
19
+ - a test for each new refusal, including the case that must refuse
20
+ - a line under Unreleased in `CHANGELOG.md` if a user would notice it
21
+ - matching updates to `README.md` and `skills/deskpilot/SKILL.md` if a command or flag changed
22
+
23
+ Tests must never touch the real desktop. Pass fakes into `Desk` and `Hyprland` as `tests/test_core.py` does. A change
24
+ to the plugin also needs a manual check on a running Hyprland, with the clients you tried named in the PR.
25
+
26
+ Keep PRs to one idea and use [Conventional Commits](https://www.conventionalcommits.org/) for titles.
27
+
28
+ ## Releasing
29
+
30
+ Bump `version` in `pyproject.toml` and `.claude-plugin/plugin.json`, run `uv lock`, move the Unreleased lines under
31
+ a new `## [X.Y.Z] - YYYY-MM-DD` heading, and merge. Then tag the merged commit `vX.Y.Z` and push the tag. The release
32
+ workflow checks the tag is on main and matches both versions, and publishes a GitHub release whose notes are that
33
+ version's changelog entry.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cillian Berragan
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.
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: deskpilot
3
+ Version: 0.2.0
4
+ Summary: Desktop control for agents on Hyprland: window-relative clicks that leave the user's pointer alone, and typing
5
+ Project-URL: Homepage, https://github.com/cjber/deskpilot
6
+ Project-URL: Repository, https://github.com/cjber/deskpilot
7
+ Project-URL: Issues, https://github.com/cjber/deskpilot/issues
8
+ Project-URL: Changelog, https://github.com/cjber/deskpilot/blob/main/CHANGELOG.md
9
+ Author-email: Cillian Berragan <cjberragan@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: evdev>=1.7
14
+ Requires-Dist: mcp>=2.3
15
+ Requires-Dist: pillow>=10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # deskpilot
19
+
20
+ Desktop control for agents on Hyprland. An agent takes a screenshot of a window, picks a point in it, and clicks
21
+ or types there, in any app: native Wayland, XWayland and Wine programs such as the Battle.net launcher and games.
22
+
23
+ The user keeps their own pointer and keyboard focus. Clicks, scrolls and keys run inside the compositor through
24
+ the deskpilot Hyprland plugin: in one step it holds the user's mouse input, delivers the event to the target window
25
+ through the user's own seat, then puts the pointer back where it was, applying any mouse motion the user made
26
+ meanwhile. Only pointer focus moves, so keyboard focus stays on the user's window and the target is not raised, even
27
+ with `focus_on_activate` set. Wayland, XWayland and Wine clients all receive the press as a real click. The plugin refuses
28
+ a click when another window covers the point.
29
+
30
+ An agent cursor shows where deskpilot acts: [cua-driver](https://github.com/trycua/cua)'s overlay arrow glides to
31
+ each point, drawn above every window and separate from the user's pointer. Without `cua-driver` on `PATH`, or with
32
+ `DESKPILOT_CURSOR=off`, actions run without it.
33
+
34
+ Keys sent to a window (`--window`) go to that window only, with each stroke complete before focus returns, and
35
+ the user's modifiers never leak into them. X11 apps take keys only from the window with X input focus, so an
36
+ XWayland target gets X focus for each stroke and sees a focus-in and focus-out around it; Hyprland's own focus
37
+ does not move.
38
+
39
+ `--take-pointer` and `--take-focus` use a virtual input device made with `/dev/uinput` instead, which the
40
+ compositor treats as real hardware. Tools that synthesize input another way, such as `xdotool` (XTest) or `wtype`
41
+ (the virtual keyboard protocol), are ignored by some clients: Battle.net drops `xdotool` clicks, and a game drops
42
+ `wtype` keys.
43
+
44
+ ## Requirements
45
+
46
+ - Hyprland, with `hyprctl` on `PATH`. Version 0.57's Lua dispatchers and the older string ones both work.
47
+ - The plugin build tools: a C++26 compiler, `make`, Hyprland's headers (`pkg-config hyprland`, shipped with
48
+ Arch's `hyprland` package), `libxcb` and `libxkbcommon`.
49
+ - Optional: `cua-driver` for the agent cursor.
50
+ - `grim` for screenshots.
51
+ - For `--take-pointer`, `--take-focus` and keys without `--window`: write access to `/dev/uinput`, usually through
52
+ the `input` group or a udev ACL. Check with `test -w /dev/uinput`.
53
+ - Python 3.12 or later and `uv`.
54
+
55
+ ## Install
56
+
57
+ ```sh
58
+ uv tool install deskpilot
59
+ ```
60
+
61
+ The agent skill, which teaches an agent the steps below, installs in Claude Code with:
62
+
63
+ ```text
64
+ /plugin install deskpilot --marketplace cjber/deskpilot
65
+ ```
66
+
67
+ ## Use
68
+
69
+ ```sh
70
+ deskpilot windows # every window: address, class, title, geometry, whether on screen
71
+ deskpilot shot battle.net.exe # screenshot; prints its id, image path and scale
72
+ deskpilot click battle.net.exe 920 367 --shot ID # a point in that screenshot
73
+ deskpilot type "/reload" --window wow.exe --delay 0.12
74
+ deskpilot key ctrl+a --window kitty
75
+ deskpilot wait-for wow.exe --timeout 120
76
+ ```
77
+
78
+ A window is named by its address, its class, or a substring of its class or title. A name that matches more
79
+ than one window is an error listing them, so a click never lands on a guessed window.
80
+
81
+ `shot` captures the window itself, so a window on top does not show in it, and saves it under an id. `move`,
82
+ `click` and `scroll` take pixels in the shot named by `--shot`, or the window's latest one, whatever scale it was
83
+ taken at. A window resized since that shot is refused, as is a point outside the window. Pass `--scale 1` to give
84
+ window pixels instead.
85
+
86
+ A window on a hidden workspace or scrolled off its monitor is reported as `"visible": false`, and `shot`, `move`,
87
+ `click` and `scroll` refuse it rather than move the user's view. `--show` focuses it to bring it on screen first.
88
+
89
+ The first action that needs the plugin builds and loads it for the running Hyprland, so it comes back by itself
90
+ after Hyprland restarts or upgrades. `deskpilot plugin load` does the same ahead of time.
91
+
92
+ `move` only shows the agent cursor. Pass `--take-pointer` to `move`, `click` or `scroll` to move the user's own
93
+ pointer there and press through the input device instead, for a hover the app must see.
94
+
95
+ A shell started before Hyprland restarted keeps a stale `HYPRLAND_INSTANCE_SIGNATURE`. deskpilot then talks to
96
+ the one instance whose socket answers, and with several live instances it stops and names them.
97
+
98
+ `key` and `type` without `--window` press keys into whatever has focus. `--take-focus` focuses the window and
99
+ leaves it focused, for an app that ignores keys while unfocused.
100
+
101
+ Games read keys once per frame, so type into them with `--delay 0.1` or more: each key is held that long, with the
102
+ same gap before the next.
103
+
104
+ Clicks, scrolls and keys from several agents at once run one after another in the order they arrive, so one `type`
105
+ is never interleaved with another's keys. Up to 32 wait behind the running one; past that an action is refused.
106
+
107
+ ## MCP
108
+
109
+ `deskpilot mcp` serves the same actions over stdio: `windows`, `focus`, `shot` (returns the image and its shot
110
+ id), `click`, `move`, `scroll`, `key`, `type_text` and `wait_for`. `click`, `move` and `scroll` take `shot`, `show`
111
+ and `take_pointer`; `key` and `type_text` take `take_focus`. It keeps one input
112
+ device and one agent cursor open for the session.
113
+
114
+ ```sh
115
+ claude mcp add deskpilot -- deskpilot mcp
116
+ ```
117
+
118
+ ## Limits
119
+
120
+ - Text is typed on a US layout; a character that layout cannot type is an error rather than skipped.
121
+ - Hyprland plugins load only into the exact Hyprland they were built against, so `deskpilot plugin load` builds
122
+ one per Hyprland commit and plugin source under `~/.local/share/deskpilot`, and replaces a loaded build of
123
+ other source.
124
+ - Each CLI action that uses the input device opens a new one and waits about a second for libinput to add it. The MCP server
125
+ avoids that wait.
126
+ - Hyprland only, Linux only.
127
+
128
+ ## Development
129
+
130
+ [AGENTS.md](AGENTS.md) has the checks, the layout and the rules a change must keep, and
131
+ [CONTRIBUTING.md](CONTRIBUTING.md) how to report a bug or send a change.