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.
- agentremote_cli-1.0.0.dist-info/METADATA +7 -0
- agentremote_cli-1.0.0.dist-info/RECORD +25 -0
- agentremote_cli-1.0.0.dist-info/WHEEL +5 -0
- agentremote_cli-1.0.0.dist-info/entry_points.txt +2 -0
- agentremote_cli-1.0.0.dist-info/top_level.txt +1 -0
- remotecli/__init__.py +1 -0
- remotecli/agent.py +673 -0
- remotecli/agent_cache.py +274 -0
- remotecli/agent_modes.py +5 -0
- remotecli/agent_open.py +323 -0
- remotecli/agent_projection.py +308 -0
- remotecli/agent_result.py +81 -0
- remotecli/agent_utils.py +113 -0
- remotecli/agent_window.py +249 -0
- remotecli/cli.py +635 -0
- remotecli/client.py +120 -0
- remotecli/config/agentremote.cli.toml +6 -0
- remotecli/cua.py +213 -0
- remotecli/guides/GUIDE.md +397 -0
- remotecli/guides/agent.md +60 -0
- remotecli/guides/cua.md +60 -0
- remotecli/guides/rpa.md +45 -0
- remotecli/guides/shell.md +49 -0
- remotecli/rpa.py +57 -0
- remotecli/shell.py +71 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Window lookup and foreground helpers for agent commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Callable, Dict, Iterable
|
|
7
|
+
|
|
8
|
+
from .agent_cache import WindowCache, WindowRef
|
|
9
|
+
from .agent_result import _error
|
|
10
|
+
from .agent_utils import _extract_int, _first_present, _first_text, _truthy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
WaitAndUnpack = Callable[[Dict[str, Any], str, float, float], Dict[str, Any]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WindowResolver:
|
|
17
|
+
"""Resolve app queries into concrete pid/window_id pairs."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
client: Any,
|
|
22
|
+
cua: Any,
|
|
23
|
+
window_cache: WindowCache,
|
|
24
|
+
wait_and_unpack: WaitAndUnpack,
|
|
25
|
+
default_session_id: str = "",
|
|
26
|
+
) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
self._cua = cua
|
|
29
|
+
self._window_cache = window_cache
|
|
30
|
+
self._wait_and_unpack = wait_and_unpack
|
|
31
|
+
self._default_session_id = default_session_id
|
|
32
|
+
|
|
33
|
+
def resolve_window(
|
|
34
|
+
self,
|
|
35
|
+
app_query: str,
|
|
36
|
+
wait_timeout: float,
|
|
37
|
+
poll_interval: float,
|
|
38
|
+
session_id: str = "",
|
|
39
|
+
use_window_cache: bool = True,
|
|
40
|
+
refresh_window: bool = False,
|
|
41
|
+
) -> tuple[WindowRef | None, Dict[str, Any] | None]:
|
|
42
|
+
session = self.resolve_session_id(session_id)
|
|
43
|
+
if use_window_cache and session and not refresh_window:
|
|
44
|
+
cached_ref = self._window_cache.get(self._client, session, app_query)
|
|
45
|
+
if cached_ref:
|
|
46
|
+
return cached_ref, None
|
|
47
|
+
|
|
48
|
+
apps_result = self._wait_and_unpack(
|
|
49
|
+
self._cua.list_apps(app_query, max_results=5),
|
|
50
|
+
"list_apps",
|
|
51
|
+
wait_timeout,
|
|
52
|
+
poll_interval,
|
|
53
|
+
)
|
|
54
|
+
if not apps_result.get("ok"):
|
|
55
|
+
apps_result["action"] = "resolve_window"
|
|
56
|
+
return None, apps_result
|
|
57
|
+
|
|
58
|
+
apps = list(_extract_apps(apps_result.get("json")))
|
|
59
|
+
if not apps:
|
|
60
|
+
return None, _error("resolve_window", f"no app matched: {app_query}", app=app_query)
|
|
61
|
+
|
|
62
|
+
last_error: Dict[str, Any] | None = None
|
|
63
|
+
for app in _ranked_apps(apps, app_query):
|
|
64
|
+
pid = _extract_int(app, "pid", "process_id", "processId", "id")
|
|
65
|
+
if not pid:
|
|
66
|
+
last_error = _error("resolve_window", f"matched app has no pid: {app_query}", app=app)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
windows_result = self._wait_and_unpack(
|
|
70
|
+
self._cua.list_windows(pid),
|
|
71
|
+
"list_windows",
|
|
72
|
+
wait_timeout,
|
|
73
|
+
poll_interval,
|
|
74
|
+
)
|
|
75
|
+
if not windows_result.get("ok"):
|
|
76
|
+
windows_result["action"] = "resolve_window"
|
|
77
|
+
last_error = windows_result
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
windows = list(_extract_windows(windows_result.get("json")))
|
|
81
|
+
if not windows:
|
|
82
|
+
last_error = _error("resolve_window", f"no windows for pid: {pid}", pid=pid, app=app)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
window = _select_window(windows)
|
|
86
|
+
window_id = _extract_int(window, "window_id", "windowId", "id", "hwnd", "handle")
|
|
87
|
+
if not window_id:
|
|
88
|
+
last_error = _error("resolve_window", f"matched window has no window_id: {pid}", window=window)
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
ref = WindowRef(
|
|
92
|
+
pid=pid,
|
|
93
|
+
window_id=window_id,
|
|
94
|
+
title=str(window.get("title") or window.get("name") or ""),
|
|
95
|
+
app=app,
|
|
96
|
+
window=window,
|
|
97
|
+
)
|
|
98
|
+
if use_window_cache and session:
|
|
99
|
+
self._window_cache.put(self._client, session, app_query, ref)
|
|
100
|
+
return ref, None
|
|
101
|
+
|
|
102
|
+
return None, last_error or _error(
|
|
103
|
+
"resolve_window",
|
|
104
|
+
f"no usable app window matched: {app_query}",
|
|
105
|
+
app=app_query,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def foreground_window(
|
|
109
|
+
self,
|
|
110
|
+
ref: WindowRef,
|
|
111
|
+
wait_timeout: float,
|
|
112
|
+
poll_interval: float,
|
|
113
|
+
) -> None:
|
|
114
|
+
self._wait_and_unpack(
|
|
115
|
+
self._cua.bring_to_front(ref.pid, ref.window_id),
|
|
116
|
+
"bring_to_front",
|
|
117
|
+
wait_timeout,
|
|
118
|
+
poll_interval,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def refresh_window(
|
|
122
|
+
self,
|
|
123
|
+
app_query: str,
|
|
124
|
+
wait_timeout: float,
|
|
125
|
+
poll_interval: float,
|
|
126
|
+
session_id: str,
|
|
127
|
+
) -> tuple[WindowRef | None, Dict[str, Any] | None]:
|
|
128
|
+
session = self.resolve_session_id(session_id)
|
|
129
|
+
if session:
|
|
130
|
+
self._window_cache.remove_app(self._client, session, app_query)
|
|
131
|
+
return self.resolve_window(
|
|
132
|
+
app_query,
|
|
133
|
+
wait_timeout,
|
|
134
|
+
poll_interval,
|
|
135
|
+
session_id=session,
|
|
136
|
+
use_window_cache=True,
|
|
137
|
+
refresh_window=True,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def should_refresh_cached_window(
|
|
141
|
+
self,
|
|
142
|
+
ref: WindowRef,
|
|
143
|
+
result: Dict[str, Any],
|
|
144
|
+
session_id: str,
|
|
145
|
+
use_window_cache: bool,
|
|
146
|
+
) -> bool:
|
|
147
|
+
return bool(
|
|
148
|
+
ref.cached
|
|
149
|
+
and use_window_cache
|
|
150
|
+
and self.resolve_session_id(session_id)
|
|
151
|
+
and not result.get("ok")
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def resolve_session_id(self, session_id: str = "") -> str:
|
|
155
|
+
return (
|
|
156
|
+
session_id
|
|
157
|
+
or self._default_session_id
|
|
158
|
+
or os.environ.get("REMOTECLI_SESSION", "")
|
|
159
|
+
).strip()
|
|
160
|
+
|
|
161
|
+
def cache_context(self) -> Dict[str, Any]:
|
|
162
|
+
if not self._window_cache.last_error:
|
|
163
|
+
return {}
|
|
164
|
+
return {"window_cache_error": self._window_cache.last_error}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _extract_apps(payload: Any) -> Iterable[Dict[str, Any]]:
|
|
168
|
+
if isinstance(payload, list):
|
|
169
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
170
|
+
if not isinstance(payload, dict):
|
|
171
|
+
return []
|
|
172
|
+
for key in ("matches", "apps", "applications", "items", "processes"):
|
|
173
|
+
value = payload.get(key)
|
|
174
|
+
if isinstance(value, list):
|
|
175
|
+
return [item for item in value if isinstance(item, dict)]
|
|
176
|
+
return [payload] if _extract_int(payload, "pid", "process_id", "id") else []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _ranked_apps(apps: list[Dict[str, Any]], query: str) -> list[Dict[str, Any]]:
|
|
180
|
+
indexed = list(enumerate(apps))
|
|
181
|
+
ranked = sorted(
|
|
182
|
+
indexed,
|
|
183
|
+
key=lambda item: (_app_score(item[1], query), -item[0]),
|
|
184
|
+
reverse=True,
|
|
185
|
+
)
|
|
186
|
+
return [app for _index, app in ranked]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _app_score(app: Dict[str, Any], query: str) -> int:
|
|
190
|
+
score = 0
|
|
191
|
+
pid = _extract_int(app, "pid", "process_id", "processId", "id")
|
|
192
|
+
if pid:
|
|
193
|
+
score += 1000
|
|
194
|
+
else:
|
|
195
|
+
score -= 500
|
|
196
|
+
|
|
197
|
+
if _truthy(_first_present(app, "running", "is_running", "isRunning")):
|
|
198
|
+
score += 300
|
|
199
|
+
if _truthy(_first_present(app, "active", "is_active", "foreground", "is_foreground")):
|
|
200
|
+
score += 120
|
|
201
|
+
|
|
202
|
+
windows = app.get("windows")
|
|
203
|
+
if isinstance(windows, list) and windows:
|
|
204
|
+
score += 80
|
|
205
|
+
|
|
206
|
+
kind = _first_text(app, "kind", "type", "app_type", "appType", "source") or ""
|
|
207
|
+
kind_text = kind.lower()
|
|
208
|
+
if any(term in kind_text for term in ("desktop", "win32", "process")):
|
|
209
|
+
score += 80
|
|
210
|
+
if any(term in kind_text for term in ("uwp", "share", "appx")):
|
|
211
|
+
score -= 80
|
|
212
|
+
|
|
213
|
+
query_text = " ".join(str(query or "").lower().split())
|
|
214
|
+
identity = " ".join(
|
|
215
|
+
str(_first_present(app, key) or "")
|
|
216
|
+
for key in (
|
|
217
|
+
"name",
|
|
218
|
+
"title",
|
|
219
|
+
"display_name",
|
|
220
|
+
"process_name",
|
|
221
|
+
"app_id",
|
|
222
|
+
"appId",
|
|
223
|
+
"path",
|
|
224
|
+
)
|
|
225
|
+
).lower()
|
|
226
|
+
if query_text and query_text in " ".join(identity.split()):
|
|
227
|
+
score += 50
|
|
228
|
+
if query_text and query_text == " ".join(identity.split()):
|
|
229
|
+
score += 50
|
|
230
|
+
return score
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _extract_windows(payload: Any) -> Iterable[Dict[str, Any]]:
|
|
234
|
+
if isinstance(payload, list):
|
|
235
|
+
return [item for item in payload if isinstance(item, dict)]
|
|
236
|
+
if not isinstance(payload, dict):
|
|
237
|
+
return []
|
|
238
|
+
for key in ("windows", "items"):
|
|
239
|
+
value = payload.get(key)
|
|
240
|
+
if isinstance(value, list):
|
|
241
|
+
return [item for item in value if isinstance(item, dict)]
|
|
242
|
+
return [payload] if _extract_int(payload, "window_id", "id", "hwnd", "handle") else []
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _select_window(windows: list[Dict[str, Any]]) -> Dict[str, Any]:
|
|
246
|
+
for window in windows:
|
|
247
|
+
if str(window.get("title") or window.get("name") or "").strip():
|
|
248
|
+
return window
|
|
249
|
+
return windows[0]
|