vdisplay 0.1.5__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.
Files changed (80) hide show
  1. vdisplay/__init__.py +12 -0
  2. vdisplay/agent_config.py +71 -0
  3. vdisplay/agent_dispatch.py +30 -0
  4. vdisplay/agent_envelope.py +17 -0
  5. vdisplay/api.py +193 -0
  6. vdisplay/application/__init__.py +14 -0
  7. vdisplay/application/commands.py +183 -0
  8. vdisplay/application/errors.py +39 -0
  9. vdisplay/application/executor.py +51 -0
  10. vdisplay/application/handlers/__init__.py +6 -0
  11. vdisplay/application/handlers/agent.py +191 -0
  12. vdisplay/application/handlers/local.py +173 -0
  13. vdisplay/application/runtime.py +82 -0
  14. vdisplay/application/services/__init__.py +3 -0
  15. vdisplay/application/services/capture.py +182 -0
  16. vdisplay/application/services/discovery.py +236 -0
  17. vdisplay/application/services/img2nl_enrich.py +97 -0
  18. vdisplay/application/services/info.py +51 -0
  19. vdisplay/application/services/sampler.py +136 -0
  20. vdisplay/application/services/session.py +198 -0
  21. vdisplay/backends/__init__.py +1 -0
  22. vdisplay/backends/base.py +64 -0
  23. vdisplay/backends/linux_x11_mirror.py +259 -0
  24. vdisplay/backends/linux_x11_relay.py +478 -0
  25. vdisplay/backends/linux_xvfb.py +164 -0
  26. vdisplay/backends/mirror_stub.py +34 -0
  27. vdisplay/capture/__init__.py +15 -0
  28. vdisplay/capture/base.py +9 -0
  29. vdisplay/capture/host.py +484 -0
  30. vdisplay/capture/linux_xwd.py +320 -0
  31. vdisplay/capture/policy.py +116 -0
  32. vdisplay/capture/portal.py +221 -0
  33. vdisplay/capture/portal_screencast.py +690 -0
  34. vdisplay/capture/providers/__init__.py +3 -0
  35. vdisplay/capture/providers/base.py +22 -0
  36. vdisplay/capture/providers/drm.py +92 -0
  37. vdisplay/capture/providers/engine.py +99 -0
  38. vdisplay/capture/providers/fbdev.py +77 -0
  39. vdisplay/capture/providers/mss.py +60 -0
  40. vdisplay/capture/providers/x11.py +35 -0
  41. vdisplay/cli.py +32 -0
  42. vdisplay/cli_handlers.py +34 -0
  43. vdisplay/client.py +263 -0
  44. vdisplay/commands/__init__.py +42 -0
  45. vdisplay/commands/agent.py +104 -0
  46. vdisplay/commands/all_cmd.py +46 -0
  47. vdisplay/commands/common.py +35 -0
  48. vdisplay/commands/diagnose.py +26 -0
  49. vdisplay/commands/info.py +16 -0
  50. vdisplay/commands/io.py +7 -0
  51. vdisplay/commands/mirror.py +53 -0
  52. vdisplay/commands/monitors.py +19 -0
  53. vdisplay/commands/nlp.py +23 -0
  54. vdisplay/commands/relay.py +97 -0
  55. vdisplay/commands/sampler.py +68 -0
  56. vdisplay/commands/screenshot.py +53 -0
  57. vdisplay/commands/virtual.py +81 -0
  58. vdisplay/commands/windows.py +29 -0
  59. vdisplay/discovery.py +352 -0
  60. vdisplay/exceptions.py +10 -0
  61. vdisplay/input/__init__.py +3 -0
  62. vdisplay/input/linux_xdotool.py +45 -0
  63. vdisplay/models.py +26 -0
  64. vdisplay/nl.py +158 -0
  65. vdisplay/nlp.py +158 -0
  66. vdisplay/payloads.py +86 -0
  67. vdisplay/utils.py +46 -0
  68. vdisplay/windows/__init__.py +46 -0
  69. vdisplay/windows/constants.py +19 -0
  70. vdisplay/windows/filter.py +173 -0
  71. vdisplay/windows/normalize.py +103 -0
  72. vdisplay/windows/query.py +209 -0
  73. vdisplay/windows/rank.py +43 -0
  74. vdisplay/windows/scan.py +110 -0
  75. vdisplay-0.1.5.dist-info/METADATA +612 -0
  76. vdisplay-0.1.5.dist-info/RECORD +80 -0
  77. vdisplay-0.1.5.dist-info/WHEEL +5 -0
  78. vdisplay-0.1.5.dist-info/entry_points.txt +2 -0
  79. vdisplay-0.1.5.dist-info/licenses/LICENSE +201 -0
  80. vdisplay-0.1.5.dist-info/top_level.txt +1 -0
