glasswarp 0.1.0__tar.gz

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,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .DS_Store
10
+ # Local demo captures (not part of the published package)
11
+ gemini_*.jpg
12
+ paint_*.jpg
13
+ examples/vp0_out/
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-07-22
4
+
5
+ Initial public release on PyPI.
6
+
7
+ - `GlasswarpClient` over Platform API `/v1/*` (`https://signal.glasswarp.com`)
8
+ - Sessions, observe / screenshot, native input, app launch/kill
9
+ - Grounding helpers (`som_annotate`, `targets_from_grid`, `click_target`) via optional `glasswarp[grounding]`
10
+ - Typed package (`py.typed`)
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Woodrow Stores LLC
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: glasswarp
3
+ Version: 0.1.0
4
+ Summary: Eyes and hands for AI agents on real Windows — Python SDK for the Glasswarp Platform API.
5
+ Project-URL: Homepage, https://www.glasswarp.com
6
+ Project-URL: Documentation, https://docs.glasswarp.com
7
+ Project-URL: Repository, https://github.com/saferelay/project-x
8
+ Project-URL: Issues, https://github.com/saferelay/project-x/issues
9
+ Project-URL: Changelog, https://github.com/saferelay/project-x/blob/main/sdk/python/CHANGELOG.md
10
+ Author-email: Woodrow Stores LLC <hello@glasswarp.com>
11
+ License-Expression: Apache-2.0
12
+ License-File: LICENSE
13
+ Keywords: agents,automation,computer-use,glasswarp,mcp,screenshot,windows
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: httpx>=0.24
28
+ Provides-Extra: dev
29
+ Requires-Dist: build>=1.2; extra == 'dev'
30
+ Requires-Dist: pytest-httpx>=0.21; extra == 'dev'
31
+ Requires-Dist: pytest>=7; extra == 'dev'
32
+ Requires-Dist: twine>=5; extra == 'dev'
33
+ Provides-Extra: grounding
34
+ Requires-Dist: pillow>=10; extra == 'grounding'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # Glasswarp Python SDK
38
+
39
+ See any screen. Control any PC. One API.
40
+
41
+ Glasswarp gives your agent **eyes and hands** on a real Windows machine; you
42
+ supply the **brain** (model, prompts, CV, solvers). Example task logic lives under
43
+ `examples/` and outcome-named starters under `templates/` — not in the platform.
44
+ Public plans: Free / Builder / Growth / Enterprise. Game Mode at `/play` is a frozen
45
+ demo of the same capture pipeline.
46
+
47
+ ```bash
48
+ pip install glasswarp
49
+ # optional SoM / image helpers:
50
+ pip install "glasswarp[grounding]"
51
+ ```
52
+
53
+ > **Publish status:** packaging targets PyPI as `glasswarp` (`0.1.0`). Until the
54
+ > first upload lands, install from this repo: `pip install -e sdk/python`.
55
+ > Operator steps: [`docs/PYPI_PUBLISH.md`](../../docs/PYPI_PUBLISH.md).
56
+
57
+ Want Claude / Cursor with **no project code**? Use the remote MCP server instead —
58
+ [glasswarp.com/mcp](https://www.glasswarp.com/mcp) ·
59
+ [docs](https://docs.glasswarp.com/get-started/mcp).
60
+
61
+ ## Quickstart
62
+
63
+ ```python
64
+ from glasswarp import GlasswarpClient
65
+
66
+ gw = GlasswarpClient(api_key="gw_live_sk_...")
67
+
68
+ # List your rigs
69
+ rigs = gw.list_rigs()
70
+ rig = rigs[0]
71
+
72
+ # Start a desktop session
73
+ session = gw.create_session(rig_id=rig.id)
74
+
75
+ # Take a screenshot (returns JPEG bytes)
76
+ screenshot = gw.screenshot(session.session_id)
77
+ with open("screen.jpg", "wb") as f:
78
+ f.write(screenshot.jpeg)
79
+
80
+ # Send input
81
+ gw.click(session.session_id, x=500, y=400)
82
+ gw.type_text(session.session_id, "Hello from Python!")
83
+ gw.key_press(session.session_id, "Enter")
84
+
85
+ # End the session
86
+ gw.end_session(session.session_id)
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ | Parameter | Env var | Default |
92
+ |-----------|---------|---------|
93
+ | `api_key` | `GLASSWARP_API_KEY` | (required) |
94
+ | `base_url` | `GLASSWARP_BASE_URL` | `https://signal.glasswarp.com` |
95
+
96
+ ## API reference
97
+
98
+ ### Rigs
99
+
100
+ - `list_rigs()` — all rigs accessible to this key
101
+ - `get_rig(rig_id)` — single rig detail
102
+
103
+ ### Sessions
104
+
105
+ - `create_session(rig_id, mode="desktop")` — start a session
106
+ - `get_session(session_id)` — current status
107
+ - `list_sessions()` — all active sessions
108
+ - `end_session(session_id)` — end and trigger safety_restore
109
+
110
+ ### Visual
111
+
112
+ - `screenshot(session_id, max_width=None, quality=None, x=y=w=h=None)` — JPEG; optional ROI crop
113
+ - `dirty_rects(session_id)` — accumulated DXGI dirty rects since last poll (VP0)
114
+ - `list_targets(session_id)` — sparse click targets from host UIA (P8); empty if none
115
+ - `observe(session_id, mark=True, targets=None, …)` — one RTT: JPEG + dirty + targets (SoM when marked)
116
+
117
+ ### Grounding (SoM)
118
+
119
+ Agents should click **targets**, not guessed pixels:
120
+
121
+ ```python
122
+ from glasswarp import targets_from_grid, som_annotate
123
+
124
+ # Host UIA targets (P8) or client CV/grid:
125
+ targets = targets_from_grid(x0=100, y0=200, x1=900, y1=800, rows=8, cols=8)
126
+ obs = gw.observe(session.session_id, max_width=1280, mark=True, targets=targets)
127
+ # obs.jpeg is Set-of-Mark annotated; model returns target id →
128
+ gw.click_target(session.session_id, "12", targets=targets)
129
+ ```
130
+
131
+ - `som_annotate(jpeg, targets, native_width=…, native_height=…)` — draw numbered marks (needs `pip install pillow`)
132
+ - `targets_from_grid(…)` — build cell targets for a rectangular board
133
+ - `click_target(session_id, target_or_id, targets=…)` — click bbox center
134
+
135
+ ### Apps
136
+
137
+ - `launch_app(session_id, path, args=None)` — spawn a process on the rig (returns `pid`)
138
+ - `kill_app(session_id, pid=None)` — kill session-launched apps (`pid` optional)
139
+
140
+ ### Input
141
+
142
+ - `send_input(session_id, events)` — batch input events
143
+ - `click(session_id, x, y, button)` — click at coordinates
144
+ - `type_text(session_id, text)` — type a string
145
+ - `key_press(session_id, key)` — press a key
146
+ - `move_mouse(session_id, x, y)` — move cursor
147
+ - `scroll(session_id, dx, dy)` — scroll
148
+
149
+ ### Stream
150
+
151
+ - `stream(session_id)` — WebRTC signaling credentials for a live 60fps feed
152
+
153
+ ## Templates
154
+
155
+ Outcome-named starters in `templates/` (observe → think → act → verify):
156
+
157
+ - `templates/observe_think_act/` — minimal agent-loop skeleton
158
+ - `templates/notepad_type/` — Notepad via UIA + type
159
+ - `templates/live_view_hitl/` — hold a session for Console Live View
160
+
161
+ ## Examples
162
+
163
+ See `examples/` for complete runnable scripts:
164
+
165
+ - `quickstart.py` — connect, screenshot, click, type, disconnect
166
+ - `vp0_smoke.py` — dirty rects + ROI screenshot smoke
167
+ - `vp0_observe.py` — shared VP0 helper (dirty skip + ROI coalesce) for agent loops
168
+ - `paint_mark_demo.py` — **an agent draws a themed GLASSWARP outline in Paint** (custom colors + native drag). See [Paint quickstart](../../docs/QUICKSTART_PAINT.md).
169
+ - `paint_mona_lisa_demo.py` — **an agent reproduces the Mona Lisa in Paint** (observe→think→act→verify: vision placement, UIA colors, tile fills, Save As→Desktop). VNC-hard differentiator. See [Mona Lisa quickstart](../../docs/QUICKSTART_MONA_LISA.md).
170
+ - `notepad_uia_demo.py` — Notepad via UIA targets + type. See [Notepad quickstart](../../docs/QUICKSTART_NOTEPAD.md).
171
+ - `minesweeper_solver_demo.py` — Minesweeper demo (CV + solver). See [quickstart](../../docs/QUICKSTART_MINESWEEPER.md).
172
+ - `gemini_agent_loop.py` — Minesweeper demo with Gemini + SoM → `click_target` → verify
173
+ - `claude_agent_loop.py` — observe → Claude → act; VP0 on by default (`VP0=0` for full frames)
174
+
175
+ ## License
176
+
177
+ Apache-2.0
@@ -0,0 +1,141 @@
1
+ # Glasswarp Python SDK
2
+
3
+ See any screen. Control any PC. One API.
4
+
5
+ Glasswarp gives your agent **eyes and hands** on a real Windows machine; you
6
+ supply the **brain** (model, prompts, CV, solvers). Example task logic lives under
7
+ `examples/` and outcome-named starters under `templates/` — not in the platform.
8
+ Public plans: Free / Builder / Growth / Enterprise. Game Mode at `/play` is a frozen
9
+ demo of the same capture pipeline.
10
+
11
+ ```bash
12
+ pip install glasswarp
13
+ # optional SoM / image helpers:
14
+ pip install "glasswarp[grounding]"
15
+ ```
16
+
17
+ > **Publish status:** packaging targets PyPI as `glasswarp` (`0.1.0`). Until the
18
+ > first upload lands, install from this repo: `pip install -e sdk/python`.
19
+ > Operator steps: [`docs/PYPI_PUBLISH.md`](../../docs/PYPI_PUBLISH.md).
20
+
21
+ Want Claude / Cursor with **no project code**? Use the remote MCP server instead —
22
+ [glasswarp.com/mcp](https://www.glasswarp.com/mcp) ·
23
+ [docs](https://docs.glasswarp.com/get-started/mcp).
24
+
25
+ ## Quickstart
26
+
27
+ ```python
28
+ from glasswarp import GlasswarpClient
29
+
30
+ gw = GlasswarpClient(api_key="gw_live_sk_...")
31
+
32
+ # List your rigs
33
+ rigs = gw.list_rigs()
34
+ rig = rigs[0]
35
+
36
+ # Start a desktop session
37
+ session = gw.create_session(rig_id=rig.id)
38
+
39
+ # Take a screenshot (returns JPEG bytes)
40
+ screenshot = gw.screenshot(session.session_id)
41
+ with open("screen.jpg", "wb") as f:
42
+ f.write(screenshot.jpeg)
43
+
44
+ # Send input
45
+ gw.click(session.session_id, x=500, y=400)
46
+ gw.type_text(session.session_id, "Hello from Python!")
47
+ gw.key_press(session.session_id, "Enter")
48
+
49
+ # End the session
50
+ gw.end_session(session.session_id)
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ | Parameter | Env var | Default |
56
+ |-----------|---------|---------|
57
+ | `api_key` | `GLASSWARP_API_KEY` | (required) |
58
+ | `base_url` | `GLASSWARP_BASE_URL` | `https://signal.glasswarp.com` |
59
+
60
+ ## API reference
61
+
62
+ ### Rigs
63
+
64
+ - `list_rigs()` — all rigs accessible to this key
65
+ - `get_rig(rig_id)` — single rig detail
66
+
67
+ ### Sessions
68
+
69
+ - `create_session(rig_id, mode="desktop")` — start a session
70
+ - `get_session(session_id)` — current status
71
+ - `list_sessions()` — all active sessions
72
+ - `end_session(session_id)` — end and trigger safety_restore
73
+
74
+ ### Visual
75
+
76
+ - `screenshot(session_id, max_width=None, quality=None, x=y=w=h=None)` — JPEG; optional ROI crop
77
+ - `dirty_rects(session_id)` — accumulated DXGI dirty rects since last poll (VP0)
78
+ - `list_targets(session_id)` — sparse click targets from host UIA (P8); empty if none
79
+ - `observe(session_id, mark=True, targets=None, …)` — one RTT: JPEG + dirty + targets (SoM when marked)
80
+
81
+ ### Grounding (SoM)
82
+
83
+ Agents should click **targets**, not guessed pixels:
84
+
85
+ ```python
86
+ from glasswarp import targets_from_grid, som_annotate
87
+
88
+ # Host UIA targets (P8) or client CV/grid:
89
+ targets = targets_from_grid(x0=100, y0=200, x1=900, y1=800, rows=8, cols=8)
90
+ obs = gw.observe(session.session_id, max_width=1280, mark=True, targets=targets)
91
+ # obs.jpeg is Set-of-Mark annotated; model returns target id →
92
+ gw.click_target(session.session_id, "12", targets=targets)
93
+ ```
94
+
95
+ - `som_annotate(jpeg, targets, native_width=…, native_height=…)` — draw numbered marks (needs `pip install pillow`)
96
+ - `targets_from_grid(…)` — build cell targets for a rectangular board
97
+ - `click_target(session_id, target_or_id, targets=…)` — click bbox center
98
+
99
+ ### Apps
100
+
101
+ - `launch_app(session_id, path, args=None)` — spawn a process on the rig (returns `pid`)
102
+ - `kill_app(session_id, pid=None)` — kill session-launched apps (`pid` optional)
103
+
104
+ ### Input
105
+
106
+ - `send_input(session_id, events)` — batch input events
107
+ - `click(session_id, x, y, button)` — click at coordinates
108
+ - `type_text(session_id, text)` — type a string
109
+ - `key_press(session_id, key)` — press a key
110
+ - `move_mouse(session_id, x, y)` — move cursor
111
+ - `scroll(session_id, dx, dy)` — scroll
112
+
113
+ ### Stream
114
+
115
+ - `stream(session_id)` — WebRTC signaling credentials for a live 60fps feed
116
+
117
+ ## Templates
118
+
119
+ Outcome-named starters in `templates/` (observe → think → act → verify):
120
+
121
+ - `templates/observe_think_act/` — minimal agent-loop skeleton
122
+ - `templates/notepad_type/` — Notepad via UIA + type
123
+ - `templates/live_view_hitl/` — hold a session for Console Live View
124
+
125
+ ## Examples
126
+
127
+ See `examples/` for complete runnable scripts:
128
+
129
+ - `quickstart.py` — connect, screenshot, click, type, disconnect
130
+ - `vp0_smoke.py` — dirty rects + ROI screenshot smoke
131
+ - `vp0_observe.py` — shared VP0 helper (dirty skip + ROI coalesce) for agent loops
132
+ - `paint_mark_demo.py` — **an agent draws a themed GLASSWARP outline in Paint** (custom colors + native drag). See [Paint quickstart](../../docs/QUICKSTART_PAINT.md).
133
+ - `paint_mona_lisa_demo.py` — **an agent reproduces the Mona Lisa in Paint** (observe→think→act→verify: vision placement, UIA colors, tile fills, Save As→Desktop). VNC-hard differentiator. See [Mona Lisa quickstart](../../docs/QUICKSTART_MONA_LISA.md).
134
+ - `notepad_uia_demo.py` — Notepad via UIA targets + type. See [Notepad quickstart](../../docs/QUICKSTART_NOTEPAD.md).
135
+ - `minesweeper_solver_demo.py` — Minesweeper demo (CV + solver). See [quickstart](../../docs/QUICKSTART_MINESWEEPER.md).
136
+ - `gemini_agent_loop.py` — Minesweeper demo with Gemini + SoM → `click_target` → verify
137
+ - `claude_agent_loop.py` — observe → Claude → act; VP0 on by default (`VP0=0` for full frames)
138
+
139
+ ## License
140
+
141
+ Apache-2.0
@@ -0,0 +1,48 @@
1
+ """Glasswarp Python SDK — see any screen, control any PC, one API."""
2
+
3
+ from glasswarp.client import GlasswarpClient, GlasswarpError
4
+ from glasswarp.grounding import som_annotate, targets_from_grid
5
+ from glasswarp.types import (
6
+ Rig,
7
+ Session,
8
+ ApiKey,
9
+ Screenshot,
10
+ Target,
11
+ ObserveResult,
12
+ InputEvent,
13
+ MouseMove,
14
+ MouseClick,
15
+ MouseDown,
16
+ MouseUp,
17
+ MouseScroll,
18
+ KeyPress,
19
+ KeyDown,
20
+ KeyUp,
21
+ TypeText,
22
+ StreamConnection,
23
+ )
24
+
25
+ __version__ = "0.1.0"
26
+ __all__ = [
27
+ "GlasswarpClient",
28
+ "GlasswarpError",
29
+ "Rig",
30
+ "Session",
31
+ "ApiKey",
32
+ "Screenshot",
33
+ "Target",
34
+ "ObserveResult",
35
+ "som_annotate",
36
+ "targets_from_grid",
37
+ "InputEvent",
38
+ "MouseMove",
39
+ "MouseClick",
40
+ "MouseDown",
41
+ "MouseUp",
42
+ "MouseScroll",
43
+ "KeyPress",
44
+ "KeyDown",
45
+ "KeyUp",
46
+ "TypeText",
47
+ "StreamConnection",
48
+ ]
@@ -0,0 +1,454 @@
1
+ """Glasswarp API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import os
7
+ from dataclasses import asdict
8
+ from typing import Optional, Sequence, Union
9
+
10
+ import httpx
11
+
12
+ from glasswarp.types import (
13
+ ApiKey,
14
+ InputEvent,
15
+ ObserveResult,
16
+ Rig,
17
+ Screenshot,
18
+ Session,
19
+ StreamConnection,
20
+ Target,
21
+ )
22
+
23
+ DEFAULT_BASE_URL = "https://signal.glasswarp.com"
24
+
25
+
26
+ class GlasswarpError(Exception):
27
+ """Raised when the API returns a non-2xx status."""
28
+
29
+ def __init__(self, status: int, message: str) -> None:
30
+ self.status = status
31
+ self.message = message
32
+ super().__init__(f"[{status}] {message}")
33
+
34
+
35
+ class GlasswarpClient:
36
+ """Synchronous client for the Glasswarp Platform API.
37
+
38
+ Usage::
39
+
40
+ from glasswarp import GlasswarpClient
41
+
42
+ gw = GlasswarpClient(api_key="gw_live_sk_...")
43
+ rigs = gw.list_rigs()
44
+ session = gw.create_session(rig_id=rigs[0].id)
45
+ screenshot = gw.screenshot(session.session_id)
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ api_key: Optional[str] = None,
51
+ base_url: Optional[str] = None,
52
+ timeout: float = 30.0,
53
+ ) -> None:
54
+ self.api_key = api_key or os.environ.get("GLASSWARP_API_KEY", "")
55
+ if not self.api_key:
56
+ raise ValueError(
57
+ "api_key is required — pass it or set GLASSWARP_API_KEY"
58
+ )
59
+ self.base_url = (base_url or os.environ.get("GLASSWARP_BASE_URL", DEFAULT_BASE_URL)).rstrip("/")
60
+ self._http = httpx.Client(
61
+ base_url=self.base_url,
62
+ headers={"Authorization": f"Bearer {self.api_key}"},
63
+ timeout=timeout,
64
+ )
65
+
66
+ def close(self) -> None:
67
+ self._http.close()
68
+
69
+ def __enter__(self) -> "GlasswarpClient":
70
+ return self
71
+
72
+ def __exit__(self, *_: object) -> None:
73
+ self.close()
74
+
75
+ # ── Helpers ──────────────────────────────────────────────────────────
76
+
77
+ def _json(self, response: httpx.Response) -> dict:
78
+ if response.status_code >= 400:
79
+ try:
80
+ body = response.json()
81
+ msg = body.get("error", response.text)
82
+ except Exception:
83
+ msg = response.text
84
+ raise GlasswarpError(response.status_code, msg)
85
+ return response.json()
86
+
87
+ # ── Rigs ─────────────────────────────────────────────────────────────
88
+
89
+ def list_rigs(self) -> list[Rig]:
90
+ """List all rigs accessible to this API key."""
91
+ data = self._json(self._http.get("/v1/rigs"))
92
+ return [Rig(**r) for r in data.get("rigs", [])]
93
+
94
+ def get_rig(self, rig_id: str) -> Rig:
95
+ """Get details for a single rig."""
96
+ data = self._json(self._http.get(f"/v1/rigs/{rig_id}"))
97
+ return Rig(**data)
98
+
99
+ # ── Sessions ─────────────────────────────────────────────────────────
100
+
101
+ def create_session(
102
+ self,
103
+ rig_id: str,
104
+ mode: str = "desktop",
105
+ *,
106
+ idle_timeout_seconds: int | None = None,
107
+ ) -> Session:
108
+ """Start a new API session on a rig.
109
+
110
+ ``idle_timeout_seconds`` (60–3600, default 900 / 15m) is how long the
111
+ session may sit with no screenshot/observe/input before the server ends
112
+ it automatically. Longer idle = more wall-clock billed minutes.
113
+ """
114
+ body: dict = {"rig_id": rig_id, "mode": mode}
115
+ if idle_timeout_seconds is not None:
116
+ body["idle_timeout_seconds"] = idle_timeout_seconds
117
+ data = self._json(self._http.post("/v1/sessions", json=body))
118
+ return Session(**data)
119
+
120
+ def get_session(self, session_id: str) -> Session:
121
+ """Get the current state of a session."""
122
+ data = self._json(self._http.get(f"/v1/sessions/{session_id}"))
123
+ return Session(**data)
124
+
125
+ def list_sessions(self) -> list[Session]:
126
+ """List all active sessions for this API key's owner."""
127
+ data = self._json(self._http.get("/v1/sessions"))
128
+ return [Session(**s) for s in data.get("sessions", [])]
129
+
130
+ def end_session(self, session_id: str, *, keep_apps: bool = False) -> dict:
131
+ """End a session (triggers safety_restore on the host).
132
+
133
+ By default, apps launched via :meth:`launch_app` are killed. Pass
134
+ ``keep_apps=True`` to leave them running (useful for demos that should
135
+ leave Paint or Notepad open on the desktop).
136
+ """
137
+ path = f"/v1/sessions/{session_id}"
138
+ if keep_apps:
139
+ path += "?keep_apps=1"
140
+ return self._json(self._http.request("DELETE", path))
141
+
142
+ # ── Screenshot ───────────────────────────────────────────────────────
143
+
144
+ def screenshot(
145
+ self,
146
+ session_id: str,
147
+ *,
148
+ max_width: int | None = None,
149
+ quality: int | None = None,
150
+ x: int | None = None,
151
+ y: int | None = None,
152
+ w: int | None = None,
153
+ h: int | None = None,
154
+ ) -> Screenshot:
155
+ """Capture a JPEG of the current screen (from the host last-frame buffer).
156
+
157
+ Capture stays at native desktop resolution (including 4K). Pass
158
+ ``max_width`` / ``quality`` to downscale on the host for faster LLM
159
+ ingest. Pass ``x,y,w,h`` for a ROI crop in native capture coords (VP0).
160
+ Click coordinates should use ``native_width`` / ``native_height``.
161
+ """
162
+ params: dict[str, int] = {}
163
+ if max_width is not None:
164
+ params["max_width"] = max_width
165
+ if quality is not None:
166
+ params["quality"] = quality
167
+ if x is not None and y is not None and w is not None and h is not None:
168
+ params["x"] = x
169
+ params["y"] = y
170
+ params["w"] = w
171
+ params["h"] = h
172
+
173
+ response = self._http.get(
174
+ f"/v1/sessions/{session_id}/screenshot",
175
+ params=params or None,
176
+ )
177
+ if response.status_code >= 400:
178
+ try:
179
+ msg = response.json().get("error", response.text)
180
+ except Exception:
181
+ msg = response.text
182
+ raise GlasswarpError(response.status_code, msg)
183
+
184
+ width = int(response.headers.get("X-Screenshot-Width", "0"))
185
+ height = int(response.headers.get("X-Screenshot-Height", "0"))
186
+ return Screenshot(
187
+ jpeg=response.content,
188
+ width=width,
189
+ height=height,
190
+ timestamp=int(response.headers.get("X-Screenshot-Timestamp", "0")),
191
+ native_width=int(
192
+ response.headers.get("X-Screenshot-Native-Width", str(width))
193
+ ),
194
+ native_height=int(
195
+ response.headers.get("X-Screenshot-Native-Height", str(height))
196
+ ),
197
+ )
198
+
199
+ def dirty_rects(self, session_id: str) -> dict:
200
+ """Return accumulated DXGI dirty rects since last poll (VP0).
201
+
202
+ Each rect is ``[x, y, w, h]`` in capture-buffer coordinates. Empty
203
+ ``rects`` means no visual change since the previous take — safe to
204
+ skip a vision model call.
205
+ """
206
+ return self._json(self._http.get(f"/v1/sessions/{session_id}/dirty"))
207
+
208
+ def list_targets(self, session_id: str) -> list[Target]:
209
+ """Return sparse click targets (VP0.5). Empty until host UIA (P8)."""
210
+ data = self._json(self._http.get(f"/v1/sessions/{session_id}/targets"))
211
+ return [_parse_target(t) for t in (data.get("targets") or [])]
212
+
213
+ def observe(
214
+ self,
215
+ session_id: str,
216
+ *,
217
+ max_width: int | None = None,
218
+ quality: int | None = None,
219
+ x: int | None = None,
220
+ y: int | None = None,
221
+ w: int | None = None,
222
+ h: int | None = None,
223
+ mark: bool = True,
224
+ targets: Sequence[Target] | None = None,
225
+ ) -> ObserveResult:
226
+ """One-RTT observe: JPEG + dirty + targets (VP0.5 grounding).
227
+
228
+ If ``targets`` is passed (e.g. client CV/grid), those override the
229
+ host list and are used for optional SoM marking when ``mark=True``.
230
+ """
231
+ params: dict[str, int] = {}
232
+ if max_width is not None:
233
+ params["max_width"] = max_width
234
+ if quality is not None:
235
+ params["quality"] = quality
236
+ if x is not None and y is not None and w is not None and h is not None:
237
+ params["x"] = x
238
+ params["y"] = y
239
+ params["w"] = w
240
+ params["h"] = h
241
+ if mark:
242
+ params["mark"] = 1
243
+
244
+ data = self._json(
245
+ self._http.get(
246
+ f"/v1/sessions/{session_id}/observe",
247
+ params=params or None,
248
+ )
249
+ )
250
+ jpeg = base64.b64decode(data["jpeg_base64"])
251
+ host_targets = [_parse_target(t) for t in (data.get("targets") or [])]
252
+ resolved = list(targets) if targets is not None else host_targets
253
+ marked = bool(data.get("marked"))
254
+ if mark and resolved and (targets is not None or not marked):
255
+ from glasswarp.grounding import som_annotate
256
+
257
+ jpeg = som_annotate(
258
+ jpeg,
259
+ resolved,
260
+ native_width=int(data.get("native_width") or data.get("width") or 1),
261
+ native_height=int(
262
+ data.get("native_height") or data.get("height") or 1
263
+ ),
264
+ )
265
+ marked = True
266
+
267
+ return ObserveResult(
268
+ jpeg=jpeg,
269
+ width=int(data.get("width") or 0),
270
+ height=int(data.get("height") or 0),
271
+ native_width=int(data.get("native_width") or data.get("width") or 0),
272
+ native_height=int(
273
+ data.get("native_height") or data.get("height") or 0
274
+ ),
275
+ timestamp=int(data.get("timestamp") or 0),
276
+ dirty=data.get("dirty") or {},
277
+ targets=resolved,
278
+ marked=marked,
279
+ )
280
+
281
+ def click_target(
282
+ self,
283
+ session_id: str,
284
+ target: Union[Target, str],
285
+ *,
286
+ targets: Sequence[Target] | None = None,
287
+ button: str = "left",
288
+ ) -> dict:
289
+ """Click the center of a grounding target (by object or id)."""
290
+ if isinstance(target, Target):
291
+ t = target
292
+ else:
293
+ pool = list(targets or [])
294
+ if not pool:
295
+ pool = self.list_targets(session_id)
296
+ match = next((x for x in pool if x.id == target), None)
297
+ if match is None:
298
+ raise GlasswarpError(400, f"Unknown target id: {target}")
299
+ t = match
300
+ cx, cy = t.center
301
+ return self.click(session_id, x=cx, y=cy, button=button)
302
+
303
+ # ── Stream ───────────────────────────────────────────────────────────
304
+
305
+ def stream(self, session_id: str) -> StreamConnection:
306
+ """Get WebRTC signaling credentials for a live 60fps video stream.
307
+
308
+ Returns connection details including a ready-to-use WebSocket URL.
309
+ Connect to ``connect_url``, send an SDP OFFER, receive an ANSWER,
310
+ and exchange ICE candidates to start receiving H.264 video.
311
+ """
312
+ data = self._json(self._http.get(f"/v1/sessions/{session_id}/stream"))
313
+ return StreamConnection(**data)
314
+
315
+ # ── Apps ─────────────────────────────────────────────────────────────
316
+
317
+ def launch_app(
318
+ self,
319
+ session_id: str,
320
+ path: str,
321
+ args: list[str] | None = None,
322
+ ) -> dict:
323
+ """Spawn an application on the remote rig (e.g. ``notepad.exe``).
324
+
325
+ Returns ``{"ok": True, "pid": <int>}``. Tracked PIDs are killed on
326
+ ``end_session`` or via :meth:`kill_app`.
327
+ """
328
+ body: dict = {"path": path}
329
+ if args is not None:
330
+ body["args"] = args
331
+ return self._json(
332
+ self._http.post(f"/v1/sessions/{session_id}/app/launch", json=body)
333
+ )
334
+
335
+ def kill_app(self, session_id: str, pid: int | None = None) -> dict:
336
+ """Kill apps launched via :meth:`launch_app` in this session.
337
+
338
+ Pass ``pid`` to kill one process; omit to kill all session-launched apps.
339
+ """
340
+ body: dict = {}
341
+ if pid is not None:
342
+ body["pid"] = pid
343
+ return self._json(
344
+ self._http.post(f"/v1/sessions/{session_id}/app/kill", json=body)
345
+ )
346
+
347
+ # ── Input ────────────────────────────────────────────────────────────
348
+
349
+ def send_input(self, session_id: str, events: list[InputEvent]) -> dict:
350
+ """Send input events (mouse, keyboard) to the session's rig."""
351
+ payload = [asdict(e) for e in events]
352
+ return self._json(
353
+ self._http.post(
354
+ f"/v1/sessions/{session_id}/input",
355
+ json={"events": payload},
356
+ )
357
+ )
358
+
359
+ def click(
360
+ self,
361
+ session_id: str,
362
+ x: Optional[int] = None,
363
+ y: Optional[int] = None,
364
+ button: str = "left",
365
+ ) -> dict:
366
+ """Convenience: click at (x, y)."""
367
+ from glasswarp.types import MouseClick
368
+ return self.send_input(session_id, [MouseClick(button=button, x=x, y=y)]) # type: ignore[arg-type]
369
+
370
+ def type_text(self, session_id: str, text: str) -> dict:
371
+ """Convenience: type a string."""
372
+ from glasswarp.types import TypeText
373
+ return self.send_input(session_id, [TypeText(text=text)])
374
+
375
+ def key_press(self, session_id: str, key: str) -> dict:
376
+ """Convenience: press and release a key."""
377
+ from glasswarp.types import KeyPress
378
+ return self.send_input(session_id, [KeyPress(key=key)])
379
+
380
+ def move_mouse(self, session_id: str, x: int, y: int) -> dict:
381
+ """Convenience: move the mouse cursor."""
382
+ from glasswarp.types import MouseMove
383
+ return self.send_input(session_id, [MouseMove(x=x, y=y)])
384
+
385
+ def mouse_down(
386
+ self,
387
+ session_id: str,
388
+ x: int,
389
+ y: int,
390
+ button: str = "left",
391
+ ) -> dict:
392
+ """Press a mouse button at (x, y) without releasing (start of a drag)."""
393
+ from glasswarp.types import MouseDown
394
+ return self.send_input(
395
+ session_id, [MouseDown(button=button, x=x, y=y)] # type: ignore[arg-type]
396
+ )
397
+
398
+ def mouse_up(
399
+ self,
400
+ session_id: str,
401
+ x: int,
402
+ y: int,
403
+ button: str = "left",
404
+ ) -> dict:
405
+ """Release a mouse button at (x, y) (end of a drag)."""
406
+ from glasswarp.types import MouseUp
407
+ return self.send_input(
408
+ session_id, [MouseUp(button=button, x=x, y=y)] # type: ignore[arg-type]
409
+ )
410
+
411
+ def drag(
412
+ self,
413
+ session_id: str,
414
+ x0: int,
415
+ y0: int,
416
+ x1: int,
417
+ y1: int,
418
+ *,
419
+ button: str = "left",
420
+ steps: int = 12,
421
+ ) -> dict:
422
+ """Drag from (x0,y0) to (x1,y1) with intermediate moves (native coords)."""
423
+ from glasswarp.types import MouseDown, MouseMove, MouseUp
424
+
425
+ steps = max(2, steps)
426
+ events: list = [MouseDown(button=button, x=x0, y=y0)]
427
+ for i in range(1, steps + 1):
428
+ t = i / steps
429
+ events.append(
430
+ MouseMove(
431
+ x=int(round(x0 + (x1 - x0) * t)),
432
+ y=int(round(y0 + (y1 - y0) * t)),
433
+ )
434
+ )
435
+ events.append(MouseUp(button=button, x=x1, y=y1))
436
+ return self.send_input(session_id, events) # type: ignore[arg-type]
437
+
438
+ def scroll(self, session_id: str, dx: int = 0, dy: int = 0) -> dict:
439
+ """Convenience: scroll the mouse wheel."""
440
+ from glasswarp.types import MouseScroll
441
+ return self.send_input(session_id, [MouseScroll(dx=dx, dy=dy)])
442
+
443
+
444
+ def _parse_target(raw: dict) -> Target:
445
+ return Target(
446
+ id=str(raw["id"]),
447
+ x=int(raw["x"]),
448
+ y=int(raw["y"]),
449
+ w=int(raw["w"]),
450
+ h=int(raw["h"]),
451
+ role=raw.get("role"),
452
+ name=raw.get("name"),
453
+ source=raw.get("source"),
454
+ )
@@ -0,0 +1,111 @@
1
+ """Set-of-Mark (SoM) helpers for agent grounding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ from typing import Sequence
7
+
8
+ from glasswarp.types import Target
9
+
10
+
11
+ def som_annotate(
12
+ jpeg: bytes,
13
+ targets: Sequence[Target],
14
+ *,
15
+ native_width: int,
16
+ native_height: int,
17
+ style: str = "badge",
18
+ ) -> bytes:
19
+ """Draw numbered marks on a JPEG for VLM grounding.
20
+
21
+ Target bboxes are in *native capture* coords; the JPEG may be downscaled.
22
+ ``style``:
23
+ - ``badge`` (default): cyan box + opaque red id badge (good for sparse UIA)
24
+ - ``corner``: thin outline + tiny corner id — keeps cell contents readable
25
+ (needed for dense grids like Minesweeper)
26
+
27
+ Requires Pillow (``pip install pillow``).
28
+ """
29
+ try:
30
+ from PIL import Image, ImageDraw, ImageFont
31
+ except ImportError as e:
32
+ raise ImportError(
33
+ "som_annotate requires Pillow — pip install pillow"
34
+ ) from e
35
+
36
+ img = Image.open(io.BytesIO(jpeg)).convert("RGB")
37
+ draw = ImageDraw.Draw(img)
38
+ jw, jh = img.size
39
+ sx = jw / max(1, native_width)
40
+ sy = jh / max(1, native_height)
41
+
42
+ corner = style == "corner"
43
+ font_size = 9 if corner else 12
44
+ try:
45
+ font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size)
46
+ except Exception:
47
+ font = ImageFont.load_default()
48
+
49
+ for t in targets:
50
+ x0 = t.x * sx
51
+ y0 = t.y * sy
52
+ x1 = (t.x + t.w) * sx
53
+ y1 = (t.y + t.h) * sy
54
+ outline = (0, 180, 255) if corner else (0, 200, 255)
55
+ draw.rectangle([x0, y0, x1, y1], outline=outline, width=1 if corner else 2)
56
+ label = t.id
57
+ bx, by = x0 + 1, y0 + 1
58
+ if corner:
59
+ tw = max(8, 5 * len(label))
60
+ th = 10
61
+ # Semi-transparent feel via dark red (small — leaves cell center free)
62
+ draw.rectangle([bx, by, bx + tw, by + th], fill=(200, 30, 30))
63
+ draw.text((bx + 1, by), label, fill=(255, 255, 255), font=font)
64
+ else:
65
+ tw = max(10, 7 * len(label))
66
+ th = 14
67
+ draw.rectangle([bx, by, bx + tw, by + th], fill=(255, 40, 40))
68
+ draw.text((bx + 2, by + 1), label, fill=(255, 255, 255), font=font)
69
+
70
+ out = io.BytesIO()
71
+ img.save(out, format="JPEG", quality=85)
72
+ return out.getvalue()
73
+
74
+
75
+ def targets_from_grid(
76
+ *,
77
+ x0: int,
78
+ y0: int,
79
+ x1: int,
80
+ y1: int,
81
+ rows: int,
82
+ cols: int,
83
+ id_prefix: str = "",
84
+ source: str = "grid",
85
+ ) -> list[Target]:
86
+ """Build one target per cell of a rectangular grid (native coords)."""
87
+ cell_w = (x1 - x0) / max(1, cols)
88
+ cell_h = (y1 - y0) / max(1, rows)
89
+ out: list[Target] = []
90
+ n = 1
91
+ for r in range(rows):
92
+ for c in range(cols):
93
+ tx = int(round(x0 + c * cell_w))
94
+ ty = int(round(y0 + r * cell_h))
95
+ tw = int(round(cell_w))
96
+ th = int(round(cell_h))
97
+ tid = f"{id_prefix}{n}" if id_prefix else str(n)
98
+ out.append(
99
+ Target(
100
+ id=tid,
101
+ x=tx,
102
+ y=ty,
103
+ w=tw,
104
+ h=th,
105
+ role="cell",
106
+ name=f"{r},{c}",
107
+ source=source,
108
+ )
109
+ )
110
+ n += 1
111
+ return out
File without changes
@@ -0,0 +1,166 @@
1
+ """Data types for the Glasswarp SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Literal, Optional, Union
7
+
8
+
9
+ @dataclass
10
+ class Rig:
11
+ id: str
12
+ name: Optional[str]
13
+ online: bool
14
+ gpu: Optional[str]
15
+ os: Optional[str]
16
+ api_access_enabled: bool
17
+ has_active_session: bool
18
+
19
+
20
+ @dataclass
21
+ class Session:
22
+ session_id: str
23
+ rig_id: str
24
+ mode: Literal["desktop", "app"]
25
+ status: Literal["active", "ended", "failed"]
26
+ started_at: Optional[str] = None
27
+ action_count: int = 0
28
+ idle_timeout_seconds: Optional[int] = None
29
+
30
+
31
+ @dataclass
32
+ class ApiKey:
33
+ id: str
34
+ name: str
35
+ key_prefix: str
36
+ environment: str
37
+ scopes: list[str] = field(default_factory=list)
38
+ allowed_rig_ids: Optional[list[str]] = None
39
+ created_at: Optional[str] = None
40
+ last_used_at: Optional[str] = None
41
+ revoked_at: Optional[str] = None
42
+
43
+
44
+ @dataclass
45
+ class Screenshot:
46
+ """`width`/`height` are the JPEG size; `native_*` is the desktop capture size."""
47
+
48
+ jpeg: bytes
49
+ width: int
50
+ height: int
51
+ timestamp: int
52
+ native_width: int = 0
53
+ native_height: int = 0
54
+
55
+
56
+ @dataclass
57
+ class Target:
58
+ """Sparse click target in *native capture* coordinates (VP0.5 grounding)."""
59
+
60
+ id: str
61
+ x: int
62
+ y: int
63
+ w: int
64
+ h: int
65
+ role: Optional[str] = None
66
+ name: Optional[str] = None
67
+ source: Optional[str] = None # uia | cv | grid | client
68
+
69
+ @property
70
+ def center(self) -> tuple[int, int]:
71
+ return self.x + self.w // 2, self.y + self.h // 2
72
+
73
+
74
+ @dataclass
75
+ class ObserveResult:
76
+ """One-RTT observe: JPEG + dirty + targets (VP0.5)."""
77
+
78
+ jpeg: bytes
79
+ width: int
80
+ height: int
81
+ native_width: int
82
+ native_height: int
83
+ timestamp: int
84
+ dirty: dict[str, Any] = field(default_factory=dict)
85
+ targets: list[Target] = field(default_factory=list)
86
+ marked: bool = False
87
+
88
+
89
+ @dataclass
90
+ class StreamConnection:
91
+ """WebRTC signaling credentials for a live video stream."""
92
+ signaling_url: str
93
+ viewer_token: str
94
+ rig_id: str
95
+ session_id: str
96
+ connect_url: str
97
+ instructions: dict[str, str] = field(default_factory=dict)
98
+
99
+
100
+ # ── Input events ─────────────────────────────────────────────────────────
101
+
102
+ @dataclass
103
+ class MouseMove:
104
+ x: int
105
+ y: int
106
+ type: str = "mouse_move"
107
+
108
+
109
+ @dataclass
110
+ class MouseClick:
111
+ button: Literal["left", "right", "middle"] = "left"
112
+ x: Optional[int] = None
113
+ y: Optional[int] = None
114
+ type: str = "mouse_click"
115
+
116
+
117
+ @dataclass
118
+ class MouseDown:
119
+ button: Literal["left", "right", "middle"] = "left"
120
+ x: Optional[int] = None
121
+ y: Optional[int] = None
122
+ type: str = "mouse_down"
123
+
124
+
125
+ @dataclass
126
+ class MouseUp:
127
+ button: Literal["left", "right", "middle"] = "left"
128
+ x: Optional[int] = None
129
+ y: Optional[int] = None
130
+ type: str = "mouse_up"
131
+
132
+
133
+ @dataclass
134
+ class MouseScroll:
135
+ dx: int = 0
136
+ dy: int = 0
137
+ type: str = "mouse_scroll"
138
+
139
+
140
+ @dataclass
141
+ class KeyPress:
142
+ key: str = ""
143
+ type: str = "key_press"
144
+
145
+
146
+ @dataclass
147
+ class KeyDown:
148
+ key: str = ""
149
+ type: str = "key_down"
150
+
151
+
152
+ @dataclass
153
+ class KeyUp:
154
+ key: str = ""
155
+ type: str = "key_up"
156
+
157
+
158
+ @dataclass
159
+ class TypeText:
160
+ text: str = ""
161
+ type: str = "type_text"
162
+
163
+
164
+ InputEvent = Union[
165
+ MouseMove, MouseClick, MouseDown, MouseUp, MouseScroll, KeyPress, KeyDown, KeyUp, TypeText
166
+ ]
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "glasswarp"
7
+ version = "0.1.0"
8
+ description = "Eyes and hands for AI agents on real Windows — Python SDK for the Glasswarp Platform API."
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Woodrow Stores LLC", email = "hello@glasswarp.com" },
14
+ ]
15
+ keywords = [
16
+ "glasswarp",
17
+ "computer-use",
18
+ "windows",
19
+ "agents",
20
+ "screenshot",
21
+ "automation",
22
+ "mcp",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: Apache Software License",
28
+ "Operating System :: OS Independent",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.9",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Topic :: Software Development :: Libraries :: Python Modules",
36
+ "Typing :: Typed",
37
+ ]
38
+ dependencies = [
39
+ "httpx>=0.24",
40
+ ]
41
+
42
+ [project.optional-dependencies]
43
+ grounding = [
44
+ "pillow>=10",
45
+ ]
46
+ dev = [
47
+ "pytest>=7",
48
+ "pytest-httpx>=0.21",
49
+ "build>=1.2",
50
+ "twine>=5",
51
+ ]
52
+
53
+ [project.urls]
54
+ Homepage = "https://www.glasswarp.com"
55
+ Documentation = "https://docs.glasswarp.com"
56
+ Repository = "https://github.com/saferelay/project-x"
57
+ Issues = "https://github.com/saferelay/project-x/issues"
58
+ Changelog = "https://github.com/saferelay/project-x/blob/main/sdk/python/CHANGELOG.md"
59
+
60
+ [tool.hatch.build]
61
+ # Keep the published sdist lean — package + docs only (no demo JPEGs / examples).
62
+ [tool.hatch.build.targets.sdist]
63
+ include = [
64
+ "/glasswarp",
65
+ "/README.md",
66
+ "/LICENSE",
67
+ "/pyproject.toml",
68
+ "/CHANGELOG.md",
69
+ ]
70
+
71
+ [tool.hatch.build.targets.wheel]
72
+ packages = ["glasswarp"]