agentremote-cli 1.0.0__py3-none-any.whl

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,308 @@
1
+ """Compact observe projection for agent-friendly output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from typing import Any, Dict, Iterable
7
+
8
+ from .agent_utils import (
9
+ _extract_int,
10
+ _first_present,
11
+ _first_text,
12
+ _optional_bool,
13
+ _target_terms,
14
+ _truncate,
15
+ _truthy,
16
+ )
17
+
18
+
19
+ def _compact_window_state(
20
+ payload: Any,
21
+ *,
22
+ title: str,
23
+ trace_id: Any,
24
+ targets: Iterable[str] | str | None,
25
+ element_limit: int,
26
+ ) -> Dict[str, Any]:
27
+ data = payload if isinstance(payload, dict) else {}
28
+ raw_elements = _extract_element_list(data)
29
+ elements = [_compact_element(element, index) for index, element in enumerate(raw_elements)]
30
+ selected = _select_elements(elements, targets, element_limit)
31
+ public_items = [_public_element(item) for item in selected]
32
+ by_role = Counter(str(item.get("role") or "Unknown") for item in elements)
33
+
34
+ state: Dict[str, Any] = {
35
+ "title": title,
36
+ "blocking": _compact_blocking(data),
37
+ "focus": _compact_focus(data, elements),
38
+ "elements": {
39
+ "total": len(elements),
40
+ "returned": len(public_items),
41
+ "truncated": len(public_items) < len(elements),
42
+ "by_role": dict(by_role),
43
+ "items": public_items,
44
+ },
45
+ "artifacts": _compact_artifacts(data, trace_id),
46
+ }
47
+ page = _compact_page(data)
48
+ if page:
49
+ state["page"] = page
50
+ return state
51
+
52
+
53
+ def _extract_element_list(payload: Dict[str, Any]) -> list[Dict[str, Any]]:
54
+ for key in ("elements", "uia_elements", "controls", "items"):
55
+ value = payload.get(key)
56
+ if isinstance(value, list):
57
+ return [item for item in value if isinstance(item, dict)]
58
+
59
+ for key in ("state", "window", "data"):
60
+ value = payload.get(key)
61
+ if isinstance(value, dict):
62
+ nested = _extract_element_list(value)
63
+ if nested:
64
+ return nested
65
+ return []
66
+
67
+
68
+ def _compact_element(element: Dict[str, Any], index: int) -> Dict[str, Any]:
69
+ role = _first_text(element, "role", "control_type", "type", "class_name", "localized_control_type") or "Unknown"
70
+ label = _first_text(element, "label", "name", "text", "title", "automation_id") or ""
71
+ value = _first_text(element, "value", "current_value", "text_value", "display_value")
72
+ is_password = _is_password_element(element, role, label)
73
+ focused = _truthy(_first_present(element, "focused", "is_focused", "has_keyboard_focus", "keyboard_focus"))
74
+
75
+ public: Dict[str, Any] = {
76
+ "id": _element_id(element, index),
77
+ "role": role,
78
+ }
79
+ if label:
80
+ public["label"] = label
81
+
82
+ if value is not None:
83
+ value_text = str(value)
84
+ public["filled"] = bool(value_text)
85
+ if is_password:
86
+ public["value"] = "***" if value_text else ""
87
+ public["value_kind"] = "password"
88
+ else:
89
+ public["value"] = _truncate(value_text, 80)
90
+ elif is_password:
91
+ public["value_kind"] = "password"
92
+
93
+ enabled = _optional_bool(element, "enabled", "is_enabled")
94
+ if enabled is not None:
95
+ public["enabled"] = enabled
96
+ visible = _optional_bool(element, "visible", "is_visible", "offscreen")
97
+ if visible is not None:
98
+ public["visible"] = visible
99
+
100
+ bounds = _compact_bounds(element)
101
+ if bounds:
102
+ public["bounds"] = bounds
103
+
104
+ public["_index"] = index
105
+ public["_score"] = _element_score(public, label, role, is_password, focused)
106
+ public["_focused"] = focused
107
+ return public
108
+
109
+
110
+ def _select_elements(
111
+ elements: list[Dict[str, Any]],
112
+ targets: Iterable[str] | str | None,
113
+ element_limit: int,
114
+ ) -> list[Dict[str, Any]]:
115
+ if element_limit <= 0 or not elements:
116
+ return []
117
+
118
+ target_terms = _target_terms(targets)
119
+ scored: list[tuple[int, int, Dict[str, Any]]] = []
120
+ for item in elements:
121
+ score = int(item.get("_score") or 0)
122
+ label = str(item.get("label") or "").lower()
123
+ role = str(item.get("role") or "").lower()
124
+ haystack = f"{label} {role}".strip()
125
+ for target in target_terms:
126
+ if target and target in haystack:
127
+ score += 5000
128
+ scored.append((score, -int(item.get("_index") or 0), item))
129
+
130
+ selected = [
131
+ item
132
+ for score, _negative_index, item in sorted(
133
+ scored,
134
+ key=lambda entry: (entry[0], entry[1]),
135
+ reverse=True,
136
+ )[:element_limit]
137
+ if score > 0
138
+ ]
139
+ selected.sort(key=lambda item: int(item.get("_index") or 0))
140
+ return selected
141
+
142
+
143
+ def _public_element(item: Dict[str, Any]) -> Dict[str, Any]:
144
+ return {key: value for key, value in item.items() if not key.startswith("_")}
145
+
146
+
147
+ def _element_score(
148
+ item: Dict[str, Any],
149
+ label: str,
150
+ role: str,
151
+ is_password: bool,
152
+ focused: bool,
153
+ ) -> int:
154
+ role_text = role.lower()
155
+ label_text = label.lower()
156
+ score = 0
157
+ if focused:
158
+ score += 900
159
+ if role_text in {"edit", "textbox", "text box", "document"}:
160
+ score += 700
161
+ if is_password:
162
+ score += 600
163
+ if role_text in {
164
+ "button",
165
+ "hyperlink",
166
+ "link",
167
+ "combobox",
168
+ "combo box",
169
+ "menuitem",
170
+ "menu item",
171
+ "checkbox",
172
+ "check box",
173
+ "radiobutton",
174
+ "radio button",
175
+ "tabitem",
176
+ "tab item",
177
+ "edit",
178
+ "textbox",
179
+ "text box",
180
+ }:
181
+ score += 400
182
+ if any(keyword in label_text for keyword in ("sign in", "login", "log in", "submit", "save", "ok", "continue", "next")):
183
+ score += 150
184
+ if item.get("enabled") is True:
185
+ score += 30
186
+ if item.get("visible") is True:
187
+ score += 20
188
+ return score
189
+
190
+
191
+ def _compact_focus(payload: Dict[str, Any], elements: list[Dict[str, Any]]) -> Dict[str, Any] | None:
192
+ focus = payload.get("focus") or payload.get("focused") or payload.get("active_element")
193
+ if isinstance(focus, dict):
194
+ return _public_element(_compact_element(focus, -1))
195
+ for item in elements:
196
+ if item.get("_focused"):
197
+ return _public_element(item)
198
+ return None
199
+
200
+
201
+ def _compact_blocking(payload: Dict[str, Any]) -> Dict[str, Any]:
202
+ top_window = payload.get("top_window") or payload.get("foreground_window") or payload.get("active_window")
203
+ overlay = payload.get("overlay") or payload.get("modal") or payload.get("dialog")
204
+ has_overlay = bool(payload.get("has_overlay") or payload.get("blocked") or payload.get("obscured") or overlay)
205
+ result: Dict[str, Any] = {"has_overlay": has_overlay}
206
+ if isinstance(top_window, dict):
207
+ result["top_window"] = _compact_window_like(top_window)
208
+ elif top_window:
209
+ result["top_window"] = str(top_window)
210
+ if isinstance(overlay, dict):
211
+ result["overlay"] = _compact_window_like(overlay)
212
+ elif overlay and not isinstance(overlay, bool):
213
+ result["overlay"] = str(overlay)
214
+ return result
215
+
216
+
217
+ def _compact_page(payload: Dict[str, Any]) -> Dict[str, Any]:
218
+ page: Dict[str, Any] = {}
219
+ url = _first_text(payload, "url", "page_url", "browser_url", "address")
220
+ if url:
221
+ page["url"] = url
222
+ page_title = _first_text(payload, "page_title", "document_title")
223
+ if page_title:
224
+ page["title"] = page_title
225
+ loading = _optional_bool(payload, "loading", "is_loading")
226
+ if loading is not None:
227
+ page["loading"] = loading
228
+ return page
229
+
230
+
231
+ def _compact_artifacts(payload: Dict[str, Any], trace_id: Any) -> Dict[str, Any]:
232
+ artifacts: Dict[str, Any] = {}
233
+ if trace_id:
234
+ artifacts["snapshot_id"] = str(trace_id)
235
+
236
+ screenshot = payload.get("screenshot")
237
+ if isinstance(screenshot, dict):
238
+ artifacts["screenshot"] = {
239
+ key: screenshot[key]
240
+ for key in ("local_path", "size_bytes", "width", "height")
241
+ if key in screenshot
242
+ }
243
+ elif isinstance(payload.get("screenshot_png_b64"), str):
244
+ artifacts["screenshot"] = {
245
+ "inline": True,
246
+ "size_bytes": len(payload["screenshot_png_b64"]),
247
+ }
248
+
249
+ if payload.get("tree_path"):
250
+ artifacts["tree_path"] = str(payload["tree_path"])
251
+ elif payload.get("tree_markdown"):
252
+ artifacts["tree_available"] = True
253
+ return artifacts
254
+
255
+
256
+ def _compact_window_like(value: Dict[str, Any]) -> Dict[str, Any]:
257
+ compact: Dict[str, Any] = {}
258
+ for key in ("pid", "window_id", "title", "name", "class_name"):
259
+ if value.get(key) not in (None, ""):
260
+ compact[key] = value[key]
261
+ return compact
262
+
263
+
264
+ def _element_id(element: Dict[str, Any], index: int) -> str:
265
+ value = _first_present(element, "id", "element_id", "element_index", "index")
266
+ if value not in (None, ""):
267
+ text = str(value)
268
+ return text if text.startswith("e") else f"e{text}"
269
+ return f"e{index}"
270
+
271
+
272
+ def _compact_bounds(element: Dict[str, Any]) -> Dict[str, int]:
273
+ bounds = element.get("bounds") or element.get("bbox") or element.get("rect")
274
+ if isinstance(bounds, dict):
275
+ x = _extract_int(bounds, "x", "left")
276
+ y = _extract_int(bounds, "y", "top")
277
+ width = _extract_int(bounds, "width", "w")
278
+ height = _extract_int(bounds, "height", "h")
279
+ right = _extract_int(bounds, "right")
280
+ bottom = _extract_int(bounds, "bottom")
281
+ if not width and right:
282
+ width = max(0, right - x)
283
+ if not height and bottom:
284
+ height = max(0, bottom - y)
285
+ result = {"x": x, "y": y}
286
+ if width:
287
+ result["width"] = width
288
+ if height:
289
+ result["height"] = height
290
+ return result
291
+ if isinstance(bounds, (list, tuple)) and len(bounds) >= 4:
292
+ try:
293
+ x1, y1, x2, y2 = [int(float(part)) for part in bounds[:4]]
294
+ except (TypeError, ValueError):
295
+ return {}
296
+ return {"x": x1, "y": y1, "width": max(0, x2 - x1), "height": max(0, y2 - y1)}
297
+ return {}
298
+
299
+
300
+ def _is_password_element(element: Dict[str, Any], role: str, label: str) -> bool:
301
+ if _truthy(_first_present(element, "is_password", "password", "isPassword")):
302
+ return True
303
+ role_text = role.lower()
304
+ input_like = role_text in {"edit", "textbox", "text box"} or "password" in role_text
305
+ if not input_like:
306
+ return False
307
+ text = f"{role} {label} {_first_text(element, 'automation_id', 'name') or ''}".lower()
308
+ return any(hint in text for hint in ("password", "passwd", "pwd", "pin", "\u5bc6\u7801", "\u53e3\u4ee4"))
@@ -0,0 +1,81 @@
1
+ """Result normalization helpers for agent-friendly commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Dict
7
+
8
+
9
+ def _normalize_task_result(data: Dict[str, Any], action: str) -> Dict[str, Any]:
10
+ result = data.get("result") if isinstance(data.get("result"), dict) else {}
11
+ output = result.get("output") if result else data.get("output")
12
+ parsed_output = _parse_json_if_possible(output)
13
+
14
+ stdout = ""
15
+ stderr = ""
16
+ exit_code = None
17
+ json_payload = None
18
+ error = ""
19
+ success_value = None
20
+
21
+ if isinstance(parsed_output, dict):
22
+ stdout = str(parsed_output.get("stdout") or "")
23
+ stderr = str(parsed_output.get("stderr") or "")
24
+ exit_code = parsed_output.get("exit_code", parsed_output.get("returncode"))
25
+ json_payload = parsed_output.get("json")
26
+ error = str(parsed_output.get("error") or "")
27
+ success_value = parsed_output.get("success")
28
+ elif isinstance(parsed_output, (list, dict)):
29
+ json_payload = parsed_output
30
+ elif isinstance(parsed_output, str):
31
+ stdout = parsed_output
32
+
33
+ if json_payload is None and stdout:
34
+ parsed_stdout = _parse_json_if_possible(stdout.strip())
35
+ if isinstance(parsed_stdout, (dict, list)):
36
+ json_payload = parsed_stdout
37
+
38
+ result_status = result.get("status") if result else data.get("status")
39
+ ok = data.get("status") == "done" or result_status in {"SUCCESS", "success", "ok"}
40
+ if result_status and str(result_status).upper() not in {"SUCCESS", "DONE", "OK"}:
41
+ ok = False
42
+ if success_value is False:
43
+ ok = False
44
+ if exit_code is not None:
45
+ try:
46
+ ok = ok and int(exit_code) == 0
47
+ except (TypeError, ValueError):
48
+ ok = False
49
+ if data.get("status") in {"failed", "timeout", "rejected"}:
50
+ ok = False
51
+
52
+ return {
53
+ "ok": bool(ok),
54
+ "action": action,
55
+ "trace_id": data.get("trace_id"),
56
+ "status": data.get("status"),
57
+ "result_status": result_status,
58
+ "stdout": stdout,
59
+ "stderr": stderr,
60
+ "exit_code": exit_code,
61
+ "json": json_payload,
62
+ "output": parsed_output,
63
+ "error": error or str(data.get("error") or data.get("reason") or ""),
64
+ "raw": data,
65
+ }
66
+
67
+
68
+ def _parse_json_if_possible(value: Any) -> Any:
69
+ if not isinstance(value, str):
70
+ return value
71
+ text = value.strip()
72
+ if not text:
73
+ return value
74
+ try:
75
+ return json.loads(text)
76
+ except json.JSONDecodeError:
77
+ return value
78
+
79
+
80
+ def _error(action: str, message: str, **extra: Any) -> Dict[str, Any]:
81
+ return {"ok": False, "action": action, "error": message, **extra}
@@ -0,0 +1,113 @@
1
+ """Shared helpers for agent-friendly remotecli wrappers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Iterable
6
+
7
+
8
+ def _coerce_int(value: Any) -> int:
9
+ try:
10
+ return int(value)
11
+ except (TypeError, ValueError):
12
+ return 0
13
+
14
+
15
+ def _coerce_float(value: Any) -> float:
16
+ try:
17
+ return float(value)
18
+ except (TypeError, ValueError):
19
+ return 0.0
20
+
21
+
22
+ def _normalize_mode(value: str, *, default: str) -> str:
23
+ mode = str(value or "").strip().lower()
24
+ return mode or default
25
+
26
+
27
+ def _normalize_scroll_direction(value: str) -> str:
28
+ direction = str(value or "down").strip().lower()
29
+ return direction if direction in {"up", "down", "left", "right"} else "down"
30
+
31
+
32
+ def _normalize_scroll_amount(value: Any) -> int:
33
+ try:
34
+ amount = int(value)
35
+ except (TypeError, ValueError):
36
+ amount = 3
37
+ return max(1, min(50, amount))
38
+
39
+
40
+ def _normalize_scroll_by(value: str) -> str:
41
+ by = str(value or "line").strip().lower()
42
+ return by if by in {"line", "page"} else "line"
43
+
44
+
45
+ def _extract_int(item: dict[str, Any], *keys: str) -> int:
46
+ for key in keys:
47
+ value = item.get(key)
48
+ if value in (None, ""):
49
+ continue
50
+ try:
51
+ return int(value)
52
+ except (TypeError, ValueError):
53
+ continue
54
+ return 0
55
+
56
+
57
+ def _target_terms(targets: Iterable[str] | str | None) -> list[str]:
58
+ if targets is None:
59
+ return []
60
+ if isinstance(targets, str):
61
+ raw_terms = targets.split(",")
62
+ else:
63
+ raw_terms = [str(term) for term in targets]
64
+ return [term.strip().lower() for term in raw_terms if term.strip()]
65
+
66
+
67
+ def _target_texts(targets: Iterable[str] | str | None) -> list[str]:
68
+ if targets is None:
69
+ return []
70
+ if isinstance(targets, str):
71
+ raw_terms = targets.split(",")
72
+ else:
73
+ raw_terms = [str(term) for term in targets]
74
+ return [term.strip() for term in raw_terms if term.strip()]
75
+
76
+
77
+ def _first_text(item: dict[str, Any], *keys: str) -> str | None:
78
+ value = _first_present(item, *keys)
79
+ if value in (None, ""):
80
+ return None
81
+ return str(value)
82
+
83
+
84
+ def _first_present(item: dict[str, Any], *keys: str) -> Any:
85
+ for key in keys:
86
+ if key in item:
87
+ return item[key]
88
+ return None
89
+
90
+
91
+ def _optional_bool(item: dict[str, Any], *keys: str) -> bool | None:
92
+ for key in keys:
93
+ if key not in item:
94
+ continue
95
+ value = _truthy(item[key])
96
+ return not value if key == "offscreen" else value
97
+ return None
98
+
99
+
100
+ def _truthy(value: Any) -> bool:
101
+ if isinstance(value, bool):
102
+ return value
103
+ if isinstance(value, (int, float)):
104
+ return value != 0
105
+ if isinstance(value, str):
106
+ return value.strip().lower() in {"1", "true", "yes", "y", "on"}
107
+ return bool(value)
108
+
109
+
110
+ def _truncate(value: str, max_length: int) -> str:
111
+ if len(value) <= max_length:
112
+ return value
113
+ return value[: max_length - 3] + "..."