vdisplay/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from .api import MirrorSession, VirtualDisplaySession, WindowRelaySession, platform_summary
2
+ from .exceptions import BackendNotAvailableError, CapabilityError, VDisplayError
3
+
4
+ __all__ = [
5
+ "VirtualDisplaySession",
6
+ "MirrorSession",
7
+ "WindowRelaySession",
8
+ "platform_summary",
9
+ "VDisplayError",
10
+ "BackendNotAvailableError",
11
+ "CapabilityError",
12
+ ]
@@ -0,0 +1,71 @@
1
+ """Resolve vdisplay-agent connection (install-once broker for all client apps)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import urllib.error
7
+ import urllib.request
8
+
9
+ _PROBE_SENTINEL = object()
10
+ _probe_cache: str | None | object = _PROBE_SENTINEL
11
+
12
+
13
+ def agent_auto_enabled() -> bool:
14
+ return os.environ.get("VDISPLAY_AGENT_AUTO", "1").strip().lower() not in {
15
+ "0",
16
+ "false",
17
+ "no",
18
+ "off",
19
+ }
20
+
21
+
22
+ def reset_agent_probe_cache() -> None:
23
+ global _probe_cache
24
+ _probe_cache = _PROBE_SENTINEL
25
+
26
+
27
+ def _default_agent_base() -> str:
28
+ host = os.environ.get("VDISPLAY_AGENT_HOST", "127.0.0.1").strip() or "127.0.0.1"
29
+ port = os.environ.get("VDISPLAY_AGENT_PORT", "8765").strip() or "8765"
30
+ return f"http://{host}:{port}"
31
+
32
+
33
+ def _probe_agent_url(base_url: str, *, timeout: float = 0.2) -> str | None:
34
+ try:
35
+ with urllib.request.urlopen(f"{base_url.rstrip('/')}/health", timeout=timeout) as response:
36
+ if response.status == 200:
37
+ return base_url.rstrip("/")
38
+ except (urllib.error.URLError, TimeoutError, OSError, ValueError):
39
+ return None
40
+ return None
41
+
42
+
43
+ def _probe_default_agent() -> str | None:
44
+ global _probe_cache
45
+ if isinstance(_probe_cache, str):
46
+ return _probe_cache
47
+ url = _probe_agent_url(_default_agent_base())
48
+ if url:
49
+ _probe_cache = url
50
+ return url
51
+
52
+
53
+ def resolve_agent_url(explicit: str | None = None, *, allow_auto: bool = False) -> str | None:
54
+ """Return agent base URL when clients should use IPC instead of in-process capture."""
55
+ url = (explicit or os.environ.get("VDISPLAY_AGENT_URL") or "").strip()
56
+ if url:
57
+ return url.rstrip("/")
58
+ if not allow_auto or not agent_auto_enabled():
59
+ return None
60
+ return _probe_default_agent()
61
+
62
+
63
+ def resolve_agent_token() -> str | None:
64
+ token = (os.environ.get("VDISPLAY_AGENT_TOKEN") or "").strip()
65
+ return token or None
66
+
67
+
68
+ def use_agent(explicit: str | None = None) -> bool:
69
+ if os.environ.get("VDISPLAY_AGENT_BROKER", "").strip().lower() in {"1", "true", "yes"}:
70
+ return False
71
+ return resolve_agent_url(explicit, allow_auto=True) is not None
@@ -0,0 +1,30 @@
1
+ """Deprecated — route DSL verbs through application.executor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from typing import Any
7
+
8
+ from .agent_config import resolve_agent_url
9
+ from .application.commands import CommandRequest
10
+ from .application.executor import execute
11
+ from .client import AgentClient
12
+ from .exceptions import VDisplayError
13
+
14
+
15
+ def agent_client(url: str | None = None) -> AgentClient:
16
+ resolved = resolve_agent_url(url)
17
+ if not resolved:
18
+ raise VDisplayError("VDISPLAY_AGENT_URL is not set")
19
+ return AgentClient(resolved)
20
+
21
+
22
+ def dispatch_via_agent(cmd: dict[str, Any], *, line: str) -> Any:
23
+ """Execute parsed DSL command through vdisplay-agent (via executor)."""
24
+ warnings.warn(
25
+ "agent_dispatch.dispatch_via_agent is deprecated; use application.executor.execute",
26
+ DeprecationWarning,
27
+ stacklevel=2,
28
+ )
29
+ request = CommandRequest.from_dsl(cmd, line=line)
30
+ return execute(request, force_route="agent").to_dsl_result()
@@ -0,0 +1,17 @@
1
+ """Flatten vdisplay-agent JSON envelopes for SDK clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def flatten_agent_envelope(payload: dict[str, Any]) -> dict[str, Any]:
9
+ """Merge envelope.data to top level for backward-compatible clients."""
10
+ if "data" in payload and "action" in payload:
11
+ flat = dict(payload.get("data") or {})
12
+ if "ok" in payload:
13
+ flat["ok"] = payload["ok"]
14
+ if payload.get("error"):
15
+ flat["error"] = payload["error"]
16
+ return flat
17
+ return payload
vdisplay/api.py ADDED
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import platform
4
+ import sys
5
+ from dataclasses import asdict
6
+
7
+ from .backends.linux_x11_mirror import LinuxX11MirrorBackend
8
+ from .backends.linux_x11_relay import LinuxX11RelayBackend
9
+ from .backends.linux_xvfb import LinuxXvfbBackend
10
+ from .exceptions import BackendNotAvailableError
11
+
12
+
13
+ def _default_virtual_backend() -> str:
14
+ if sys.platform.startswith("linux"):
15
+ return "xvfb"
16
+ return "xvfb"
17
+
18
+
19
+ def _default_mirror_backend() -> str:
20
+ if sys.platform.startswith("linux"):
21
+ return "x11"
22
+ return "stub"
23
+
24
+
25
+ def _default_relay_backend() -> str:
26
+ if sys.platform.startswith("linux"):
27
+ return "x11"
28
+ return "x11"
29
+
30
+
31
+ class VirtualDisplaySession:
32
+ def __init__(self, backend) -> None:
33
+ self.backend = backend
34
+
35
+ @classmethod
36
+ def create(
37
+ cls,
38
+ width: int = 1920,
39
+ height: int = 1080,
40
+ backend: str | None = None,
41
+ display: str = ":99",
42
+ ):
43
+ backend = backend or _default_virtual_backend()
44
+ if backend == "xvfb":
45
+ if not sys.platform.startswith("linux"):
46
+ raise BackendNotAvailableError("xvfb backend is only available on Linux")
47
+ return cls(LinuxXvfbBackend(width=width, height=height, display=display))
48
+ raise BackendNotAvailableError(f"Unknown virtual backend: {backend}")
49
+
50
+ def start(self) -> None:
51
+ self.backend.start()
52
+
53
+ def stop(self) -> None:
54
+ self.backend.stop()
55
+
56
+ def launch(self, command):
57
+ return self.backend.launch(command)
58
+
59
+ def screenshot_bytes(self) -> bytes:
60
+ return self.backend.screenshot_bytes()
61
+
62
+ def save_screenshot(self, path: str) -> str:
63
+ return self.backend.save_screenshot(path)
64
+
65
+ def adopt_window(self, *, match_title: str | None = None, window_id: str | None = None) -> str:
66
+ return self.backend.adopt_window(match_title=match_title, window_id=window_id)
67
+
68
+ def release_window(self, *, match_title: str | None = None, window_id: str | None = None) -> str:
69
+ return self.backend.release_window(match_title=match_title, window_id=window_id)
70
+
71
+ def info(self) -> dict:
72
+ return asdict(self.backend.info())
73
+
74
+ def capabilities(self) -> dict:
75
+ return asdict(self.backend.capabilities())
76
+
77
+
78
+ class MirrorSession:
79
+ def __init__(self, backend) -> None:
80
+ self.backend = backend
81
+ self.pointer = getattr(backend, "pointer", None)
82
+
83
+ @classmethod
84
+ def create(
85
+ cls,
86
+ source: str = "primary",
87
+ target: str | None = None,
88
+ backend: str | None = None,
89
+ display: str | None = None,
90
+ ):
91
+ backend = backend or _default_mirror_backend()
92
+ if backend in {"x11", "linux-x11", "linux-x11-mirror"}:
93
+ if not sys.platform.startswith("linux"):
94
+ raise BackendNotAvailableError("linux-x11 mirror backend is only available on Linux")
95
+ return cls(LinuxX11MirrorBackend(source=source, target=target, display=display))
96
+ if backend == "stub":
97
+ from .backends.mirror_stub import MirrorStubBackend
98
+
99
+ return cls(MirrorStubBackend(source=source, target=target or "virtual:1"))
100
+ raise BackendNotAvailableError(f"Unknown mirror backend: {backend}")
101
+
102
+ def start(self) -> None:
103
+ self.backend.start()
104
+
105
+ def stop(self) -> None:
106
+ self.backend.stop()
107
+
108
+ def screenshot_bytes(self) -> bytes:
109
+ return self.backend.screenshot_bytes()
110
+
111
+ def save_screenshot(self, path: str) -> str:
112
+ return self.backend.save_screenshot(path)
113
+
114
+ def info(self) -> dict:
115
+ return asdict(self.backend.info())
116
+
117
+ def capabilities(self) -> dict:
118
+ return asdict(self.backend.capabilities())
119
+
120
+
121
+ class WindowRelaySession:
122
+ def __init__(self, backend) -> None:
123
+ self.backend = backend
124
+
125
+ @classmethod
126
+ def create(cls, backend: str | None = None, display: str | None = None):
127
+ backend = backend or _default_relay_backend()
128
+ if backend in {"x11", "linux-x11", "linux-x11-relay"}:
129
+ if not sys.platform.startswith("linux"):
130
+ raise BackendNotAvailableError("linux-x11 relay backend is only available on Linux")
131
+ return cls(LinuxX11RelayBackend(display=display))
132
+ raise BackendNotAvailableError(f"Unknown relay backend: {backend}")
133
+
134
+ def start(self) -> None:
135
+ self.backend.start()
136
+
137
+ def stop(self) -> None:
138
+ self.backend.stop()
139
+
140
+ def adopt_window(
141
+ self,
142
+ *,
143
+ match_title: str | None = None,
144
+ window_id: str | None = None,
145
+ match_class: str | None = None,
146
+ match_pid: int | None = None,
147
+ match_app: str | None = None,
148
+ target: str = "offscreen",
149
+ ) -> str:
150
+ return self.backend.adopt_window(
151
+ match_title=match_title,
152
+ window_id=window_id,
153
+ match_class=match_class,
154
+ match_pid=match_pid,
155
+ match_app=match_app,
156
+ target=target,
157
+ )
158
+
159
+ def release_window(
160
+ self,
161
+ *,
162
+ match_title: str | None = None,
163
+ window_id: str | None = None,
164
+ match_class: str | None = None,
165
+ match_pid: int | None = None,
166
+ match_app: str | None = None,
167
+ ) -> str:
168
+ return self.backend.release_window(
169
+ match_title=match_title,
170
+ window_id=window_id,
171
+ match_class=match_class,
172
+ match_pid=match_pid,
173
+ match_app=match_app,
174
+ )
175
+
176
+ def list_adopted(self) -> list[dict]:
177
+ return self.backend.list_adopted()
178
+
179
+ def info(self) -> dict:
180
+ return asdict(self.backend.info())
181
+
182
+ def capabilities(self) -> dict:
183
+ return asdict(self.backend.capabilities())
184
+
185
+
186
+ def platform_summary() -> dict:
187
+ return {
188
+ "platform": platform.system(),
189
+ "python": platform.python_version(),
190
+ "virtual_backend": _default_virtual_backend(),
191
+ "mirror_backend": _default_mirror_backend(),
192
+ "relay_backend": _default_relay_backend(),
193
+ }
@@ -0,0 +1,14 @@
1
+ """Application use-cases — single execution layer for all interfaces."""
2
+
3
+ from . import commands, errors
4
+ from .services import capture, discovery, info, session
5
+
6
+ __all__ = ["capture", "commands", "discovery", "errors", "executor", "info", "session"]
7
+
8
+
9
+ def __getattr__(name: str):
10
+ if name == "executor":
11
+ from . import executor as executor_mod
12
+
13
+ return executor_mod
14
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,183 @@
1
+ """Shared command model for CLI, DSL, REST, and agent client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from enum import StrEnum
8
+ from typing import Any
9
+
10
+ from .errors import ApplicationError
11
+
12
+
13
+ class CommandVerb(StrEnum):
14
+ HEALTH = "HEALTH"
15
+ INFO = "INFO"
16
+ OUTPUTS = "OUTPUTS"
17
+ MONITORS = "MONITORS"
18
+ WINDOWS = "WINDOWS"
19
+ ALL = "ALL"
20
+ CAPABILITIES = "CAPABILITIES"
21
+ VALIDATE = "VALIDATE"
22
+ SCREENSHOT = "SCREENSHOT"
23
+ VIRTUAL_START = "VIRTUAL_START"
24
+ VIRTUAL_STOP = "VIRTUAL_STOP"
25
+ LAUNCH = "LAUNCH"
26
+ MIRROR = "MIRROR"
27
+ ADOPT = "ADOPT"
28
+ RELEASE = "RELEASE"
29
+
30
+
31
+ QUERY_VERBS = frozenset(
32
+ {
33
+ CommandVerb.HEALTH,
34
+ CommandVerb.INFO,
35
+ CommandVerb.OUTPUTS,
36
+ CommandVerb.MONITORS,
37
+ CommandVerb.WINDOWS,
38
+ CommandVerb.ALL,
39
+ CommandVerb.CAPABILITIES,
40
+ CommandVerb.VALIDATE,
41
+ }
42
+ )
43
+
44
+ COMMAND_VERBS = frozenset(
45
+ {
46
+ CommandVerb.SCREENSHOT,
47
+ CommandVerb.VIRTUAL_START,
48
+ CommandVerb.VIRTUAL_STOP,
49
+ CommandVerb.LAUNCH,
50
+ CommandVerb.MIRROR,
51
+ CommandVerb.ADOPT,
52
+ CommandVerb.RELEASE,
53
+ }
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class CommandRequest:
59
+ verb: CommandVerb
60
+ line: str = ""
61
+ display: str | None = None
62
+ apps_only: bool = False
63
+ include_all: bool = True
64
+ match_class: str | None = None
65
+ match_pid: int | None = None
66
+ match_app: str | None = None
67
+ match_title: str | None = None
68
+ window_id: str | None = None
69
+ min_width: int = 0
70
+ min_height: int = 0
71
+ output: str | None = None
72
+ width: int = 1920
73
+ height: int = 1080
74
+ source: str | None = None
75
+ target: str | None = None
76
+ mode: str = "host"
77
+ all_monitors: bool = False
78
+ out_dir: str | None = None
79
+ vd_display: str = ":99"
80
+ backend: str = "xvfb"
81
+ monitor: int | None = None
82
+ local_only: bool = False
83
+ extra: dict[str, Any] = field(default_factory=dict)
84
+
85
+ @property
86
+ def action(self) -> str:
87
+ if self.verb == CommandVerb.OUTPUTS:
88
+ return "outputs"
89
+ return self.verb.value.lower()
90
+
91
+ @classmethod
92
+ def from_dsl(cls, cmd: dict[str, Any], *, line: str = "") -> CommandRequest:
93
+ verb_raw = str(cmd.get("verb", "HEALTH")).upper()
94
+ try:
95
+ verb = CommandVerb(verb_raw)
96
+ except ValueError:
97
+ verb = CommandVerb.HEALTH
98
+ apps_only = bool(cmd.get("apps_only", False))
99
+ return cls(
100
+ verb=verb,
101
+ line=line,
102
+ display=cmd.get("display"),
103
+ apps_only=apps_only,
104
+ include_all=not apps_only,
105
+ match_class=cmd.get("class"),
106
+ match_pid=cmd.get("pid"),
107
+ match_app=cmd.get("app"),
108
+ match_title=cmd.get("title"),
109
+ window_id=cmd.get("window_id"),
110
+ output=cmd.get("out"),
111
+ width=int(cmd.get("width", 1920)),
112
+ height=int(cmd.get("height", 1080)),
113
+ source=cmd.get("source"),
114
+ target=cmd.get("target"),
115
+ vd_display=str(cmd.get("display", ":99")),
116
+ backend=str(cmd.get("backend", "xvfb")),
117
+ extra={k: v for k, v in cmd.items() if k not in {"verb"}},
118
+ )
119
+
120
+
121
+ @dataclass
122
+ class CommandResult:
123
+ ok: bool
124
+ action: str
125
+ data: dict[str, Any] = field(default_factory=dict)
126
+ error: ApplicationError | None = None
127
+ meta: dict[str, Any] = field(default_factory=dict)
128
+ command: str = ""
129
+
130
+ def to_dict(self) -> dict[str, Any]:
131
+ payload: dict[str, Any] = {
132
+ "ok": self.ok,
133
+ "action": self.action,
134
+ "data": self.data,
135
+ "meta": self.meta,
136
+ }
137
+ if self.command:
138
+ payload["command"] = self.command
139
+ if self.error is not None:
140
+ payload["error"] = self.error.to_dict()
141
+ return payload
142
+
143
+ def to_dsl_result(self) -> Any:
144
+ from dsl2vdisplay.result import DslResult
145
+
146
+ return DslResult(
147
+ ok=self.ok,
148
+ command=self.command,
149
+ action=self.action,
150
+ output=json.dumps(self.data, indent=2, ensure_ascii=False) if self.ok else "",
151
+ data=self.data,
152
+ error=None if self.ok else (self.error.message if self.error else "unknown error"),
153
+ )
154
+
155
+ @classmethod
156
+ def success(
157
+ cls,
158
+ *,
159
+ action: str,
160
+ data: dict[str, Any],
161
+ command: str = "",
162
+ meta: dict[str, Any] | None = None,
163
+ ) -> CommandResult:
164
+ return cls(ok=True, action=action, data=data, command=command, meta=meta or {})
165
+
166
+ @classmethod
167
+ def failure(
168
+ cls,
169
+ *,
170
+ action: str,
171
+ error: ApplicationError,
172
+ data: dict[str, Any] | None = None,
173
+ command: str = "",
174
+ meta: dict[str, Any] | None = None,
175
+ ) -> CommandResult:
176
+ return cls(
177
+ ok=False,
178
+ action=action,
179
+ data=data or {},
180
+ error=error,
181
+ command=command,
182
+ meta=meta or {},
183
+ )
@@ -0,0 +1,39 @@
1
+ """Stable error codes for command execution and agent HTTP envelope."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import StrEnum
7
+ from typing import Any
8
+
9
+
10
+ class ErrorCode(StrEnum):
11
+ NOT_SUPPORTED = "not_supported"
12
+ DEPENDENCY_MISSING = "dependency_missing"
13
+ PERMISSION_REQUIRED = "permission_required"
14
+ SESSION_NOT_FOUND = "session_not_found"
15
+ INVALID_REQUEST = "invalid_request"
16
+ BACKEND_UNAVAILABLE = "backend_unavailable"
17
+ INTERNAL = "internal"
18
+
19
+
20
+ @dataclass
21
+ class ApplicationError:
22
+ code: ErrorCode
23
+ message: str
24
+ details: dict[str, Any] = field(default_factory=dict)
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ return {"code": self.code.value, "message": self.message, "details": self.details}
28
+
29
+
30
+ def error_from_exception(exc: Exception) -> ApplicationError:
31
+ from ..exceptions import BackendNotAvailableError, CapabilityError, VDisplayError
32
+
33
+ if isinstance(exc, CapabilityError):
34
+ return ApplicationError(ErrorCode.NOT_SUPPORTED, str(exc))
35
+ if isinstance(exc, BackendNotAvailableError):
36
+ return ApplicationError(ErrorCode.BACKEND_UNAVAILABLE, str(exc))
37
+ if isinstance(exc, VDisplayError):
38
+ return ApplicationError(ErrorCode.INVALID_REQUEST, str(exc))
39
+ return ApplicationError(ErrorCode.INTERNAL, str(exc))
@@ -0,0 +1,51 @@
1
+ """Execute CommandRequest via agent or local application services."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .commands import CommandRequest, CommandResult, CommandVerb
8
+ from .errors import error_from_exception
9
+ from .handlers import execute_agent, execute_local
10
+ from .runtime import ExecutionPolicy, Route, get_execution_policy
11
+ from ..exceptions import VDisplayError
12
+
13
+
14
+ def _maybe_enrich_screenshot(cmd: CommandRequest, data: dict[str, Any]) -> dict[str, Any]:
15
+ if cmd.verb != CommandVerb.SCREENSHOT:
16
+ return data
17
+ if cmd.extra.get("skip_img2nl"):
18
+ return data
19
+ from .services import img2nl_enrich
20
+
21
+ return img2nl_enrich.enrich_screenshot_payload(data)
22
+
23
+
24
+ def execute(
25
+ cmd: CommandRequest,
26
+ *,
27
+ policy: ExecutionPolicy | None = None,
28
+ force_route: Route | None = None,
29
+ ) -> CommandResult:
30
+ """Single entry for command execution across CLI, DSL, REST, and agent dispatch."""
31
+ pol = policy or get_execution_policy()
32
+ route = force_route or pol.route(cmd)
33
+ meta = pol.meta_for(route)
34
+ try:
35
+ data = execute_agent(cmd) if route == "agent" else execute_local(cmd)
36
+ data = _maybe_enrich_screenshot(cmd, data)
37
+ return CommandResult.success(action=cmd.action, data=data, command=cmd.line, meta=meta)
38
+ except VDisplayError as exc:
39
+ return CommandResult.failure(
40
+ action=cmd.action,
41
+ error=error_from_exception(exc),
42
+ command=cmd.line,
43
+ meta=meta,
44
+ )
45
+ except Exception as exc:
46
+ return CommandResult.failure(
47
+ action=cmd.action,
48
+ error=error_from_exception(exc),
49
+ command=cmd.line,
50
+ meta=meta,
51
+ )
@@ -0,0 +1,6 @@
1
+ """Per-verb command handlers for local and agent execution."""
2
+
3
+ from .agent import execute_agent
4
+ from .local import execute_local
5
+
6
+ __all__ = ["execute_agent", "execute_local"]