opendesk 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.
- opendesk-0.1.0/LICENSE +21 -0
- opendesk-0.1.0/PKG-INFO +364 -0
- opendesk-0.1.0/README.md +318 -0
- opendesk-0.1.0/opendesk/__init__.py +58 -0
- opendesk-0.1.0/opendesk/cli.py +90 -0
- opendesk-0.1.0/opendesk/computer/__init__.py +1 -0
- opendesk-0.1.0/opendesk/computer/capture.py +191 -0
- opendesk-0.1.0/opendesk/computer/deps.py +27 -0
- opendesk-0.1.0/opendesk/computer/marks.py +415 -0
- opendesk-0.1.0/opendesk/computer/ocr.py +159 -0
- opendesk-0.1.0/opendesk/computer/sandbox.py +188 -0
- opendesk-0.1.0/opendesk/integrations/__init__.py +1 -0
- opendesk-0.1.0/opendesk/integrations/claude_code.py +253 -0
- opendesk-0.1.0/opendesk/integrations/langchain_compat.py +106 -0
- opendesk-0.1.0/opendesk/integrations/mcp.py +173 -0
- opendesk-0.1.0/opendesk/integrations/openai_compat.py +201 -0
- opendesk-0.1.0/opendesk/learn/__init__.py +19 -0
- opendesk-0.1.0/opendesk/learn/recorder.py +366 -0
- opendesk-0.1.0/opendesk/learn/storage.py +67 -0
- opendesk-0.1.0/opendesk/learn/trajectory.py +201 -0
- opendesk-0.1.0/opendesk/registry.py +110 -0
- opendesk-0.1.0/opendesk/tools/__init__.py +19 -0
- opendesk-0.1.0/opendesk/tools/app.py +219 -0
- opendesk-0.1.0/opendesk/tools/base.py +239 -0
- opendesk-0.1.0/opendesk/tools/clipboard.py +137 -0
- opendesk-0.1.0/opendesk/tools/keyboard.py +208 -0
- opendesk-0.1.0/opendesk/tools/learn.py +194 -0
- opendesk-0.1.0/opendesk/tools/mouse.py +254 -0
- opendesk-0.1.0/opendesk/tools/ocr.py +74 -0
- opendesk-0.1.0/opendesk/tools/screenshot.py +237 -0
- opendesk-0.1.0/opendesk/tools/ui.py +727 -0
- opendesk-0.1.0/opendesk.egg-info/PKG-INFO +364 -0
- opendesk-0.1.0/opendesk.egg-info/SOURCES.txt +38 -0
- opendesk-0.1.0/opendesk.egg-info/dependency_links.txt +1 -0
- opendesk-0.1.0/opendesk.egg-info/entry_points.txt +3 -0
- opendesk-0.1.0/opendesk.egg-info/requires.txt +32 -0
- opendesk-0.1.0/opendesk.egg-info/top_level.txt +1 -0
- opendesk-0.1.0/pyproject.toml +94 -0
- opendesk-0.1.0/setup.cfg +4 -0
- opendesk-0.1.0/tests/test_base.py +195 -0
opendesk-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vitalops.ai
|
|
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.
|
opendesk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opendesk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open Computer Use Agent — framework for desktop and browser automation
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/abhijithneilabraham/opendesk
|
|
7
|
+
Project-URL: Documentation, https://github.com/abhijithneilabraham/opendesk/tree/main/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/abhijithneilabraham/opendesk/issues
|
|
9
|
+
Keywords: computer-use,automation,accessibility,ai,agent,mcp
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Classifier: Topic :: System :: Operating System
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Provides-Extra: core
|
|
24
|
+
Requires-Dist: mss>=9.0; extra == "core"
|
|
25
|
+
Requires-Dist: Pillow>=10.0; extra == "core"
|
|
26
|
+
Requires-Dist: pyautogui>=0.9; extra == "core"
|
|
27
|
+
Provides-Extra: mcp
|
|
28
|
+
Requires-Dist: mcp>=1.0; extra == "mcp"
|
|
29
|
+
Provides-Extra: openai
|
|
30
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
31
|
+
Provides-Extra: langchain
|
|
32
|
+
Requires-Dist: langchain-core>=0.2; extra == "langchain"
|
|
33
|
+
Provides-Extra: linux
|
|
34
|
+
Requires-Dist: pyatspi; extra == "linux"
|
|
35
|
+
Provides-Extra: windows
|
|
36
|
+
Requires-Dist: pywinauto; extra == "windows"
|
|
37
|
+
Requires-Dist: pygetwindow; extra == "windows"
|
|
38
|
+
Requires-Dist: pyperclip; extra == "windows"
|
|
39
|
+
Provides-Extra: ocr
|
|
40
|
+
Requires-Dist: pytesseract; extra == "ocr"
|
|
41
|
+
Provides-Extra: learn
|
|
42
|
+
Requires-Dist: pynput; extra == "learn"
|
|
43
|
+
Provides-Extra: all
|
|
44
|
+
Requires-Dist: opendesk[core,langchain,learn,mcp,ocr,openai]; extra == "all"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# opendesk
|
|
48
|
+
|
|
49
|
+
**Open Computer Use Agent** — gives any AI agent eyes and hands on your desktop.
|
|
50
|
+
|
|
51
|
+
opendesk runs as an MCP server. Install it, register it with your agent tool, and it adds screenshot, accessibility-based UI control, mouse, keyboard, clipboard, and OCR to every conversation — on macOS, Linux, and Windows.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install 'opendesk[core,mcp]'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Claude Code** — one command:
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add opendesk -- opendesk-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"opendesk": { "command": "opendesk-mcp" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Cursor / Continue** — same pattern, point `command` at `opendesk-mcp`.
|
|
76
|
+
|
|
77
|
+
That's it. Your agent can now say "take a screenshot", "click the Save button", or "type Hello World into TextEdit" and it will work.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## What it adds to your agent
|
|
82
|
+
|
|
83
|
+
| Tool | What it does |
|
|
84
|
+
|------|-------------|
|
|
85
|
+
| `ui` | Clicks buttons, types text, reads values — by element name, no pixel coordinates. Uses the platform's native accessibility tree (AppleScript / AT-SPI2 / UI Automation). |
|
|
86
|
+
| `screenshot` | Captures the screen. With `marks=True`, overlays numbered boxes on every interactive element so the agent can say "click mark 3". |
|
|
87
|
+
| `mouse` | Pixel-level mouse control with automatic Retina/HiDPI scaling. Last resort when `ui` has nothing to click. |
|
|
88
|
+
| `keyboard` | Types text (full Unicode), presses keys, sends hotkeys. |
|
|
89
|
+
| `app` | Opens, closes, and focuses applications. |
|
|
90
|
+
| `clipboard` | Reads and writes the system clipboard. |
|
|
91
|
+
| `ocr` | Extracts text from any screen region without sending to the LLM. |
|
|
92
|
+
|
|
93
|
+
**The agent follows a natural priority:** `ui` first (no coordinates needed) → `screenshot(marks=True)` to see numbered elements → `mouse` as last resort for unlabelled canvas areas.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## How the MCP integration works
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Claude Code / Claude Desktop / Cursor / Continue
|
|
101
|
+
|
|
|
102
|
+
| MCP stdio
|
|
103
|
+
v
|
|
104
|
+
opendesk-mcp
|
|
105
|
+
|
|
|
106
|
+
+-- screenshot, ui, mouse, keyboard, app, clipboard, ocr
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
opendesk starts as a child process, speaks the MCP protocol over stdin/stdout, and the LLM client handles all tool-calling automatically. You never write tool-calling code.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Why opendesk?
|
|
114
|
+
|
|
115
|
+
- **MCP-first** — works out of the box with any MCP client, zero glue code.
|
|
116
|
+
- **Accessibility tree first** — the `ui` tool interacts with apps the same way a screen reader does, without pixel coordinates or Retina scaling headaches.
|
|
117
|
+
- **Framework-agnostic** — also ships Anthropic SDK, OpenAI, and LangChain adapters.
|
|
118
|
+
- **Sandboxed** — per-session audit log, app allow-list, screen region constraints.
|
|
119
|
+
- **Extensible** — one class to add a custom tool; it appears in all integrations automatically.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Installation
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Minimal (just the framework, no hardware deps)
|
|
127
|
+
pip install opendesk
|
|
128
|
+
|
|
129
|
+
# Core computer use: screen capture + mouse/keyboard
|
|
130
|
+
pip install 'opendesk[core]'
|
|
131
|
+
|
|
132
|
+
# With MCP server support
|
|
133
|
+
pip install 'opendesk[core,mcp]'
|
|
134
|
+
|
|
135
|
+
# Everything
|
|
136
|
+
pip install 'opendesk[all]'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### System dependencies
|
|
140
|
+
|
|
141
|
+
| Platform | Required |
|
|
142
|
+
|----------|---------|
|
|
143
|
+
| macOS | Screen Recording permission (System Settings → Privacy → Screen Recording); Accessibility permission for mouse/keyboard |
|
|
144
|
+
| Linux | `xclip` for clipboard; `xdotool` or `pyatspi` for keyboard/UI |
|
|
145
|
+
| Windows | No extra system deps (uses Win32 APIs) |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Quick start
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import asyncio
|
|
153
|
+
from opendesk import create_registry, allow_all_context
|
|
154
|
+
|
|
155
|
+
async def main():
|
|
156
|
+
registry = create_registry()
|
|
157
|
+
ctx = allow_all_context()
|
|
158
|
+
|
|
159
|
+
# Take a screenshot with Set-of-Marks overlay
|
|
160
|
+
screenshot = registry.get("screenshot")
|
|
161
|
+
result = await screenshot.execute(ctx, screenshot.Params(marks=True))
|
|
162
|
+
print(result.output) # lists all interactive elements as [1] Button "OK" ...
|
|
163
|
+
# result.attachments[0].content -> PNG bytes
|
|
164
|
+
|
|
165
|
+
# Click a button by name — no pixel coordinates needed
|
|
166
|
+
ui = registry.get("ui")
|
|
167
|
+
await ui.execute(ctx, ui.Params(action="click", app="Safari", title="Go"))
|
|
168
|
+
|
|
169
|
+
# Type text
|
|
170
|
+
kb = registry.get("keyboard")
|
|
171
|
+
await kb.execute(ctx, kb.Params(action="type", text="hello world"))
|
|
172
|
+
|
|
173
|
+
asyncio.run(main())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Integrations
|
|
179
|
+
|
|
180
|
+
### MCP server (Claude Desktop, Continue, Cursor, ...)
|
|
181
|
+
|
|
182
|
+
Run the MCP server over stdio:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
opendesk-mcp
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Add to Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"mcpServers": {
|
|
193
|
+
"opendesk": {
|
|
194
|
+
"command": "opendesk-mcp"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or create a server in Python:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from opendesk.integrations.mcp import create_mcp_server
|
|
204
|
+
from opendesk.registry import create_registry
|
|
205
|
+
from mcp.server.stdio import stdio_server
|
|
206
|
+
|
|
207
|
+
server = create_mcp_server(create_registry())
|
|
208
|
+
async with stdio_server() as (r, w):
|
|
209
|
+
await server.run(r, w, server.create_initialization_options())
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### Claude Code / Anthropic SDK
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import anthropic
|
|
218
|
+
from opendesk.integrations.claude_code import ClaudeCodeAdapter
|
|
219
|
+
from opendesk.registry import create_registry
|
|
220
|
+
|
|
221
|
+
client = anthropic.Anthropic()
|
|
222
|
+
adapter = ClaudeCodeAdapter(create_registry())
|
|
223
|
+
|
|
224
|
+
messages = [{"role": "user", "content": "Open Safari and take a screenshot"}]
|
|
225
|
+
|
|
226
|
+
# Full agentic loop (handles tool use automatically)
|
|
227
|
+
final_text = await adapter.run_loop(
|
|
228
|
+
client,
|
|
229
|
+
model="claude-opus-4-6",
|
|
230
|
+
messages=messages,
|
|
231
|
+
system="You are a computer use agent. Use the ui tool first, mouse as last resort.",
|
|
232
|
+
)
|
|
233
|
+
print(final_text)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Manual control:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
response = client.messages.create(
|
|
240
|
+
model="claude-opus-4-6",
|
|
241
|
+
max_tokens=4096,
|
|
242
|
+
tools=adapter.tool_definitions(),
|
|
243
|
+
messages=messages,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Dispatch all tool_use blocks in parallel
|
|
247
|
+
tool_results = await adapter.handle_response(response)
|
|
248
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
249
|
+
messages.append({"role": "user", "content": tool_results})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### OpenAI function calling
|
|
255
|
+
|
|
256
|
+
Works with OpenAI, Groq, Together AI, Ollama, LiteLLM, and any OpenAI-compatible provider:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from openai import OpenAI
|
|
260
|
+
from opendesk.integrations.openai_compat import OpenAIAdapter
|
|
261
|
+
from opendesk.registry import create_registry
|
|
262
|
+
|
|
263
|
+
client = OpenAI()
|
|
264
|
+
adapter = OpenAIAdapter(create_registry())
|
|
265
|
+
|
|
266
|
+
messages = [{"role": "user", "content": "Take a screenshot"}]
|
|
267
|
+
final_text = await adapter.run_loop(client, model="gpt-4o", messages=messages)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### LangChain / LangGraph
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from langchain_openai import ChatOpenAI
|
|
276
|
+
from langgraph.prebuilt import create_react_agent
|
|
277
|
+
|
|
278
|
+
from opendesk.integrations.langchain_compat import as_langchain_tools
|
|
279
|
+
from opendesk.registry import create_registry
|
|
280
|
+
|
|
281
|
+
tools = as_langchain_tools(create_registry())
|
|
282
|
+
agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools)
|
|
283
|
+
result = agent.invoke({"messages": [("user", "Take a screenshot")]})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Tools
|
|
289
|
+
|
|
290
|
+
| Tool | Description |
|
|
291
|
+
|------|-------------|
|
|
292
|
+
| `ui` | Click, type, and read UI elements by name via the accessibility tree. **Use this first.** |
|
|
293
|
+
| `screenshot` | Capture screen with Set-of-Marks overlay, cursor dot, zoom, and change detection |
|
|
294
|
+
| `mouse` | Click, scroll, drag — with automatic Retina/HiDPI coordinate scaling |
|
|
295
|
+
| `keyboard` | Type (full Unicode), press keys, hotkeys, hold |
|
|
296
|
+
| `app` | Open, close, focus, or list applications |
|
|
297
|
+
| `clipboard` | Read or write the system clipboard |
|
|
298
|
+
| `ocr` | Extract text from any screen region (pytesseract / Vision / WinRT) |
|
|
299
|
+
|
|
300
|
+
### Tool priority
|
|
301
|
+
|
|
302
|
+
When the agent needs to interact with a UI element:
|
|
303
|
+
|
|
304
|
+
1. **`ui` tool** — click by element title/role, no coordinates needed. Most reliable.
|
|
305
|
+
2. **`screenshot` with `marks=True`** — if `ui` doesn't find the element, get a SoM overlay showing numbered bounding boxes.
|
|
306
|
+
3. **`mouse` with `image_width`/`image_height`** — last resort for unlabelled canvas areas. Always provide image dimensions for correct Retina scaling.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Architecture
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
opendesk/
|
|
314
|
+
├── tools/ # Tool definitions (base.py + one file per tool)
|
|
315
|
+
├── computer/ # Low-level helpers: capture, marks (SoM), OCR, sandbox
|
|
316
|
+
├── integrations/ # MCP, Claude Code, OpenAI, LangChain adapters
|
|
317
|
+
└── registry.py # ToolRegistry + create_registry()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
See [docs/architecture.md](docs/architecture.md) for a deep dive.
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Permission model
|
|
325
|
+
|
|
326
|
+
Every tool action goes through a `ToolContext.check_permission()` call before execution.
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
from opendesk.tools.base import allow_all_context, interactive_context
|
|
330
|
+
|
|
331
|
+
# Headless / autonomous — approve everything automatically
|
|
332
|
+
ctx = allow_all_context()
|
|
333
|
+
|
|
334
|
+
# Interactive — prompt on stdout before each action
|
|
335
|
+
ctx = interactive_context()
|
|
336
|
+
|
|
337
|
+
# Custom handler — integrate with your own UI or policy engine
|
|
338
|
+
async def my_handler(tool: str, argument: str, description: str) -> None:
|
|
339
|
+
if "production" in description.lower():
|
|
340
|
+
raise PermissionDeniedError("Refusing to act on production.")
|
|
341
|
+
|
|
342
|
+
from opendesk.tools.base import ToolContext
|
|
343
|
+
ctx = ToolContext(session_id="my-session", permission_handler=my_handler)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Platform support
|
|
349
|
+
|
|
350
|
+
| Feature | macOS | Linux | Windows |
|
|
351
|
+
|---------|-------|-------|---------|
|
|
352
|
+
| Screenshot | mss + Pillow | mss + Pillow | mss + Pillow |
|
|
353
|
+
| Mouse control | pyautogui | pyautogui | pyautogui |
|
|
354
|
+
| Keyboard (Unicode) | pbcopy + cmd+v | xclip/xsel + ctrl+v | pyperclip + ctrl+v |
|
|
355
|
+
| AX tree (`ui` tool) | AppleScript | AT-SPI2 / xdotool | pywinauto |
|
|
356
|
+
| SoM marks | AppleScript | pyatspi | pywinauto |
|
|
357
|
+
| OCR | pytesseract / Vision | pytesseract | pytesseract / WinRT |
|
|
358
|
+
| App open/close | `open -a` / AppleScript | xdg-open / pkill | start / taskkill |
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
MIT
|
opendesk-0.1.0/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# opendesk
|
|
2
|
+
|
|
3
|
+
**Open Computer Use Agent** — gives any AI agent eyes and hands on your desktop.
|
|
4
|
+
|
|
5
|
+
opendesk runs as an MCP server. Install it, register it with your agent tool, and it adds screenshot, accessibility-based UI control, mouse, keyboard, clipboard, and OCR to every conversation — on macOS, Linux, and Windows.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install 'opendesk[core,mcp]'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Claude Code** — one command:
|
|
16
|
+
```bash
|
|
17
|
+
claude mcp add opendesk -- opendesk-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"opendesk": { "command": "opendesk-mcp" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Cursor / Continue** — same pattern, point `command` at `opendesk-mcp`.
|
|
30
|
+
|
|
31
|
+
That's it. Your agent can now say "take a screenshot", "click the Save button", or "type Hello World into TextEdit" and it will work.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## What it adds to your agent
|
|
36
|
+
|
|
37
|
+
| Tool | What it does |
|
|
38
|
+
|------|-------------|
|
|
39
|
+
| `ui` | Clicks buttons, types text, reads values — by element name, no pixel coordinates. Uses the platform's native accessibility tree (AppleScript / AT-SPI2 / UI Automation). |
|
|
40
|
+
| `screenshot` | Captures the screen. With `marks=True`, overlays numbered boxes on every interactive element so the agent can say "click mark 3". |
|
|
41
|
+
| `mouse` | Pixel-level mouse control with automatic Retina/HiDPI scaling. Last resort when `ui` has nothing to click. |
|
|
42
|
+
| `keyboard` | Types text (full Unicode), presses keys, sends hotkeys. |
|
|
43
|
+
| `app` | Opens, closes, and focuses applications. |
|
|
44
|
+
| `clipboard` | Reads and writes the system clipboard. |
|
|
45
|
+
| `ocr` | Extracts text from any screen region without sending to the LLM. |
|
|
46
|
+
|
|
47
|
+
**The agent follows a natural priority:** `ui` first (no coordinates needed) → `screenshot(marks=True)` to see numbered elements → `mouse` as last resort for unlabelled canvas areas.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## How the MCP integration works
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Claude Code / Claude Desktop / Cursor / Continue
|
|
55
|
+
|
|
|
56
|
+
| MCP stdio
|
|
57
|
+
v
|
|
58
|
+
opendesk-mcp
|
|
59
|
+
|
|
|
60
|
+
+-- screenshot, ui, mouse, keyboard, app, clipboard, ocr
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
opendesk starts as a child process, speaks the MCP protocol over stdin/stdout, and the LLM client handles all tool-calling automatically. You never write tool-calling code.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Why opendesk?
|
|
68
|
+
|
|
69
|
+
- **MCP-first** — works out of the box with any MCP client, zero glue code.
|
|
70
|
+
- **Accessibility tree first** — the `ui` tool interacts with apps the same way a screen reader does, without pixel coordinates or Retina scaling headaches.
|
|
71
|
+
- **Framework-agnostic** — also ships Anthropic SDK, OpenAI, and LangChain adapters.
|
|
72
|
+
- **Sandboxed** — per-session audit log, app allow-list, screen region constraints.
|
|
73
|
+
- **Extensible** — one class to add a custom tool; it appears in all integrations automatically.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Minimal (just the framework, no hardware deps)
|
|
81
|
+
pip install opendesk
|
|
82
|
+
|
|
83
|
+
# Core computer use: screen capture + mouse/keyboard
|
|
84
|
+
pip install 'opendesk[core]'
|
|
85
|
+
|
|
86
|
+
# With MCP server support
|
|
87
|
+
pip install 'opendesk[core,mcp]'
|
|
88
|
+
|
|
89
|
+
# Everything
|
|
90
|
+
pip install 'opendesk[all]'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### System dependencies
|
|
94
|
+
|
|
95
|
+
| Platform | Required |
|
|
96
|
+
|----------|---------|
|
|
97
|
+
| macOS | Screen Recording permission (System Settings → Privacy → Screen Recording); Accessibility permission for mouse/keyboard |
|
|
98
|
+
| Linux | `xclip` for clipboard; `xdotool` or `pyatspi` for keyboard/UI |
|
|
99
|
+
| Windows | No extra system deps (uses Win32 APIs) |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Quick start
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
from opendesk import create_registry, allow_all_context
|
|
108
|
+
|
|
109
|
+
async def main():
|
|
110
|
+
registry = create_registry()
|
|
111
|
+
ctx = allow_all_context()
|
|
112
|
+
|
|
113
|
+
# Take a screenshot with Set-of-Marks overlay
|
|
114
|
+
screenshot = registry.get("screenshot")
|
|
115
|
+
result = await screenshot.execute(ctx, screenshot.Params(marks=True))
|
|
116
|
+
print(result.output) # lists all interactive elements as [1] Button "OK" ...
|
|
117
|
+
# result.attachments[0].content -> PNG bytes
|
|
118
|
+
|
|
119
|
+
# Click a button by name — no pixel coordinates needed
|
|
120
|
+
ui = registry.get("ui")
|
|
121
|
+
await ui.execute(ctx, ui.Params(action="click", app="Safari", title="Go"))
|
|
122
|
+
|
|
123
|
+
# Type text
|
|
124
|
+
kb = registry.get("keyboard")
|
|
125
|
+
await kb.execute(ctx, kb.Params(action="type", text="hello world"))
|
|
126
|
+
|
|
127
|
+
asyncio.run(main())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Integrations
|
|
133
|
+
|
|
134
|
+
### MCP server (Claude Desktop, Continue, Cursor, ...)
|
|
135
|
+
|
|
136
|
+
Run the MCP server over stdio:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
opendesk-mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Add to Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"opendesk": {
|
|
148
|
+
"command": "opendesk-mcp"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or create a server in Python:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from opendesk.integrations.mcp import create_mcp_server
|
|
158
|
+
from opendesk.registry import create_registry
|
|
159
|
+
from mcp.server.stdio import stdio_server
|
|
160
|
+
|
|
161
|
+
server = create_mcp_server(create_registry())
|
|
162
|
+
async with stdio_server() as (r, w):
|
|
163
|
+
await server.run(r, w, server.create_initialization_options())
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### Claude Code / Anthropic SDK
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import anthropic
|
|
172
|
+
from opendesk.integrations.claude_code import ClaudeCodeAdapter
|
|
173
|
+
from opendesk.registry import create_registry
|
|
174
|
+
|
|
175
|
+
client = anthropic.Anthropic()
|
|
176
|
+
adapter = ClaudeCodeAdapter(create_registry())
|
|
177
|
+
|
|
178
|
+
messages = [{"role": "user", "content": "Open Safari and take a screenshot"}]
|
|
179
|
+
|
|
180
|
+
# Full agentic loop (handles tool use automatically)
|
|
181
|
+
final_text = await adapter.run_loop(
|
|
182
|
+
client,
|
|
183
|
+
model="claude-opus-4-6",
|
|
184
|
+
messages=messages,
|
|
185
|
+
system="You are a computer use agent. Use the ui tool first, mouse as last resort.",
|
|
186
|
+
)
|
|
187
|
+
print(final_text)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Manual control:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
response = client.messages.create(
|
|
194
|
+
model="claude-opus-4-6",
|
|
195
|
+
max_tokens=4096,
|
|
196
|
+
tools=adapter.tool_definitions(),
|
|
197
|
+
messages=messages,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Dispatch all tool_use blocks in parallel
|
|
201
|
+
tool_results = await adapter.handle_response(response)
|
|
202
|
+
messages.append({"role": "assistant", "content": response.content})
|
|
203
|
+
messages.append({"role": "user", "content": tool_results})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### OpenAI function calling
|
|
209
|
+
|
|
210
|
+
Works with OpenAI, Groq, Together AI, Ollama, LiteLLM, and any OpenAI-compatible provider:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from openai import OpenAI
|
|
214
|
+
from opendesk.integrations.openai_compat import OpenAIAdapter
|
|
215
|
+
from opendesk.registry import create_registry
|
|
216
|
+
|
|
217
|
+
client = OpenAI()
|
|
218
|
+
adapter = OpenAIAdapter(create_registry())
|
|
219
|
+
|
|
220
|
+
messages = [{"role": "user", "content": "Take a screenshot"}]
|
|
221
|
+
final_text = await adapter.run_loop(client, model="gpt-4o", messages=messages)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### LangChain / LangGraph
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
from langchain_openai import ChatOpenAI
|
|
230
|
+
from langgraph.prebuilt import create_react_agent
|
|
231
|
+
|
|
232
|
+
from opendesk.integrations.langchain_compat import as_langchain_tools
|
|
233
|
+
from opendesk.registry import create_registry
|
|
234
|
+
|
|
235
|
+
tools = as_langchain_tools(create_registry())
|
|
236
|
+
agent = create_react_agent(ChatOpenAI(model="gpt-4o"), tools)
|
|
237
|
+
result = agent.invoke({"messages": [("user", "Take a screenshot")]})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Tools
|
|
243
|
+
|
|
244
|
+
| Tool | Description |
|
|
245
|
+
|------|-------------|
|
|
246
|
+
| `ui` | Click, type, and read UI elements by name via the accessibility tree. **Use this first.** |
|
|
247
|
+
| `screenshot` | Capture screen with Set-of-Marks overlay, cursor dot, zoom, and change detection |
|
|
248
|
+
| `mouse` | Click, scroll, drag — with automatic Retina/HiDPI coordinate scaling |
|
|
249
|
+
| `keyboard` | Type (full Unicode), press keys, hotkeys, hold |
|
|
250
|
+
| `app` | Open, close, focus, or list applications |
|
|
251
|
+
| `clipboard` | Read or write the system clipboard |
|
|
252
|
+
| `ocr` | Extract text from any screen region (pytesseract / Vision / WinRT) |
|
|
253
|
+
|
|
254
|
+
### Tool priority
|
|
255
|
+
|
|
256
|
+
When the agent needs to interact with a UI element:
|
|
257
|
+
|
|
258
|
+
1. **`ui` tool** — click by element title/role, no coordinates needed. Most reliable.
|
|
259
|
+
2. **`screenshot` with `marks=True`** — if `ui` doesn't find the element, get a SoM overlay showing numbered bounding boxes.
|
|
260
|
+
3. **`mouse` with `image_width`/`image_height`** — last resort for unlabelled canvas areas. Always provide image dimensions for correct Retina scaling.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Architecture
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
opendesk/
|
|
268
|
+
├── tools/ # Tool definitions (base.py + one file per tool)
|
|
269
|
+
├── computer/ # Low-level helpers: capture, marks (SoM), OCR, sandbox
|
|
270
|
+
├── integrations/ # MCP, Claude Code, OpenAI, LangChain adapters
|
|
271
|
+
└── registry.py # ToolRegistry + create_registry()
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
See [docs/architecture.md](docs/architecture.md) for a deep dive.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Permission model
|
|
279
|
+
|
|
280
|
+
Every tool action goes through a `ToolContext.check_permission()` call before execution.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from opendesk.tools.base import allow_all_context, interactive_context
|
|
284
|
+
|
|
285
|
+
# Headless / autonomous — approve everything automatically
|
|
286
|
+
ctx = allow_all_context()
|
|
287
|
+
|
|
288
|
+
# Interactive — prompt on stdout before each action
|
|
289
|
+
ctx = interactive_context()
|
|
290
|
+
|
|
291
|
+
# Custom handler — integrate with your own UI or policy engine
|
|
292
|
+
async def my_handler(tool: str, argument: str, description: str) -> None:
|
|
293
|
+
if "production" in description.lower():
|
|
294
|
+
raise PermissionDeniedError("Refusing to act on production.")
|
|
295
|
+
|
|
296
|
+
from opendesk.tools.base import ToolContext
|
|
297
|
+
ctx = ToolContext(session_id="my-session", permission_handler=my_handler)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Platform support
|
|
303
|
+
|
|
304
|
+
| Feature | macOS | Linux | Windows |
|
|
305
|
+
|---------|-------|-------|---------|
|
|
306
|
+
| Screenshot | mss + Pillow | mss + Pillow | mss + Pillow |
|
|
307
|
+
| Mouse control | pyautogui | pyautogui | pyautogui |
|
|
308
|
+
| Keyboard (Unicode) | pbcopy + cmd+v | xclip/xsel + ctrl+v | pyperclip + ctrl+v |
|
|
309
|
+
| AX tree (`ui` tool) | AppleScript | AT-SPI2 / xdotool | pywinauto |
|
|
310
|
+
| SoM marks | AppleScript | pyatspi | pywinauto |
|
|
311
|
+
| OCR | pytesseract / Vision | pytesseract | pytesseract / WinRT |
|
|
312
|
+
| App open/close | `open -a` / AppleScript | xdg-open / pkill | start / taskkill |
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT
|