locum 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 (120) hide show
  1. locum-0.1.0/.gitignore +13 -0
  2. locum-0.1.0/LICENSE +21 -0
  3. locum-0.1.0/PKG-INFO +510 -0
  4. locum-0.1.0/README.md +464 -0
  5. locum-0.1.0/docs/ARCHITECTURE.md +189 -0
  6. locum-0.1.0/docs/COMPARISON.md +94 -0
  7. locum-0.1.0/docs/LLM.md +473 -0
  8. locum-0.1.0/examples/basic_automation.py +89 -0
  9. locum-0.1.0/examples/data/invoices.csv +6 -0
  10. locum-0.1.0/examples/data/sales.csv +5 -0
  11. locum-0.1.0/examples/excel_report_flow.yaml +57 -0
  12. locum-0.1.0/examples/llm_agent.py +131 -0
  13. locum-0.1.0/examples/mcp_config.json +12 -0
  14. locum-0.1.0/examples/pyautogui_migration.py +85 -0
  15. locum-0.1.0/examples/sap_style_data_entry.py +262 -0
  16. locum-0.1.0/pyproject.toml +63 -0
  17. locum-0.1.0/src/locum/__init__.py +397 -0
  18. locum-0.1.0/src/locum/__main__.py +10 -0
  19. locum-0.1.0/src/locum/_nodeinfo.py +235 -0
  20. locum-0.1.0/src/locum/backends/__init__.py +98 -0
  21. locum-0.1.0/src/locum/backends/_x11clip.py +373 -0
  22. locum-0.1.0/src/locum/backends/base.py +373 -0
  23. locum-0.1.0/src/locum/backends/macos.py +2924 -0
  24. locum-0.1.0/src/locum/backends/mock.py +775 -0
  25. locum-0.1.0/src/locum/backends/windows.py +2848 -0
  26. locum-0.1.0/src/locum/backends/x11.py +2462 -0
  27. locum-0.1.0/src/locum/cli.py +1286 -0
  28. locum-0.1.0/src/locum/compat/__init__.py +41 -0
  29. locum-0.1.0/src/locum/compat/pyautogui.py +1100 -0
  30. locum-0.1.0/src/locum/desktop.py +1200 -0
  31. locum-0.1.0/src/locum/element.py +1374 -0
  32. locum-0.1.0/src/locum/errors.py +208 -0
  33. locum-0.1.0/src/locum/failsafe.py +331 -0
  34. locum-0.1.0/src/locum/keyboard.py +404 -0
  35. locum-0.1.0/src/locum/keys.py +258 -0
  36. locum-0.1.0/src/locum/llm/__init__.py +64 -0
  37. locum-0.1.0/src/locum/llm/agent.py +783 -0
  38. locum-0.1.0/src/locum/llm/computer_use.py +813 -0
  39. locum-0.1.0/src/locum/llm/mcp_server.py +368 -0
  40. locum-0.1.0/src/locum/llm/observe.py +540 -0
  41. locum-0.1.0/src/locum/llm/safety.py +2113 -0
  42. locum-0.1.0/src/locum/llm/tools.py +1891 -0
  43. locum-0.1.0/src/locum/motion.py +372 -0
  44. locum-0.1.0/src/locum/mouse.py +327 -0
  45. locum-0.1.0/src/locum/ocr.py +673 -0
  46. locum-0.1.0/src/locum/rpa/__init__.py +61 -0
  47. locum-0.1.0/src/locum/rpa/flow.py +1516 -0
  48. locum-0.1.0/src/locum/rpa/queue.py +1524 -0
  49. locum-0.1.0/src/locum/rpa/recorder.py +1237 -0
  50. locum-0.1.0/src/locum/rpa/report.py +486 -0
  51. locum-0.1.0/src/locum/rpa/robot.py +968 -0
  52. locum-0.1.0/src/locum/screen.py +345 -0
  53. locum-0.1.0/src/locum/selectors.py +1801 -0
  54. locum-0.1.0/src/locum/selftest.py +571 -0
  55. locum-0.1.0/src/locum/types.py +239 -0
  56. locum-0.1.0/src/locum/vision.py +1156 -0
  57. locum-0.1.0/src/locum/wait.py +323 -0
  58. locum-0.1.0/src/locum/window.py +398 -0
  59. locum-0.1.0/tests/_queue_workers.py +37 -0
  60. locum-0.1.0/tests/_stub_desktop.py +65 -0
  61. locum-0.1.0/tests/conftest.py +38 -0
  62. locum-0.1.0/tests/test_api_docs_review7_fixes.py +369 -0
  63. locum-0.1.0/tests/test_cli.py +681 -0
  64. locum-0.1.0/tests/test_compat_pyautogui.py +468 -0
  65. locum-0.1.0/tests/test_core_correctness_fixes.py +418 -0
  66. locum-0.1.0/tests/test_desktop.py +645 -0
  67. locum-0.1.0/tests/test_element.py +438 -0
  68. locum-0.1.0/tests/test_element_focus_container.py +155 -0
  69. locum-0.1.0/tests/test_element_focus_proof.py +284 -0
  70. locum-0.1.0/tests/test_element_review2_fixes.py +387 -0
  71. locum-0.1.0/tests/test_element_wait_fail_fast.py +144 -0
  72. locum-0.1.0/tests/test_failsafe.py +138 -0
  73. locum-0.1.0/tests/test_input_review2_fixes.py +217 -0
  74. locum-0.1.0/tests/test_integration_fixes.py +333 -0
  75. locum-0.1.0/tests/test_keyboard.py +351 -0
  76. locum-0.1.0/tests/test_llm_agent.py +367 -0
  77. locum-0.1.0/tests/test_llm_computer_use.py +307 -0
  78. locum-0.1.0/tests/test_llm_observe.py +160 -0
  79. locum-0.1.0/tests/test_llm_review2_fixes.py +310 -0
  80. locum-0.1.0/tests/test_llm_rpa_review6_fixes.py +286 -0
  81. locum-0.1.0/tests/test_llm_rpa_safety_fixes.py +454 -0
  82. locum-0.1.0/tests/test_llm_safety.py +163 -0
  83. locum-0.1.0/tests/test_llm_tools.py +408 -0
  84. locum-0.1.0/tests/test_llm_tools_review3_fixes.py +335 -0
  85. locum-0.1.0/tests/test_macos_backend.py +1612 -0
  86. locum-0.1.0/tests/test_mcp_server.py +135 -0
  87. locum-0.1.0/tests/test_mock_examples_review4_fixes.py +368 -0
  88. locum-0.1.0/tests/test_motion.py +142 -0
  89. locum-0.1.0/tests/test_mouse.py +254 -0
  90. locum-0.1.0/tests/test_ocr.py +443 -0
  91. locum-0.1.0/tests/test_package.py +192 -0
  92. locum-0.1.0/tests/test_package_review5_fixes.py +246 -0
  93. locum-0.1.0/tests/test_packaging_metadata.py +71 -0
  94. locum-0.1.0/tests/test_policy_allow_rule_scope.py +271 -0
  95. locum-0.1.0/tests/test_policy_allowlist.py +351 -0
  96. locum-0.1.0/tests/test_policy_nameless_targets.py +205 -0
  97. locum-0.1.0/tests/test_queue_concurrency.py +159 -0
  98. locum-0.1.0/tests/test_queue_durability.py +196 -0
  99. locum-0.1.0/tests/test_queue_leases.py +197 -0
  100. locum-0.1.0/tests/test_queue_write_failures.py +174 -0
  101. locum-0.1.0/tests/test_readme_claims.py +42 -0
  102. locum-0.1.0/tests/test_review6_fixes.py +510 -0
  103. locum-0.1.0/tests/test_rpa_flow.py +384 -0
  104. locum-0.1.0/tests/test_rpa_flow_cli_review3_fixes.py +411 -0
  105. locum-0.1.0/tests/test_rpa_policy_gate.py +322 -0
  106. locum-0.1.0/tests/test_rpa_queue.py +222 -0
  107. locum-0.1.0/tests/test_rpa_recorder.py +344 -0
  108. locum-0.1.0/tests/test_rpa_review2_fixes.py +406 -0
  109. locum-0.1.0/tests/test_rpa_robot.py +441 -0
  110. locum-0.1.0/tests/test_screen.py +299 -0
  111. locum-0.1.0/tests/test_selectors.py +581 -0
  112. locum-0.1.0/tests/test_selectors_review2_fixes.py +293 -0
  113. locum-0.1.0/tests/test_selftest.py +82 -0
  114. locum-0.1.0/tests/test_type_verify.py +208 -0
  115. locum-0.1.0/tests/test_vision.py +628 -0
  116. locum-0.1.0/tests/test_wait.py +239 -0
  117. locum-0.1.0/tests/test_wait_fail_fast.py +350 -0
  118. locum-0.1.0/tests/test_window.py +252 -0
  119. locum-0.1.0/tests/test_windows_backend.py +1771 -0
  120. locum-0.1.0/tests/test_x11_backend.py +1352 -0
