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
remotecli/agent_cache.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Persistent window cache for agent-friendly GUI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, Iterable
|
|
12
|
+
|
|
13
|
+
from .agent_utils import _coerce_float, _coerce_int
|
|
14
|
+
from .client import RemoteCliClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_WINDOW_CACHE_FILENAME = "remotecli_window_cache.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class WindowRef:
|
|
22
|
+
pid: int
|
|
23
|
+
window_id: int
|
|
24
|
+
title: str
|
|
25
|
+
app: Dict[str, Any]
|
|
26
|
+
window: Dict[str, Any]
|
|
27
|
+
cached: bool = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _default_window_cache_path() -> Path:
|
|
31
|
+
return _default_window_cache_paths()[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _default_window_cache_paths() -> list[Path]:
|
|
35
|
+
explicit_path = os.environ.get("REMOTECLI_WINDOW_CACHE", "").strip()
|
|
36
|
+
if explicit_path:
|
|
37
|
+
return [Path(explicit_path)]
|
|
38
|
+
|
|
39
|
+
candidates: list[Path] = []
|
|
40
|
+
base_dir = os.environ.get("LOCALAPPDATA", "").strip()
|
|
41
|
+
if base_dir:
|
|
42
|
+
candidates.append(Path(base_dir) / "AgentRemote" / _WINDOW_CACHE_FILENAME)
|
|
43
|
+
|
|
44
|
+
state_dir = os.environ.get("AGENTREMOTE_STATE_DIR", "").strip()
|
|
45
|
+
if state_dir:
|
|
46
|
+
candidates.append(Path(state_dir) / _WINDOW_CACHE_FILENAME)
|
|
47
|
+
|
|
48
|
+
runtime_state_dir = Path(__file__).resolve().parents[1] / "client" / "runtime_state"
|
|
49
|
+
candidates.append(runtime_state_dir / _WINDOW_CACHE_FILENAME)
|
|
50
|
+
candidates.append(Path.home() / ".agentremote" / _WINDOW_CACHE_FILENAME)
|
|
51
|
+
candidates.append(Path(tempfile.gettempdir()) / "AgentRemote" / _WINDOW_CACHE_FILENAME)
|
|
52
|
+
return _dedupe_paths(candidates)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _dedupe_paths(paths: Iterable[Path]) -> list[Path]:
|
|
56
|
+
result: list[Path] = []
|
|
57
|
+
seen: set[str] = set()
|
|
58
|
+
for path in paths:
|
|
59
|
+
key = os.path.normcase(str(path.expanduser()))
|
|
60
|
+
if key in seen:
|
|
61
|
+
continue
|
|
62
|
+
seen.add(key)
|
|
63
|
+
result.append(path)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _session_cache_key(client: RemoteCliClient, session: str) -> str:
|
|
68
|
+
return "|".join(
|
|
69
|
+
[
|
|
70
|
+
str(getattr(client, "middleware_url", "")),
|
|
71
|
+
str(getattr(client, "tenant_id", "")),
|
|
72
|
+
str(getattr(client, "node_id", "")),
|
|
73
|
+
session.strip(),
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _normalize_cache_key(value: str) -> str:
|
|
79
|
+
return " ".join(value.strip().lower().split())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class WindowCache:
|
|
83
|
+
"""Persistent session-scoped cache for app pid/window_id lookups."""
|
|
84
|
+
|
|
85
|
+
def __init__(self, path: str | Path | None = None) -> None:
|
|
86
|
+
self._paths = [Path(path)] if path else _default_window_cache_paths()
|
|
87
|
+
self.path = self._paths[0]
|
|
88
|
+
self.last_error = ""
|
|
89
|
+
|
|
90
|
+
def get(self, client: RemoteCliClient, session: str, app_query: str) -> WindowRef | None:
|
|
91
|
+
session_data = self._session_data(client, session)
|
|
92
|
+
apps = session_data.get("apps") if isinstance(session_data, dict) else None
|
|
93
|
+
if not isinstance(apps, dict):
|
|
94
|
+
return None
|
|
95
|
+
value = apps.get(_normalize_cache_key(app_query))
|
|
96
|
+
if not isinstance(value, dict):
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
pid = _coerce_int(value.get("pid"))
|
|
100
|
+
window_id = _coerce_int(value.get("window_id"))
|
|
101
|
+
if not pid or not window_id:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
app = value.get("app") if isinstance(value.get("app"), dict) else {}
|
|
105
|
+
window = value.get("window") if isinstance(value.get("window"), dict) else {}
|
|
106
|
+
return WindowRef(
|
|
107
|
+
pid=pid,
|
|
108
|
+
window_id=window_id,
|
|
109
|
+
title=str(value.get("title") or window.get("title") or ""),
|
|
110
|
+
app=app,
|
|
111
|
+
window=window,
|
|
112
|
+
cached=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def put(
|
|
116
|
+
self,
|
|
117
|
+
client: RemoteCliClient,
|
|
118
|
+
session: str,
|
|
119
|
+
app_query: str,
|
|
120
|
+
ref: WindowRef,
|
|
121
|
+
) -> None:
|
|
122
|
+
data = self._read()
|
|
123
|
+
sessions = data.setdefault("sessions", {})
|
|
124
|
+
if not isinstance(sessions, dict):
|
|
125
|
+
sessions = {}
|
|
126
|
+
data["sessions"] = sessions
|
|
127
|
+
session_key = _session_cache_key(client, session)
|
|
128
|
+
session_data = sessions.setdefault(
|
|
129
|
+
session_key,
|
|
130
|
+
{
|
|
131
|
+
"session": session,
|
|
132
|
+
"middleware_url": str(getattr(client, "middleware_url", "")),
|
|
133
|
+
"tenant_id": str(getattr(client, "tenant_id", "")),
|
|
134
|
+
"node_id": str(getattr(client, "node_id", "")),
|
|
135
|
+
"apps": {},
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
if not isinstance(session_data, dict):
|
|
139
|
+
session_data = {"session": session, "apps": {}}
|
|
140
|
+
sessions[session_key] = session_data
|
|
141
|
+
apps = session_data.setdefault("apps", {})
|
|
142
|
+
if not isinstance(apps, dict):
|
|
143
|
+
apps = {}
|
|
144
|
+
session_data["apps"] = apps
|
|
145
|
+
|
|
146
|
+
now = time.time()
|
|
147
|
+
session_data["updated_at"] = now
|
|
148
|
+
apps[_normalize_cache_key(app_query)] = {
|
|
149
|
+
"pid": ref.pid,
|
|
150
|
+
"window_id": ref.window_id,
|
|
151
|
+
"title": ref.title,
|
|
152
|
+
"app": ref.app,
|
|
153
|
+
"window": ref.window,
|
|
154
|
+
"updated_at": now,
|
|
155
|
+
}
|
|
156
|
+
try:
|
|
157
|
+
self._write(data)
|
|
158
|
+
except OSError as exc:
|
|
159
|
+
self.last_error = str(exc)
|
|
160
|
+
|
|
161
|
+
def remove_app(self, client: RemoteCliClient, session: str, app_query: str) -> None:
|
|
162
|
+
data = self._read()
|
|
163
|
+
session_data = self._session_data(client, session, data=data)
|
|
164
|
+
apps = session_data.get("apps") if isinstance(session_data, dict) else None
|
|
165
|
+
if isinstance(apps, dict):
|
|
166
|
+
apps.pop(_normalize_cache_key(app_query), None)
|
|
167
|
+
try:
|
|
168
|
+
self._write(data)
|
|
169
|
+
except OSError as exc:
|
|
170
|
+
self.last_error = str(exc)
|
|
171
|
+
|
|
172
|
+
def clear_session(self, client: RemoteCliClient, session: str) -> None:
|
|
173
|
+
data = self._read()
|
|
174
|
+
sessions = data.get("sessions")
|
|
175
|
+
if isinstance(sessions, dict):
|
|
176
|
+
sessions.pop(_session_cache_key(client, session), None)
|
|
177
|
+
try:
|
|
178
|
+
self._write(data)
|
|
179
|
+
except OSError as exc:
|
|
180
|
+
self.last_error = str(exc)
|
|
181
|
+
|
|
182
|
+
def _session_data(
|
|
183
|
+
self,
|
|
184
|
+
client: RemoteCliClient,
|
|
185
|
+
session: str,
|
|
186
|
+
data: Dict[str, Any] | None = None,
|
|
187
|
+
) -> Dict[str, Any]:
|
|
188
|
+
data = data if data is not None else self._read()
|
|
189
|
+
sessions = data.get("sessions")
|
|
190
|
+
if not isinstance(sessions, dict):
|
|
191
|
+
return {}
|
|
192
|
+
value = sessions.get(_session_cache_key(client, session))
|
|
193
|
+
return value if isinstance(value, dict) else {}
|
|
194
|
+
|
|
195
|
+
def _read(self) -> Dict[str, Any]:
|
|
196
|
+
data: Dict[str, Any] = {"version": 1, "sessions": {}}
|
|
197
|
+
for path in self._read_paths():
|
|
198
|
+
_merge_window_cache_data(data, self._read_path(path))
|
|
199
|
+
return data
|
|
200
|
+
|
|
201
|
+
def _read_paths(self) -> list[Path]:
|
|
202
|
+
return self._paths
|
|
203
|
+
|
|
204
|
+
def _read_path(self, path: Path) -> Dict[str, Any]:
|
|
205
|
+
try:
|
|
206
|
+
if not path.exists():
|
|
207
|
+
return {"version": 1, "sessions": {}}
|
|
208
|
+
loaded = json.loads(path.read_text(encoding="utf-8"))
|
|
209
|
+
except (OSError, json.JSONDecodeError):
|
|
210
|
+
return {"version": 1, "sessions": {}}
|
|
211
|
+
if not isinstance(loaded, dict):
|
|
212
|
+
return {"version": 1, "sessions": {}}
|
|
213
|
+
data = dict(loaded)
|
|
214
|
+
data.setdefault("sessions", {})
|
|
215
|
+
return data
|
|
216
|
+
|
|
217
|
+
def _write(self, data: Dict[str, Any]) -> None:
|
|
218
|
+
errors: list[str] = []
|
|
219
|
+
for path in self._paths:
|
|
220
|
+
try:
|
|
221
|
+
self._write_to_path(path, data)
|
|
222
|
+
except OSError as exc:
|
|
223
|
+
errors.append(f"{path}: {exc}")
|
|
224
|
+
continue
|
|
225
|
+
self.path = path
|
|
226
|
+
self.last_error = ""
|
|
227
|
+
return
|
|
228
|
+
raise OSError("; ".join(errors) or "no writable window cache path")
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _write_to_path(path: Path, data: Dict[str, Any]) -> None:
|
|
232
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
233
|
+
path.write_text(
|
|
234
|
+
json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True),
|
|
235
|
+
encoding="utf-8",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _merge_window_cache_data(target: Dict[str, Any], source: Dict[str, Any]) -> None:
|
|
240
|
+
source_sessions = source.get("sessions")
|
|
241
|
+
if not isinstance(source_sessions, dict):
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
target_sessions = target.setdefault("sessions", {})
|
|
245
|
+
if not isinstance(target_sessions, dict):
|
|
246
|
+
target_sessions = {}
|
|
247
|
+
target["sessions"] = target_sessions
|
|
248
|
+
|
|
249
|
+
for session_key, source_session in source_sessions.items():
|
|
250
|
+
if not isinstance(source_session, dict):
|
|
251
|
+
continue
|
|
252
|
+
target_session = target_sessions.get(session_key)
|
|
253
|
+
if not isinstance(target_session, dict):
|
|
254
|
+
target_sessions[session_key] = source_session
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
if _coerce_float(source_session.get("updated_at")) >= _coerce_float(target_session.get("updated_at")):
|
|
258
|
+
for key, value in source_session.items():
|
|
259
|
+
if key != "apps":
|
|
260
|
+
target_session[key] = value
|
|
261
|
+
|
|
262
|
+
source_apps = source_session.get("apps")
|
|
263
|
+
if not isinstance(source_apps, dict):
|
|
264
|
+
continue
|
|
265
|
+
target_apps = target_session.setdefault("apps", {})
|
|
266
|
+
if not isinstance(target_apps, dict):
|
|
267
|
+
target_apps = {}
|
|
268
|
+
target_session["apps"] = target_apps
|
|
269
|
+
for app_key, source_app in source_apps.items():
|
|
270
|
+
target_app = target_apps.get(app_key)
|
|
271
|
+
if not isinstance(target_app, dict) or _coerce_float(source_app.get("updated_at")) >= _coerce_float(
|
|
272
|
+
target_app.get("updated_at")
|
|
273
|
+
):
|
|
274
|
+
target_apps[app_key] = source_app
|
remotecli/agent_modes.py
ADDED
remotecli/agent_open.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Optional fast app-open helper for agent commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict
|
|
6
|
+
|
|
7
|
+
from .agent_cache import WindowCache, WindowRef
|
|
8
|
+
from .agent_result import _error
|
|
9
|
+
from .agent_utils import _extract_int
|
|
10
|
+
from .client import RemoteCliClient
|
|
11
|
+
from .shell import ShellCommands
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
WaitAndUnpack = Callable[[Dict[str, Any], str, float, float], Dict[str, Any]]
|
|
15
|
+
|
|
16
|
+
_APP_ALIASES: dict[str, tuple[str, ...]] = {
|
|
17
|
+
"firefox": ("firefox", "mozilla firefox", "火狐", "火狐浏览器"),
|
|
18
|
+
"chrome": ("chrome", "google chrome", "谷歌", "谷歌浏览器"),
|
|
19
|
+
"edge": ("edge", "microsoft edge", "msedge", "微软浏览器", "edge浏览器"),
|
|
20
|
+
"wechat": ("wechat", "weixin", "微信"),
|
|
21
|
+
"wecom": ("wecom", "wxwork", "企业微信", "企微"),
|
|
22
|
+
"notepad": ("notepad", "notepad.exe", "记事本"),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def open_app_window(
|
|
27
|
+
client: RemoteCliClient,
|
|
28
|
+
shell: ShellCommands,
|
|
29
|
+
window_cache: WindowCache,
|
|
30
|
+
wait_and_unpack: WaitAndUnpack,
|
|
31
|
+
app_query: str,
|
|
32
|
+
*,
|
|
33
|
+
session_id: str = "",
|
|
34
|
+
use_window_cache: bool = True,
|
|
35
|
+
launch_path: str = "",
|
|
36
|
+
app_args: str = "",
|
|
37
|
+
start_if_missing: bool = True,
|
|
38
|
+
foreground: bool = True,
|
|
39
|
+
launch_timeout: int = 15,
|
|
40
|
+
wait_timeout: float = 30.0,
|
|
41
|
+
poll_interval: float = 0.1,
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
app_query = str(app_query or "").strip()
|
|
44
|
+
if not app_query:
|
|
45
|
+
return _error("open", "app query is required")
|
|
46
|
+
|
|
47
|
+
script = build_open_app_script(
|
|
48
|
+
app_query,
|
|
49
|
+
launch_path=launch_path,
|
|
50
|
+
app_args=app_args,
|
|
51
|
+
start_if_missing=start_if_missing,
|
|
52
|
+
foreground=foreground,
|
|
53
|
+
launch_timeout=launch_timeout,
|
|
54
|
+
)
|
|
55
|
+
result = wait_and_unpack(
|
|
56
|
+
shell.exec_script(script, shell="powershell", timeout_sec=max(60, launch_timeout + 10)),
|
|
57
|
+
"open",
|
|
58
|
+
wait_timeout,
|
|
59
|
+
poll_interval,
|
|
60
|
+
)
|
|
61
|
+
payload = result.get("json") if isinstance(result.get("json"), dict) else {}
|
|
62
|
+
ok = bool(result.get("ok")) and bool(payload.get("ok", True))
|
|
63
|
+
pid = _extract_int(payload, "pid", "process_id", "processId", "id")
|
|
64
|
+
window_id = _extract_int(payload, "window_id", "windowId", "hwnd", "handle")
|
|
65
|
+
title = str(payload.get("title") or payload.get("MainWindowTitle") or "")
|
|
66
|
+
|
|
67
|
+
if not ok or not pid or not window_id:
|
|
68
|
+
return {
|
|
69
|
+
"ok": False,
|
|
70
|
+
"action": "open",
|
|
71
|
+
"app": app_query,
|
|
72
|
+
"pid": pid,
|
|
73
|
+
"window_id": window_id,
|
|
74
|
+
"title": title,
|
|
75
|
+
"started": bool(payload.get("started")),
|
|
76
|
+
"source": str(payload.get("source") or ""),
|
|
77
|
+
"error": str(payload.get("error") or result.get("error") or "open did not return a usable window"),
|
|
78
|
+
"result": result,
|
|
79
|
+
**_cache_context(window_cache),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
app = {
|
|
83
|
+
"name": app_query,
|
|
84
|
+
"pid": pid,
|
|
85
|
+
"running": True,
|
|
86
|
+
"source": payload.get("source") or "shell_open",
|
|
87
|
+
"launch_target": payload.get("launch_target") or "",
|
|
88
|
+
}
|
|
89
|
+
window = {
|
|
90
|
+
"window_id": window_id,
|
|
91
|
+
"title": title,
|
|
92
|
+
"pid": pid,
|
|
93
|
+
"source": payload.get("source") or "shell_open",
|
|
94
|
+
}
|
|
95
|
+
ref = WindowRef(pid=pid, window_id=window_id, title=title, app=app, window=window)
|
|
96
|
+
if use_window_cache and session_id:
|
|
97
|
+
window_cache.put(client, session_id, app_query, ref)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"ok": True,
|
|
101
|
+
"action": "open",
|
|
102
|
+
"app": app_query,
|
|
103
|
+
"started": bool(payload.get("started")),
|
|
104
|
+
"pid": pid,
|
|
105
|
+
"window_id": window_id,
|
|
106
|
+
"title": title,
|
|
107
|
+
"source": str(payload.get("source") or "shell_open"),
|
|
108
|
+
"launch_target": str(payload.get("launch_target") or ""),
|
|
109
|
+
"window_cached": False,
|
|
110
|
+
"result": result,
|
|
111
|
+
**_cache_context(window_cache),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_open_app_script(
|
|
116
|
+
app_query: str,
|
|
117
|
+
*,
|
|
118
|
+
launch_path: str = "",
|
|
119
|
+
app_args: str = "",
|
|
120
|
+
start_if_missing: bool = True,
|
|
121
|
+
foreground: bool = True,
|
|
122
|
+
launch_timeout: int = 15,
|
|
123
|
+
) -> str:
|
|
124
|
+
terms = _search_terms(app_query)
|
|
125
|
+
terms_literal = ", ".join(_ps_quote(term) for term in terms)
|
|
126
|
+
return f"""
|
|
127
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
128
|
+
$query = {_ps_quote(app_query)}
|
|
129
|
+
$launchPath = {_ps_quote(launch_path)}
|
|
130
|
+
$appArgs = {_ps_quote(app_args)}
|
|
131
|
+
$startIfMissing = {_ps_bool(start_if_missing)}
|
|
132
|
+
$foreground = {_ps_bool(foreground)}
|
|
133
|
+
$launchTimeoutSec = {max(1, int(launch_timeout))}
|
|
134
|
+
$terms = @({terms_literal})
|
|
135
|
+
|
|
136
|
+
function Test-AgentRemoteTextMatch($value) {{
|
|
137
|
+
$text = [string]$value
|
|
138
|
+
if ([string]::IsNullOrWhiteSpace($text)) {{ return $false }}
|
|
139
|
+
$lower = $text.ToLowerInvariant()
|
|
140
|
+
foreach ($term in $terms) {{
|
|
141
|
+
if ($lower.Contains(([string]$term).ToLowerInvariant())) {{ return $true }}
|
|
142
|
+
}}
|
|
143
|
+
return $false
|
|
144
|
+
}}
|
|
145
|
+
|
|
146
|
+
function Test-AgentRemoteProcessMatch($proc) {{
|
|
147
|
+
if (-not $proc) {{ return $false }}
|
|
148
|
+
if (Test-AgentRemoteTextMatch $proc.ProcessName) {{ return $true }}
|
|
149
|
+
if (Test-AgentRemoteTextMatch $proc.MainWindowTitle) {{ return $true }}
|
|
150
|
+
try {{
|
|
151
|
+
if (Test-AgentRemoteTextMatch $proc.Path) {{ return $true }}
|
|
152
|
+
}} catch {{}}
|
|
153
|
+
return $false
|
|
154
|
+
}}
|
|
155
|
+
|
|
156
|
+
function Find-AgentRemoteWindow() {{
|
|
157
|
+
Get-Process |
|
|
158
|
+
Where-Object {{
|
|
159
|
+
$_.MainWindowTitle -ne "" -and
|
|
160
|
+
[int64]$_.MainWindowHandle -ne 0 -and
|
|
161
|
+
(Test-AgentRemoteProcessMatch $_)
|
|
162
|
+
}} |
|
|
163
|
+
Select-Object -First 1
|
|
164
|
+
}}
|
|
165
|
+
|
|
166
|
+
function Set-AgentRemoteForeground($hwnd) {{
|
|
167
|
+
if (-not $foreground -or -not $hwnd -or [int64]$hwnd -eq 0) {{ return $false }}
|
|
168
|
+
try {{
|
|
169
|
+
$type = @"
|
|
170
|
+
using System;
|
|
171
|
+
using System.Runtime.InteropServices;
|
|
172
|
+
public static class AgentRemoteUser32 {{
|
|
173
|
+
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
174
|
+
[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
|
|
175
|
+
}}
|
|
176
|
+
"@
|
|
177
|
+
Add-Type -TypeDefinition $type -ErrorAction SilentlyContinue | Out-Null
|
|
178
|
+
[AgentRemoteUser32]::ShowWindowAsync([IntPtr][int64]$hwnd, 9) | Out-Null
|
|
179
|
+
return [AgentRemoteUser32]::SetForegroundWindow([IntPtr][int64]$hwnd)
|
|
180
|
+
}} catch {{
|
|
181
|
+
return $false
|
|
182
|
+
}}
|
|
183
|
+
}}
|
|
184
|
+
|
|
185
|
+
function Find-AgentRemoteStartApp() {{
|
|
186
|
+
Get-StartApps |
|
|
187
|
+
Where-Object {{
|
|
188
|
+
Test-AgentRemoteTextMatch $_.Name -or Test-AgentRemoteTextMatch $_.AppID
|
|
189
|
+
}} |
|
|
190
|
+
Select-Object -First 1
|
|
191
|
+
}}
|
|
192
|
+
|
|
193
|
+
function Find-AgentRemoteCommand() {{
|
|
194
|
+
foreach ($term in $terms) {{
|
|
195
|
+
$clean = ([string]$term).Trim()
|
|
196
|
+
if (-not $clean -or $clean.Contains(" ")) {{ continue }}
|
|
197
|
+
$cmd = Get-Command ($clean + ".exe") -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
198
|
+
if ($cmd) {{ return $cmd }}
|
|
199
|
+
$cmd = Get-Command $clean -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
200
|
+
if ($cmd) {{ return $cmd }}
|
|
201
|
+
}}
|
|
202
|
+
return $null
|
|
203
|
+
}}
|
|
204
|
+
|
|
205
|
+
$started = $false
|
|
206
|
+
$source = "running_process"
|
|
207
|
+
$launchTarget = ""
|
|
208
|
+
$proc = Find-AgentRemoteWindow
|
|
209
|
+
|
|
210
|
+
if (-not $proc) {{
|
|
211
|
+
if (-not $startIfMissing) {{
|
|
212
|
+
[pscustomobject]@{{ ok=$false; app=$query; started=$false; error="window not found"; source="not_started" }} |
|
|
213
|
+
ConvertTo-Json -Compress
|
|
214
|
+
exit 0
|
|
215
|
+
}}
|
|
216
|
+
|
|
217
|
+
if ($launchPath) {{
|
|
218
|
+
if (Test-Path -LiteralPath $launchPath) {{
|
|
219
|
+
if ($appArgs) {{
|
|
220
|
+
Start-Process -FilePath $launchPath -ArgumentList $appArgs
|
|
221
|
+
}} else {{
|
|
222
|
+
Start-Process -FilePath $launchPath
|
|
223
|
+
}}
|
|
224
|
+
$started = $true
|
|
225
|
+
$source = "path"
|
|
226
|
+
$launchTarget = $launchPath
|
|
227
|
+
}}
|
|
228
|
+
}}
|
|
229
|
+
|
|
230
|
+
if (-not $started) {{
|
|
231
|
+
$startApp = Find-AgentRemoteStartApp
|
|
232
|
+
if ($startApp) {{
|
|
233
|
+
$launchTarget = "shell:AppsFolder\\" + $startApp.AppID
|
|
234
|
+
if ($appArgs) {{
|
|
235
|
+
Start-Process $launchTarget -ArgumentList $appArgs
|
|
236
|
+
}} else {{
|
|
237
|
+
Start-Process $launchTarget
|
|
238
|
+
}}
|
|
239
|
+
$started = $true
|
|
240
|
+
$source = "start_apps"
|
|
241
|
+
}}
|
|
242
|
+
}}
|
|
243
|
+
|
|
244
|
+
if (-not $started) {{
|
|
245
|
+
$cmd = Find-AgentRemoteCommand
|
|
246
|
+
if ($cmd) {{
|
|
247
|
+
if ($appArgs) {{
|
|
248
|
+
Start-Process -FilePath $cmd.Source -ArgumentList $appArgs
|
|
249
|
+
}} else {{
|
|
250
|
+
Start-Process -FilePath $cmd.Source
|
|
251
|
+
}}
|
|
252
|
+
$started = $true
|
|
253
|
+
$source = "command"
|
|
254
|
+
$launchTarget = $cmd.Source
|
|
255
|
+
}}
|
|
256
|
+
}}
|
|
257
|
+
|
|
258
|
+
if (-not $started) {{
|
|
259
|
+
[pscustomobject]@{{ ok=$false; app=$query; started=$false; error="app launch target not found"; source="not_found" }} |
|
|
260
|
+
ConvertTo-Json -Compress
|
|
261
|
+
exit 0
|
|
262
|
+
}}
|
|
263
|
+
|
|
264
|
+
$deadline = (Get-Date).AddSeconds($launchTimeoutSec)
|
|
265
|
+
do {{
|
|
266
|
+
Start-Sleep -Milliseconds 100
|
|
267
|
+
$proc = Find-AgentRemoteWindow
|
|
268
|
+
}} while (-not $proc -and (Get-Date) -lt $deadline)
|
|
269
|
+
}}
|
|
270
|
+
|
|
271
|
+
if (-not $proc) {{
|
|
272
|
+
[pscustomobject]@{{ ok=$false; app=$query; started=$started; error="window not found after launch"; source=$source; launch_target=$launchTarget }} |
|
|
273
|
+
ConvertTo-Json -Compress
|
|
274
|
+
exit 0
|
|
275
|
+
}}
|
|
276
|
+
|
|
277
|
+
$foregrounded = Set-AgentRemoteForeground $proc.MainWindowHandle
|
|
278
|
+
[pscustomobject]@{{
|
|
279
|
+
ok = $true
|
|
280
|
+
app = $query
|
|
281
|
+
started = $started
|
|
282
|
+
pid = [int]$proc.Id
|
|
283
|
+
window_id = [int64]$proc.MainWindowHandle
|
|
284
|
+
title = [string]$proc.MainWindowTitle
|
|
285
|
+
process = [string]$proc.ProcessName
|
|
286
|
+
source = $source
|
|
287
|
+
launch_target = $launchTarget
|
|
288
|
+
foregrounded = [bool]$foregrounded
|
|
289
|
+
}} | ConvertTo-Json -Compress
|
|
290
|
+
""".strip()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _search_terms(app_query: str) -> list[str]:
|
|
294
|
+
query = " ".join(str(app_query or "").strip().split())
|
|
295
|
+
lowered = query.casefold()
|
|
296
|
+
terms: list[str] = []
|
|
297
|
+
|
|
298
|
+
def add(value: str) -> None:
|
|
299
|
+
text = " ".join(str(value or "").strip().split())
|
|
300
|
+
if text and text not in terms:
|
|
301
|
+
terms.append(text)
|
|
302
|
+
|
|
303
|
+
add(query)
|
|
304
|
+
for aliases in _APP_ALIASES.values():
|
|
305
|
+
normalized_aliases = [alias.casefold() for alias in aliases]
|
|
306
|
+
if any(lowered == alias or lowered in alias or alias in lowered for alias in normalized_aliases):
|
|
307
|
+
for alias in aliases:
|
|
308
|
+
add(alias)
|
|
309
|
+
return terms
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _ps_quote(value: Any) -> str:
|
|
313
|
+
return "'" + str(value or "").replace("'", "''") + "'"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _ps_bool(value: bool) -> str:
|
|
317
|
+
return "$true" if value else "$false"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _cache_context(window_cache: WindowCache) -> Dict[str, Any]:
|
|
321
|
+
if not window_cache.last_error:
|
|
322
|
+
return {}
|
|
323
|
+
return {"window_cache_error": window_cache.last_error}
|