@elizaos/computeruse 0.24.20

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.
@@ -0,0 +1,65 @@
1
+ use napi::{self, Status};
2
+ use computeruse::errors::AutomationError;
3
+
4
+ /// Map ComputerUse errors to NAPI errors
5
+ pub fn map_error(err: AutomationError) -> napi::Error {
6
+ match err {
7
+ AutomationError::ElementNotFound(msg) => {
8
+ napi::Error::new(Status::InvalidArg, format!("ELEMENT_NOT_FOUND: {msg}"))
9
+ }
10
+ AutomationError::Timeout(msg) => napi::Error::new(
11
+ Status::GenericFailure,
12
+ format!("OPERATION_TIMED_OUT: {msg}"),
13
+ ),
14
+ AutomationError::PermissionDenied(msg) => {
15
+ napi::Error::new(Status::GenericFailure, format!("PERMISSION_DENIED: {msg}"))
16
+ }
17
+ AutomationError::PlatformError(e) => {
18
+ napi::Error::new(Status::GenericFailure, format!("PLATFORM_ERROR: {e}"))
19
+ }
20
+ AutomationError::UnsupportedOperation(msg) => {
21
+ napi::Error::new(Status::InvalidArg, format!("UNSUPPORTED_OPERATION: {msg}"))
22
+ }
23
+ AutomationError::UnsupportedPlatform(msg) => {
24
+ napi::Error::new(Status::InvalidArg, format!("UNSUPPORTED_PLATFORM: {msg}"))
25
+ }
26
+ AutomationError::InvalidArgument(e) => {
27
+ napi::Error::new(Status::InvalidArg, format!("INVALID_ARGUMENT: {e}"))
28
+ }
29
+ AutomationError::Internal(e) => {
30
+ napi::Error::new(Status::GenericFailure, format!("INTERNAL_ERROR: {e}"))
31
+ }
32
+ AutomationError::InvalidSelector(e) => {
33
+ napi::Error::new(Status::InvalidArg, format!("INVALID_SELECTOR: {e}"))
34
+ }
35
+ AutomationError::UIAutomationAPIError { message, .. } => napi::Error::new(
36
+ Status::GenericFailure,
37
+ format!("UI_AUTOMATION_API_ERROR: {message}"),
38
+ ),
39
+ AutomationError::ElementDetached(msg) => {
40
+ napi::Error::new(Status::InvalidArg, format!("ELEMENT_DETACHED: {msg}"))
41
+ }
42
+ AutomationError::ElementNotVisible(msg) => {
43
+ napi::Error::new(Status::InvalidArg, format!("ELEMENT_NOT_VISIBLE: {msg}"))
44
+ }
45
+ AutomationError::ElementNotEnabled(msg) => {
46
+ napi::Error::new(Status::InvalidArg, format!("ELEMENT_NOT_ENABLED: {msg}"))
47
+ }
48
+ AutomationError::ElementNotStable(msg) => {
49
+ napi::Error::new(Status::GenericFailure, format!("ELEMENT_NOT_STABLE: {msg}"))
50
+ }
51
+ AutomationError::ElementObscured(msg) => {
52
+ napi::Error::new(Status::InvalidArg, format!("ELEMENT_OBSCURED: {msg}"))
53
+ }
54
+ AutomationError::ScrollFailed(msg) => {
55
+ napi::Error::new(Status::GenericFailure, format!("SCROLL_FAILED: {msg}"))
56
+ }
57
+ AutomationError::OperationCancelled(msg) => {
58
+ napi::Error::new(Status::Cancelled, format!("OPERATION_CANCELLED: {msg}"))
59
+ }
60
+ AutomationError::VerificationFailed(msg) => napi::Error::new(
61
+ Status::GenericFailure,
62
+ format!("VERIFICATION_FAILED: {msg}"),
63
+ ),
64
+ }
65
+ }
package/src/lib.rs ADDED
@@ -0,0 +1,26 @@
1
+ mod desktop;
2
+ mod element;
3
+ mod exceptions;
4
+ mod locator;
5
+ mod selector;
6
+ mod types;
7
+ mod window_manager;
8
+
9
+ // Main types first
10
+ pub use desktop::Desktop;
11
+ pub use element::{Element, TypeTextOptions};
12
+ pub use locator::Locator;
13
+ pub use selector::Selector;
14
+ pub use types::{
15
+ ActionResult, Bounds, BoundsEntry, BrowserDomElement, BrowserDomResult, ClickResult, ClickType,
16
+ ClusteredBoundsEntry, ClusteredFormattingResult, CommandOutput, Coordinates, DomBoundsEntry,
17
+ ElementSource, FontStyle, GeminiVisionResult, HighlightHandle, InspectElement, Monitor,
18
+ MonitorScreenshotPair, OcrBoundsEntry, OcrElement, OcrResult, OmniparserBoundsEntry,
19
+ OmniparserItem, OmniparserResult, OverlayDisplayMode, PropertyLoadingMode, ScreenshotResult,
20
+ TextPosition, TreeBuildConfig, TreeOutputFormat, UIElementAttributes, UINode,
21
+ VisionBoundsEntry, VisionElement, VisionType, WindowTreeResult,
22
+ };
23
+ pub use window_manager::{WindowInfo, WindowManager};
24
+
25
+ // Error handling - see exceptions.rs for detailed architecture
26
+ pub use exceptions::map_error;
package/src/locator.rs ADDED
@@ -0,0 +1,172 @@
1
+ use napi_derive::napi;
2
+ use computeruse::locator::WaitCondition as ComputerUseWaitCondition;
3
+ use computeruse::Locator as ComputerUseLocator;
4
+
5
+ use crate::map_error;
6
+ use crate::Element;
7
+ use crate::Selector;
8
+ use napi::bindgen_prelude::Either;
9
+
10
+ /// Locator for finding UI elements by selector.
11
+ #[napi(js_name = "Locator")]
12
+ pub struct Locator {
13
+ inner: ComputerUseLocator,
14
+ }
15
+
16
+ impl std::fmt::Display for Locator {
17
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18
+ write!(f, "Locator({})", self.inner.selector_string())
19
+ }
20
+ }
21
+
22
+ impl From<ComputerUseLocator> for Locator {
23
+ fn from(l: ComputerUseLocator) -> Self {
24
+ Locator { inner: l }
25
+ }
26
+ }
27
+
28
+ #[napi]
29
+ impl Locator {
30
+ /// (async) Get the first matching element.
31
+ ///
32
+ /// @param {number} [timeoutMs] - Timeout in milliseconds (default: 10000).
33
+ /// @returns {Promise<Element>} The first matching element.
34
+ #[napi]
35
+ pub async fn first(&self, timeout_ms: Option<f64>) -> napi::Result<Element> {
36
+ use std::time::Duration;
37
+ let timeout = Duration::from_millis(timeout_ms.unwrap_or(10000.0) as u64);
38
+ self.inner
39
+ .first(Some(timeout))
40
+ .await
41
+ .map(Element::from)
42
+ .map_err(map_error)
43
+ }
44
+
45
+ /// (async) Get all matching elements.
46
+ ///
47
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
48
+ /// @param {number} [depth] - Maximum depth to search.
49
+ /// @returns {Promise<Array<Element>>} List of matching elements.
50
+ #[napi]
51
+ pub async fn all(&self, timeout_ms: f64, depth: Option<u32>) -> napi::Result<Vec<Element>> {
52
+ use std::time::Duration;
53
+ let timeout = Duration::from_millis(timeout_ms as u64);
54
+ let depth = depth.map(|d| d as usize);
55
+ self.inner
56
+ .all(Some(timeout), depth)
57
+ .await
58
+ .map(|els| els.into_iter().map(Element::from).collect())
59
+ .map_err(map_error)
60
+ }
61
+
62
+ /// Set a default timeout for this locator.
63
+ ///
64
+ /// @param {number} timeoutMs - Timeout in milliseconds.
65
+ /// @returns {Locator} A new locator with the specified timeout.
66
+ #[napi]
67
+ pub fn timeout(&self, timeout_ms: f64) -> Locator {
68
+ let loc = self
69
+ .inner
70
+ .clone()
71
+ .set_default_timeout(std::time::Duration::from_millis(timeout_ms as u64));
72
+ Locator::from(loc)
73
+ }
74
+
75
+ /// Set the root element for this locator.
76
+ ///
77
+ /// @param {Element} element - The root element.
78
+ /// @returns {Locator} A new locator with the specified root element.
79
+ #[napi]
80
+ pub fn within(&self, element: &Element) -> Locator {
81
+ let loc = self.inner.clone().within(element.inner.clone());
82
+ Locator::from(loc)
83
+ }
84
+
85
+ /// Chain another selector.
86
+ /// Accepts either a selector string or a Selector object.
87
+ ///
88
+ /// @param {string | Selector} selector - The selector.
89
+ /// @returns {Locator} A new locator with the chained selector.
90
+ #[napi]
91
+ pub fn locator(
92
+ &self,
93
+ #[napi(ts_arg_type = "string | Selector")] selector: Either<String, &Selector>,
94
+ ) -> napi::Result<Locator> {
95
+ use napi::bindgen_prelude::Either::*;
96
+ let sel_rust: computeruse::selector::Selector = match selector {
97
+ A(sel_str) => sel_str.as_str().into(),
98
+ B(sel_obj) => sel_obj.inner.clone(),
99
+ };
100
+ let loc = self.inner.clone().locator(sel_rust);
101
+ Ok(Locator::from(loc))
102
+ }
103
+
104
+ /// (async) Validate element existence without throwing an error.
105
+ ///
106
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
107
+ /// @returns {Promise<ValidationResult>} Validation result with exists flag and optional element.
108
+ #[napi]
109
+ pub async fn validate(&self, timeout_ms: f64) -> napi::Result<ValidationResult> {
110
+ use std::time::Duration;
111
+ let timeout = Duration::from_millis(timeout_ms as u64);
112
+ match self.inner.validate(Some(timeout)).await {
113
+ Ok(Some(element)) => Ok(ValidationResult {
114
+ exists: true,
115
+ element: Some(Element::from(element)),
116
+ error: None,
117
+ }),
118
+ Ok(None) => Ok(ValidationResult {
119
+ exists: false,
120
+ element: None,
121
+ error: None,
122
+ }),
123
+ Err(e) => Ok(ValidationResult {
124
+ exists: false,
125
+ element: None,
126
+ error: Some(e.to_string()),
127
+ }),
128
+ }
129
+ }
130
+
131
+ /// (async) Wait for an element to meet a specific condition.
132
+ ///
133
+ /// @param {string} condition - Condition to wait for: 'exists', 'visible', 'enabled', 'focused'
134
+ /// @param {number} timeoutMs - Timeout in milliseconds (required).
135
+ /// @returns {Promise<Element>} The element when condition is met.
136
+ #[napi]
137
+ pub async fn wait_for(&self, condition: String, timeout_ms: f64) -> napi::Result<Element> {
138
+ use std::time::Duration;
139
+ let wait_condition = parse_condition(&condition)?;
140
+ let timeout = Duration::from_millis(timeout_ms as u64);
141
+
142
+ self.inner
143
+ .wait_for(wait_condition, Some(timeout))
144
+ .await
145
+ .map(Element::from)
146
+ .map_err(map_error)
147
+ }
148
+ }
149
+
150
+ /// Result of element validation
151
+ #[napi(object)]
152
+ pub struct ValidationResult {
153
+ /// Whether the element exists
154
+ pub exists: bool,
155
+ /// The element if found
156
+ pub element: Option<Element>,
157
+ /// Error message if validation failed (not element not found, but actual error)
158
+ pub error: Option<String>,
159
+ }
160
+ /// Convert string condition to WaitCondition enum
161
+ fn parse_condition(condition: &str) -> napi::Result<ComputerUseWaitCondition> {
162
+ match condition.to_lowercase().as_str() {
163
+ "exists" => Ok(ComputerUseWaitCondition::Exists),
164
+ "visible" => Ok(ComputerUseWaitCondition::Visible),
165
+ "enabled" => Ok(ComputerUseWaitCondition::Enabled),
166
+ "focused" => Ok(ComputerUseWaitCondition::Focused),
167
+ _ => Err(napi::Error::new(
168
+ napi::Status::InvalidArg,
169
+ format!("Invalid condition '{condition}'. Valid: exists, visible, enabled, focused"),
170
+ )),
171
+ }
172
+ }
@@ -0,0 +1,158 @@
1
+ use napi::bindgen_prelude::FromNapiValue;
2
+ use napi_derive::napi;
3
+ use std::collections::BTreeMap;
4
+ use computeruse::selector::Selector as ComputerUseSelector;
5
+
6
+ /// Selector for locating UI elements. Provides a typed alternative to the string based selector API.
7
+ #[napi(js_name = "Selector")]
8
+ pub struct Selector {
9
+ pub(crate) inner: ComputerUseSelector,
10
+ }
11
+
12
+ impl std::fmt::Display for Selector {
13
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14
+ write!(f, "{:?}", self.inner)
15
+ }
16
+ }
17
+
18
+ impl From<ComputerUseSelector> for Selector {
19
+ fn from(sel: ComputerUseSelector) -> Self {
20
+ Selector { inner: sel }
21
+ }
22
+ }
23
+
24
+ impl From<&Selector> for ComputerUseSelector {
25
+ fn from(sel: &Selector) -> Self {
26
+ sel.inner.clone()
27
+ }
28
+ }
29
+
30
+ impl FromNapiValue for Selector {
31
+ unsafe fn from_napi_value(
32
+ env: napi::sys::napi_env,
33
+ napi_val: napi::sys::napi_value,
34
+ ) -> napi::Result<Self> {
35
+ let mut result = std::ptr::null_mut();
36
+ let status = napi::sys::napi_get_value_external(env, napi_val, &mut result);
37
+ if status != napi::sys::Status::napi_ok {
38
+ return Err(napi::Error::new(
39
+ napi::Status::InvalidArg,
40
+ "Failed to get external value for Selector".to_string(),
41
+ ));
42
+ }
43
+ Ok(std::ptr::read(result as *const Selector))
44
+ }
45
+ }
46
+
47
+ #[napi]
48
+ impl Selector {
49
+ /// Create a selector that matches elements by their accessibility `name`.
50
+ #[napi(factory)]
51
+ pub fn name(name: String) -> Self {
52
+ Selector::from(ComputerUseSelector::Name(name))
53
+ }
54
+
55
+ /// Create a selector that matches elements by role (and optionally name).
56
+ #[napi(factory)]
57
+ pub fn role(role: String, name: Option<String>) -> Self {
58
+ Selector::from(ComputerUseSelector::Role { role, name })
59
+ }
60
+
61
+ /// Create a selector that matches elements by accessibility `id`.
62
+ #[napi(factory, js_name = "id")]
63
+ pub fn id_factory(id: String) -> Self {
64
+ Selector::from(ComputerUseSelector::Id(id))
65
+ }
66
+
67
+ /// Create a selector that matches elements by the text they display.
68
+ #[napi(factory)]
69
+ pub fn text(text: String) -> Self {
70
+ Selector::from(ComputerUseSelector::Text(text))
71
+ }
72
+
73
+ /// Create a selector from an XPath-like path string.
74
+ #[napi(factory)]
75
+ pub fn path(path: String) -> Self {
76
+ Selector::from(ComputerUseSelector::Path(path))
77
+ }
78
+
79
+ /// Create a selector that matches elements by a native automation id (e.g., AutomationID on Windows).
80
+ #[napi(factory, js_name = "nativeId")]
81
+ pub fn native_id(id: String) -> Self {
82
+ Selector::from(ComputerUseSelector::NativeId(id))
83
+ }
84
+
85
+ /// Create a selector that matches elements by their class name.
86
+ #[napi(factory, js_name = "className")]
87
+ pub fn class_name(name: String) -> Self {
88
+ Selector::from(ComputerUseSelector::ClassName(name))
89
+ }
90
+
91
+ /// Create a selector from an arbitrary attribute map.
92
+ #[napi(factory)]
93
+ pub fn attributes(attributes: std::collections::HashMap<String, String>) -> Self {
94
+ let map: BTreeMap<String, String> = attributes.into_iter().collect();
95
+ Selector::from(ComputerUseSelector::Attributes(map))
96
+ }
97
+
98
+ /// Chain another selector onto this selector.
99
+ #[napi]
100
+ pub fn chain(&self, other: &Selector) -> Selector {
101
+ Selector::from(ComputerUseSelector::Chain(vec![
102
+ self.inner.clone(),
103
+ other.inner.clone(),
104
+ ]))
105
+ }
106
+
107
+ /// Filter by visibility.
108
+ #[napi]
109
+ pub fn visible(&self, is_visible: bool) -> Selector {
110
+ Selector::from(ComputerUseSelector::Chain(vec![
111
+ self.inner.clone(),
112
+ ComputerUseSelector::Visible(is_visible),
113
+ ]))
114
+ }
115
+
116
+ /// Create a selector that selects the nth element from matches.
117
+ /// Positive values are 0-based from the start (0 = first, 1 = second).
118
+ /// Negative values are from the end (-1 = last, -2 = second-to-last).
119
+ #[napi(factory)]
120
+ pub fn nth(index: i32) -> Self {
121
+ Selector::from(ComputerUseSelector::Nth(index))
122
+ }
123
+
124
+ /// Create a selector that matches elements having at least one descendant matching the inner selector.
125
+ /// This is similar to Playwright's :has() pseudo-class.
126
+ #[napi(factory)]
127
+ pub fn has(inner_selector: &Selector) -> Self {
128
+ Selector::from(ComputerUseSelector::Has(Box::new(
129
+ inner_selector.inner.clone(),
130
+ )))
131
+ }
132
+
133
+ /// Create a selector that navigates to the parent element.
134
+ /// This is similar to Playwright's .. syntax.
135
+ #[napi(factory)]
136
+ pub fn parent() -> Self {
137
+ Selector::from(ComputerUseSelector::Parent)
138
+ }
139
+
140
+ /// Create a selector that scopes the search to a specific process.
141
+ /// This is typically used as the first part of a chained selector.
142
+ /// Example: `Selector.process("chrome").chain(Selector.role("Button", "Submit"))`
143
+ #[napi(factory)]
144
+ pub fn process(process_name: String) -> Self {
145
+ Selector::from(ComputerUseSelector::Process(process_name))
146
+ }
147
+
148
+ /// Create a selector that scopes the search to a specific window within a process.
149
+ /// Typically chained after a process selector.
150
+ /// Example: `Selector.process("notepad").chain(Selector.window("Untitled"))`
151
+ #[napi(factory)]
152
+ pub fn window(title: String) -> Self {
153
+ Selector::from(ComputerUseSelector::Role {
154
+ role: "Window".to_string(),
155
+ name: Some(title),
156
+ })
157
+ }
158
+ }