locum-0.1.0/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .venv/
8
+ locum_runs/
9
+ *.log
10
+ __pycache__/
11
+
12
+ # local agent worktrees / tool state
13
+ .claude/
locum-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 locum contributors
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.
locum-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,510 @@
1
+ Metadata-Version: 2.5
2
+ Name: locum
3
+ Version: 0.1.0
4
+ Summary: Desktop automation for Python (Windows first; macOS and Linux/X11 backends): input, vision, accessibility selectors, RPA workflows, and LLM agent control.
5
+ Project-URL: Homepage, https://github.com/israerusan/locum
6
+ Project-URL: Repository, https://github.com/israerusan/locum
7
+ Project-URL: Issues, https://github.com/israerusan/locum/issues
8
+ Project-URL: Documentation, https://github.com/israerusan/locum/blob/main/docs/LLM.md
9
+ Author: israerusan
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,automation,computer-use,desktop,gui,llm,mcp,pyautogui,rpa
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Desktop Environment
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: comtypes>=1.2; sys_platform == 'win32'
22
+ Requires-Dist: mss>=9.0
23
+ Requires-Dist: numpy>=1.22
24
+ Requires-Dist: pillow>=9.0
25
+ Requires-Dist: pyobjc-framework-applicationservices>=9.0; sys_platform == 'darwin'
26
+ Requires-Dist: pyobjc-framework-cocoa>=9.0; sys_platform == 'darwin'
27
+ Requires-Dist: pyobjc-framework-quartz>=9.0; sys_platform == 'darwin'
28
+ Requires-Dist: python-xlib>=0.33; sys_platform == 'linux'
29
+ Provides-Extra: all
30
+ Requires-Dist: anthropic>=0.40; extra == 'all'
31
+ Requires-Dist: pynput>=1.7; extra == 'all'
32
+ Requires-Dist: pytesseract>=0.3.10; extra == 'all'
33
+ Requires-Dist: pyyaml>=6.0; extra == 'all'
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7; extra == 'dev'
36
+ Requires-Dist: pyyaml>=6.0; extra == 'dev'
37
+ Provides-Extra: llm
38
+ Requires-Dist: anthropic>=0.40; extra == 'llm'
39
+ Provides-Extra: ocr
40
+ Requires-Dist: pytesseract>=0.3.10; extra == 'ocr'
41
+ Provides-Extra: record
42
+ Requires-Dist: pynput>=1.7; extra == 'record'
43
+ Provides-Extra: yaml
44
+ Requires-Dist: pyyaml>=6.0; extra == 'yaml'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # locum
48
+
49
+ **Desktop automation for Python that understands the screen.** One library for
50
+ mouse/keyboard input, screenshots and template matching, accessibility
51
+ selectors, RPA robots, and LLM-agent control. Windows is the most-validated
52
+ platform; macOS and Linux (X11) backends exist but have been tested less (see
53
+ below). **Native Wayland is not supported**: on Linux, locum drives X11
54
+ sessions and XWayland windows only.
55
+
56
+ | Platform | Backend | How it is tested today |
57
+ |----------|---------|------------------------|
58
+ | Windows 10/11 | SendInput + **UI Automation** | unit tests against a fake Win32/UIA API; validated on a real Windows 11 machine by the built-in self-test |
59
+ | macOS 12+ | Quartz events + **AX API** | unit tests against a fake Quartz/AX API only — **not yet validated on a real Mac** |
60
+ | Linux (X11 / XWayland) | XTEST + **AT-SPI** | unit tests plus X11 tests under Xvfb in CI; native Wayland input/capture **not supported** |
61
+
62
+ * **Selectors, not coordinates.** `d.click('button "Save"')` finds the element
63
+ through UI Automation / AX / AT-SPI, survives window moves, DPI and theme
64
+ changes, and falls back to the native *invoke* action when the control is
65
+ covered. UiPath-style XML selectors work too.
66
+ * **Vision when there is no accessibility tree.** FFT-based normalized
67
+ cross-correlation, multi-scale, multi-monitor, **alpha channel = mask**, OCR
68
+ hooks, wait-for-change / wait-until-stable.
69
+ * **Input that behaves like a person** (optional): Fitts's-law timing,
70
+ Bézier mouse paths with overshoot, Unicode keystroke injection that does not
71
+ depend on the keyboard layout (with read-back verification for text fields,
72
+ see [below](#humanized-layout-independent-input)), clipboard paste,
73
+ stuck-modifier-proof hotkeys, a corner fail-safe and a kill hotkey.
74
+ * **RPA built in.** Robots with steps/retries/evidence screenshots and HTML
75
+ reports, resumable file-backed work queues with business exceptions that
76
+ several worker processes on one machine can drain together (locked, leased,
77
+ fsynced), YAML flows, and a recorder that writes selectors instead of raw
78
+ coordinates.
79
+ * **LLM-native.** Set-of-marks observations; a Claude (Anthropic) agent loop
80
+ and computer-use adapter; tool schemas exportable in Anthropic, OpenAI and
81
+ MCP formats (locum ships no OpenAI agent runtime — bring your own loop);
82
+ a zero-dependency MCP server; and a safety policy with human confirmation,
83
+ dry-run and audit logs.
84
+ * **A drop-in PyAutoGUI layer** (`import locum.compat.pyautogui as pyautogui`)
85
+ so existing scripts move over with one line.
86
+
87
+ > Status: **0.1 alpha** — usable, tested, but APIs may still change between
88
+ > releases. The design is in [`docs/ARCHITECTURE.md`](https://github.com/israerusan/locum/blob/main/docs/ARCHITECTURE.md).
89
+ > See [`docs/COMPARISON.md`](https://github.com/israerusan/locum/blob/main/docs/COMPARISON.md) and [`docs/LLM.md`](https://github.com/israerusan/locum/blob/main/docs/LLM.md).
90
+
91
+ ---
92
+
93
+ ## Install
94
+
95
+ ```bash
96
+ pip install locum # core: pillow, numpy, mss (+ python-xlib / comtypes / pyobjc per OS)
97
+ pip install "locum[llm]" # + anthropic SDK for the Agent
98
+ pip install "locum[ocr]" # + pytesseract only (install the Tesseract binary too)
99
+ pip install "locum[record]" # + pynput: live recorder and global kill hotkey
100
+ pip install "locum[yaml]" # + PyYAML for YAML flows (JSON flows need nothing)
101
+ pip install "locum[all]" # everything above
102
+ ```
103
+
104
+ From a checkout: `pip install -e ".[dev]"` and `python -m pytest -q`.
105
+
106
+ Platform notes: on **Linux** the clipboard works out of the box (native X11
107
+ selection handling; `xclip`/`xsel` are used if present) and, for
108
+ accessibility selectors, install PyGObject with AT-SPI (`python3-gi gir1.2-atspi-2.0`
109
+ on Debian/Ubuntu). On **macOS** grant your terminal/IDE *Accessibility* and
110
+ *Screen Recording* permission (System Settings → Privacy & Security).
111
+
112
+ ## Check it on your machine
113
+
114
+ Before automating real work, run the self-test. It opens a fresh, empty
115
+ editor tab (Notepad / TextEdit / gedit), types into it, uses the clipboard and
116
+ moves that window, then cleans up and restores your clipboard and mouse. It
117
+ refuses to type into an editor that already contains text:
118
+
119
+ ```bash
120
+ python -m locum.selftest # report in ./locum_selftest_<timestamp>/
121
+ ```
122
+
123
+ Don't touch the mouse or keyboard while it runs (about a minute); slam the
124
+ mouse into a screen corner to abort.
125
+
126
+ ## 60-second quickstart
127
+
128
+ locum ships with a simulated desktop (an ERP form, a spreadsheet and a text
129
+ editor), so you can try everything without touching your real screen:
130
+
131
+ ```python
132
+ from locum import Desktop
133
+ from locum.cli import DemoBackend
134
+
135
+ d = Desktop(DemoBackend()) # Desktop() drives your real screen instead
136
+
137
+ form = d.window("Invoice Entry*") # exact title, substring, glob or regex
138
+ form.find("edit#vendor").set_text("ACME Corp") # native set-value, falls back to typing;
139
+ # read back: refused/truncated input raises
140
+ form.find('edit "Invoice No"').set_text("INV-42")
141
+ form.find("edit#amount").set_text("1250.00")
142
+ d.click('button "Save"') # selector -> element -> click
143
+ print(d.find("text#status").text) # Saved invoice INV-42
144
+ ```
145
+
146
+ The same from the shell (`--backend mock` = the demo desktop):
147
+
148
+ ```bash
149
+ locum --backend mock windows
150
+ locum --backend mock tree --window "Invoice Entry"
151
+ locum --backend mock click 'button "Save"'
152
+ locum --backend mock observe # what an LLM agent sees
153
+ locum --backend mock run examples/excel_report_flow.yaml
154
+ ```
155
+
156
+ On your real desktop the same calls work against real applications — e.g.
157
+ `locum inspect` prints a robust selector for the element under the mouse.
158
+
159
+ ## Feature tour
160
+
161
+ The snippets below continue the quickstart (`d` is the demo desktop) and are
162
+ executed by the test-suite, so they stay correct.
163
+
164
+ ### Selectors: CSS-like, UiPath XML or dicts
165
+
166
+ ```python
167
+ d.find('window[name%="Invoice*"] button:has-text("save")') # glob, case-insensitive text
168
+ d.find("<wnd app='erp' title='Invoice Entry*' /><ctrl automationid='amount' role='editable text' />")
169
+ d.find({"role": "button", "name": "Clear", "within": {"role": "window", "name": "Invoice Entry - ERP"}})
170
+ d.find('window "Book1 - Excel" > edit "Name Box"') # child combinator
171
+ cells = d.find_all('edit[name~="^[A-E]1$"]') # regex attribute match
172
+ assert [c.name for c in cells] == ["A1", "B1", "C1", "D1", "E1"]
173
+
174
+ from locum import selector_for
175
+ el = d.element_at(370, 171) # what is under this point?
176
+ print(selector_for(el.node, roots=d.roots())) # window "Invoice Entry - ERP" edit#amount
177
+ print(d.tree(window="Invoice Entry*", interactive_only=True)) # readable tree dump
178
+ ```
179
+
180
+ Operators: `=` `!=` `*=` `^=` `$=` `~=` (regex) `%=` (glob), `i` flag for
181
+ case-insensitive; pseudo-classes `:enabled :visible :focused :checked :unchecked :mixed
182
+ :has-text() :has() :not() :nth() :first :last`. Every lookup waits up to
183
+ `default_timeout` and failures say what *was* there:
184
+ `No element matching 'button "Sav"'; did you mean: button "Save" #save`.
185
+
186
+ ### Elements and windows
187
+
188
+ ```python
189
+ amount = d.find("edit#amount")
190
+ amount.set_text("99.90").press("tab") # actions chain
191
+ d.find("checkbox#urgent").check()
192
+ assert d.find("checkbox#urgent").is_checked
193
+ d.wait('button "Save"', state="enabled", timeout=5)
194
+
195
+ excel = d.window("Book1*")
196
+ excel.activate()
197
+ print(excel.rect, excel.app, excel.is_active)
198
+ d.window("Invoice Entry*").activate()
199
+ ```
200
+
201
+ ### Vision: template matching with alpha masks
202
+
203
+ ```python
204
+ needle = d.screenshot((180, 268, 100, 32)) # a crop of the Save button
205
+ match = d.locate(needle, confidence=0.95)
206
+ print(match.rect, round(match.confidence, 3)) # Rect(left=180, top=268, ...) 1.0
207
+
208
+ masked = needle.convert("RGBA")
209
+ masked.paste((255, 0, 255, 0), (0, 0, 100, 8)) # transparent pixels are ignored when matching
210
+ assert d.locate(masked, region=(0, 0, 700, 600)).center == match.center
211
+ d.click_image(needle, timeout=2) # wait for it, then click its center
212
+ ```
213
+
214
+ `locate_all` (non-max suppression), `scales=(0.8, 1.0, 1.25)` for DPI
215
+ differences, `grayscale=True`, `region=`, `d.wait_until_stable()` and
216
+ `d.wait_for_change()` are all there. With an OCR engine installed,
217
+ `d.find_text("Total")`, `d.click_text("Submit")` and `d.read_text(region)` work
218
+ on any pixels. `locum[ocr]` installs only `pytesseract` (plus the Tesseract
219
+ binary from your OS); RapidOCR (`pip install rapidocr-onnxruntime`) and EasyOCR
220
+ (`pip install easyocr`) are optional, separate installs that locum picks up
221
+ when present.
222
+
223
+ ### Humanized, layout-independent input
224
+
225
+ ```python
226
+ d.move('button "Clear"', humanize=True, duration=0.2) # Bezier path with jitter and overshoot
227
+ d.click("edit#vendor")
228
+ d.type("Café Zoë ✓") # Unicode injection, no keymap needed
229
+ d.hotkey("mod+a") # ctrl+a, or cmd+a on macOS
230
+ d.press("backspace")
231
+ with d.hold("shift"):
232
+ d.press("tab")
233
+ ```
234
+
235
+ How reliable is typing? `d.type()` injects Unicode keystrokes (no keymap
236
+ lookup, so the keyboard layout does not matter), paced per character. That
237
+ does not make it infallible: some applications drop or garble synthesized
238
+ keystrokes while busy — seen intermittently with Windows 11 Notepad. For text
239
+ fields, `d.find("edit#vendor").type("…")` (`Element.type`, `verify=True` by
240
+ default) reads the field back afterwards and repairs a garbled result, or
241
+ raises instead of reporting success; `set_text()` sets the value natively and
242
+ verifies it; `d.type(text, mode="clipboard")` pastes instead of typing. Use one
243
+ of those when the exact text matters.
244
+
245
+ `Desktop(humanize=True)` (or `Humanize(speed=..., jitter=..., seed=...)`) makes
246
+ every move human-like; `pause=` mirrors PyAutoGUI's `PAUSE`. Moving the mouse
247
+ into a screen corner raises `FailSafeTriggered`; `locum.install_kill_hotkey()`
248
+ (needs `pynput`) aborts from anywhere.
249
+
250
+ ### RPA: robots, work queues, flows, recorder
251
+
252
+ ```python
253
+ import re
254
+ from locum import FailSafeTriggered
255
+ from locum.rpa import BusinessException, Robot, WorkQueue
256
+
257
+ robot = Robot(d, name="invoices", run_dir="locum_runs")
258
+ queue = WorkQueue("invoices", path="locum_runs/invoices.queue.json") # persistent: runs resume
259
+ queue.add({"vendor": "Initech", "invoice_no": "INV-43", "amount": "75"}, reference="Initech|INV-43")
260
+ queue.add({"vendor": "Umbrella", "invoice_no": "INV-44", "amount": "-1"}, reference="Umbrella|INV-44")
261
+
262
+ def enter_invoice(item):
263
+ if not re.fullmatch(r"\d+(\.\d{1,2})?", item["amount"]) or float(item["amount"]) <= 0:
264
+ raise BusinessException(f"invalid amount {item['amount']!r}") # recorded, never retried
265
+ d.click('button "Clear"') # known state: status "Cleared"
266
+ d.find("edit#vendor").set_text(item["vendor"]) # errors up to here are retried
267
+ d.find("edit#invoice_no").set_text(item["invoice_no"])
268
+ d.find("edit#amount").set_text(item["amount"])
269
+
270
+ def answered(): # the old status is still on screen
271
+ status = d.find("text#status").text
272
+ return status if status not in ("", "Cleared") else None
273
+
274
+ d.click('button "Save"')
275
+ try: # after Save, a retry could post twice
276
+ status = d.wait_until(answered, timeout=10)
277
+ except FailSafeTriggered:
278
+ raise # the operator stopped the robot
279
+ except Exception as exc:
280
+ raise BusinessException(f"needs review: no answer after Save ({exc})") from exc
281
+ if status.lower().startswith("error"):
282
+ raise BusinessException(status) # rejected by the app: never "success"
283
+ return {"status": status}
284
+
285
+ @robot.task(retries=1)
286
+ def process():
287
+ with robot.step("enter invoices"):
288
+ robot.vars["stats"] = queue.process(enter_invoice, retries=2)
289
+
290
+ report = robot.run() # timings, logs, error screenshots, report.json + report.html
291
+ print(report.summary(), robot.vars["stats"]["success"], robot.vars["stats"]["business_exception"])
292
+ ```
293
+
294
+ Declarative flows (YAML/JSON) cover the same ground without Python —
295
+ `click, type, set_text, wait, if_exists, for_each (csv/json/items), try/catch,
296
+ repeat, call, read_value, assert_exists, ...` with `${var}` interpolation; see
297
+ [`examples/excel_report_flow.yaml`](https://github.com/israerusan/locum/blob/main/examples/excel_report_flow.yaml):
298
+
299
+ ```python
300
+ from locum.rpa import run_flow
301
+
302
+ report = run_flow({"steps": [
303
+ {"set_text": {"target": "edit#vendor", "text": "${vendor}"}},
304
+ {"assert_exists": {"target": "edit#vendor", "equals": "Hooli"}},
305
+ ]}, d, variables={"vendor": "Hooli"})
306
+ assert report.ok
307
+ ```
308
+
309
+ The `Recorder` turns live input (or anything locum itself does) into flows
310
+ or Python **with selectors instead of coordinates**:
311
+
312
+ ```python
313
+ from locum.rpa import Recorder
314
+
315
+ rec = Recorder(d).start(source="backend") # "auto" = global hooks via pynput
316
+ d.click("edit#due_date")
317
+ d.type("2026-12-31")
318
+ rec.stop()
319
+ print(rec.to_python()) # d.click('window "Invoice Entry - ERP" edit#due_date') ...
320
+ ```
321
+
322
+ ### LLM control: tools, computer use, MCP, agents
323
+
324
+ ```python
325
+ from locum.llm import DesktopToolkit, Policy
326
+
327
+ kit = DesktopToolkit(d, policy=Policy()) # every call is policy-checked and audited
328
+ tools = kit.to_anthropic() # also .to_openai() / .to_mcp()
329
+ obs = kit.call("observe", {"include_screenshot": False})
330
+ print(obs.text.splitlines()[-1]) # [N] ... numbered elements (set-of-marks)
331
+ print(kit.call("set_value", {"selector": "edit#vendor", "value": "Pied Piper"}).text)
332
+ print(kit.call("click", {"selector": 'button "Submit Payment"'}).is_error) # True: needs confirmation
333
+ ```
334
+
335
+ <!-- norun -->
336
+ ```python
337
+ from locum import Desktop
338
+ from locum.llm import Agent, AuditLog, Policy, console_confirm
339
+
340
+ agent = Agent(Desktop(), policy=Policy(confirm=console_confirm), audit=AuditLog("audit.jsonl"))
341
+ result = agent.run("Open the Q3 report in Excel and e-mail the totals to Dana")
342
+ print(result.success, result.final_text)
343
+ ```
344
+
345
+ * `mode="tools"` (default) gives Claude the toolkit above: element ids,
346
+ selectors, OCR, keyboard/mouse, windows, apps, clipboard, and a `done` tool.
347
+ * `mode="computer_use"` uses Claude's computer-use toolset, executed by
348
+ `ComputerUseAdapter` (screenshot scaling and coordinate mapping included).
349
+ * `locum mcp` exposes the same tools to Claude Desktop, Claude Code or any
350
+ MCP client — config in [`examples/mcp_config.json`](https://github.com/israerusan/locum/blob/main/examples/mcp_config.json):
351
+
352
+ ```json
353
+ {"mcpServers": {"locum": {"command": "locum", "args": ["mcp", "--policy", "default"]}}}
354
+ ```
355
+
356
+ or `claude mcp add locum -- locum mcp --policy default` for Claude Code.
357
+ Details, prompting tips and the safety model: [`docs/LLM.md`](https://github.com/israerusan/locum/blob/main/docs/LLM.md).
358
+
359
+ ### Safety policy
360
+
361
+ ```python
362
+ from locum.llm import CARD_NUMBER, Policy
363
+
364
+ policy = Policy(
365
+ allowed_apps=["erp", "excel"], # act (and launch) only inside these apps
366
+ deny_text_patterns=[CARD_NUMBER], # never type card numbers (Luhn-checked; IBANs pass)
367
+ confirm=lambda description, details: input(f"Allow {description}? [y/N] ") == "y",
368
+ max_actions=200, max_actions_per_minute=30,
369
+ region=(0, 0, 1920, 1080), # pointer must stay in this rectangle
370
+ dry_run=False, # True: describe instead of act
371
+ )
372
+ strict = Policy.strict() # no launching/clipboard, shells blocked, broad confirmations
373
+ authorized = Policy.strict(allow=[ # default-deny: only these run without a human
374
+ 'window "Invoice Entry - ERP" edit',
375
+ 'window "Invoice Entry - ERP" button "Save"',
376
+ ])
377
+ ```
378
+
379
+ The default policy is a **heuristic tripwire**, not authorization. A human
380
+ `confirm` is needed (and without a callback the action is **blocked**) when the
381
+ target looks like a commit — *pay, transfer, wire, submit, send, delete, approve,
382
+ post, purchase, release, …*, their plurals and German SAP labels such as *Buchen*
383
+ and *Überweisen* — when Ctrl/Cmd+Enter or Ctrl/Cmd+S is pressed, or when a click,
384
+ Enter or field entry reaches a control that cannot be identified (nothing under
385
+ the pointer, or no name, automation id, value or named child: an icon-only
386
+ button). Keyword matching misses other languages, "Continue"/"Apply"-style
387
+ commits and shortcuts it was not told about.
388
+
389
+ For money and data actions use **allow-list mode**: with `Policy(allow=[...])` /
390
+ `Policy.strict(allow=[...])` every consequential action (clicks, text entry,
391
+ invoke, launching, clipboard writes, non-navigation keys) needs a human unless one
392
+ of your rules — a selector, `"window=..."`, `"app=..."`, `"launch=..."`,
393
+ `"keys=..."` or `"tool=..."` — matches what really receives it: the control the
394
+ mouse actually presses (not the container around it), or the default button that
395
+ Enter presses. A rule without `keys=` covers only typing, editing and navigation
396
+ keys and Enter/Space on the control it names; application shortcuts such as
397
+ Escape, F-keys or Alt+F4 need a `keys=` rule. Robots and flows are gated only when
398
+ you pass a policy (`Robot(policy=...)`, `run_flow(..., policy=...)`); without one
399
+ they run ungated as before. Text typed into password fields is masked in audit
400
+ logs and recordings. Details: [`docs/LLM.md`](https://github.com/israerusan/locum/blob/main/docs/LLM.md#6-safety-model).
401
+
402
+ ### PyAutoGUI compatibility
403
+
404
+ <!-- norun -->
405
+ ```python
406
+ import locum.compat.pyautogui as pyautogui # was: import pyautogui
407
+
408
+ pyautogui.moveTo(100, 200, duration=0.5, tween=pyautogui.easeInOutQuad)
409
+ pyautogui.write("Hello!\n", interval=0.05)
410
+ box = pyautogui.locateOnScreen("button.png", confidence=0.9)
411
+ pyautogui.click(pyautogui.center(box))
412
+ ```
413
+
414
+ See [`examples/pyautogui_migration.py`](https://github.com/israerusan/locum/blob/main/examples/pyautogui_migration.py) for
415
+ the before/after.
416
+
417
+ ## How it compares
418
+
419
+ | | PyAutoGUI | UiPath | **locum** |
420
+ |-----------------------------------|:---------:|:------:|:-----------:|
421
+ | Mouse/keyboard/screenshots | ✓ | ✓ | ✓ |
422
+ | Accessibility selectors (UIA/AX/AT-SPI) | — | ✓ (Windows-first) | ✓ |
423
+ | UiPath-style XML selectors | — | ✓ | ✓ (compatible subset) |
424
+ | Template matching w/ alpha masks, multi-scale | confidence needs OpenCV | ✓ | ✓ (numpy; OpenCV optional) |
425
+ | OCR | — | ✓ | pluggable: Tesseract via `locum[ocr]`; RapidOCR / EasyOCR as separate installs |
426
+ | Native invoke / set-value (works when covered) | — | ✓ | ✓ |
427
+ | Humanized motion, Unicode typing | partial | — | ✓ |
428
+ | Robots, retries, evidence reports | — | ✓ | ✓ (code-first) |
429
+ | Transaction queues | — | ✓ (Orchestrator: central, multi-machine) | ✓ (local JSON file: resumable, multi-process on one machine, leases; no network shares or multi-machine) |
430
+ | Recorder | — | ✓ (Studio) | ✓ (emits Python/YAML with selectors) |
431
+ | Visual designer, Orchestrator, enterprise governance, support | — | ✓ | — |
432
+ | LLM tools, MCP server, computer-use adapter, agent loop | — | add-ons | ✓ built in |
433
+ | Price / license | free, BSD-3 | commercial (free Community tier) | free, MIT |
434
+
435
+ UiPath is a full enterprise platform — Studio's visual designer, Orchestrator
436
+ scheduling and credential vaults, Action Center, governance and vendor
437
+ support. locum does not replace that; it is a **code-first, free,
438
+ LLM-native** library for developers who want UiPath-grade robustness inside
439
+ plain Python. Full write-up: [`docs/COMPARISON.md`](https://github.com/israerusan/locum/blob/main/docs/COMPARISON.md).
440
+
441
+ ## Platform details
442
+
443
+ How each platform is tested is in the table at the top; this is what each
444
+ backend uses.
445
+
446
+ | Platform | Input | Screenshots | Windows | Accessibility | Notes |
447
+ |----------|-------|-------------|---------|---------------|-------|
448
+ | Windows 10/11 | SendInput (Unicode, DPI-aware, multi-monitor) | mss / GDI | Win32 | **UI Automation** (comtypes) | run as the same integrity level as the target app (UAC); text locum puts on the clipboard is kept out of Clipboard History (Win+V) and cloud sync (`WindowsBackend(clipboard_history=True)` allows it) |
449
+ | macOS 12+ | Quartz events | Quartz (Retina-aware) | Quartz / AppKit | **AX API** | needs Accessibility + Screen Recording permission; not yet validated on a real Mac |
450
+ | Linux X11 | XTEST (Unicode via spare keycodes) | mss / XGetImage | EWMH | **AT-SPI** (PyGObject) | native X11 clipboard (xclip/xsel optional) |
451
+ | Linux Wayland | XWayland apps only | XWayland only | XWayland only | AT-SPI works | see below |
452
+ | Any (tests, CI, evals) | `MockBackend` / `DemoBackend` | ✓ | ✓ | ✓ | deterministic, no display |
453
+
454
+ **Wayland:** Wayland deliberately prevents applications from
455
+ injecting input into, or capturing, other applications. The X11 backend works
456
+ inside an X11 session and with XWayland windows; native Wayland windows cannot
457
+ be driven or captured through it. Use an X11 session (“GNOME on Xorg”) for
458
+ full-desktop automation; portal-based (`xdg-desktop-portal` RemoteDesktop /
459
+ ScreenCast) support is not implemented yet.
460
+
461
+ ## Safety notes for LLM control
462
+
463
+ * An agent with desktop access can do anything **you** can: send e-mail, move
464
+ money, delete files. Run unattended agents in a VM or a dedicated user
465
+ account, never on a machine with privileged sessions open.
466
+ * Keep the default policy (or `Policy.strict()`): consequential actions need
467
+ a human `confirm`; without one they are blocked. Use `allowed_apps` to fence
468
+ the agent into the applications the task needs, and `dry_run=True` to
469
+ rehearse.
470
+ * Keep the fail-safe on (mouse to a corner aborts) and arm the kill hotkey
471
+ (`locum --kill-hotkey agent ...` or `install_kill_hotkey()`).
472
+ * Treat on-screen text as untrusted: a web page or document can contain
473
+ instructions aimed at the model (prompt injection). The policy enforces
474
+ limits regardless of what the model was told.
475
+ * Keep the audit log (`AuditLog(path)` / `--audit FILE`): every tool call, its
476
+ arguments, the policy decision and the result are recorded as JSON lines.
477
+
478
+ ## CLI
479
+
480
+ ```text
481
+ locum [--backend NAME] [--timeout S] [--json] [--no-failsafe] [--humanize] [--kill-hotkey] COMMAND
482
+ info | screenshot [-o PNG] [--region L,T,W,H] [--monitor N] | windows | tree [--window T] [--depth N] [--all]
483
+ find SELECTOR [--all] | click TARGET | move TARGET | type TEXT [-e] [--into SEL] | hotkey COMBO | press KEY...
484
+ scroll DY [--at TARGET] | wait SELECTOR [--state S] | locate IMAGE [--confidence C] [--click]
485
+ observe [--json] [--screenshot PNG] | inspect [X,Y]
486
+ mcp [--policy strict|default|permissive] [--dry-run] [--audit FILE]
487
+ agent TASK [--model M] [--max-steps N] [--dry-run] [--mode tools|computer_use] [--replay FILE]
488
+ run FLOW.yaml [--var k=v] [--report-dir DIR] [--validate] | record [-o out.py|out.yaml] [--duration S]
489
+ ```
490
+
491
+ Exit codes: 0 ok · 1 failed (not found, timeout, policy, failed flow/agent) ·
492
+ 2 usage/invalid input · 3 unavailable (backend, capability, missing extra) ·
493
+ 4 fail-safe abort · 130 interrupted.
494
+
495
+ ## Examples
496
+
497
+ | File | What it shows |
498
+ |------|---------------|
499
+ | [`basic_automation.py`](https://github.com/israerusan/locum/blob/main/examples/basic_automation.py) | windows, selectors, typing, screenshots, template matching |
500
+ | [`sap_style_data_entry.py`](https://github.com/israerusan/locum/blob/main/examples/sap_style_data_entry.py) | robot + CSV work queue entering invoices via selectors, business exceptions, reports |
501
+ | [`excel_report_flow.yaml`](https://github.com/israerusan/locum/blob/main/examples/excel_report_flow.yaml) | YAML flow filling a spreadsheet from CSV |
502
+ | [`llm_agent.py`](https://github.com/israerusan/locum/blob/main/examples/llm_agent.py) | Claude agent with a policy, confirmation callback and audit log |
503
+ | [`mcp_config.json`](https://github.com/israerusan/locum/blob/main/examples/mcp_config.json) | Claude Desktop / Claude Code MCP configuration |
504
+ | [`pyautogui_migration.py`](https://github.com/israerusan/locum/blob/main/examples/pyautogui_migration.py) | one-line PyAutoGUI switch, then the upgrade |
505
+
506
+ All of them run on the demo desktop: `LOCUM_BACKEND=mock python examples/<file>`.
507
+
508
+ ## License
509
+
510
+ MIT.