@widgex/wgx 0.0.0-stage → 0.4.2
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.
- package/LICENSE.txt +49 -0
- package/README.md +33 -2
- package/ThirdPartyNotices.txt +4685 -0
- package/package.json +30 -4
- package/provider-clis.json +1211 -0
- package/python/XACTOR.md +146 -0
- package/python/browser_actor_host.py +456 -0
- package/python/cdp_page.py +363 -0
- package/python/harness_check.py +77 -0
- package/python/requirements.txt +1 -0
- package/python/test_line_endings.py +185 -0
- package/python/xactor/__init__.py +18 -0
- package/python/xactor/__main__.py +226 -0
- package/python/xactor/api.py +882 -0
- package/python/xactor/bridge.py +784 -0
- package/python/xactor/dom.py +117 -0
- package/python/xactor/man/add_card.md +89 -0
- package/python/xactor/man/agent-vs-fork.md +108 -0
- package/python/xactor/man/agent.md +333 -0
- package/python/xactor/man/ask_model.md +110 -0
- package/python/xactor/man/ask_reference.md +56 -0
- package/python/xactor/man/ask_user.md +103 -0
- package/python/xactor/man/attach_app.md +595 -0
- package/python/xactor/man/attach_ide.md +65 -0
- package/python/xactor/man/attach_ide_surface.md +31 -0
- package/python/xactor/man/await_user.md +214 -0
- package/python/xactor/man/bash.md +142 -0
- package/python/xactor/man/browser-driver.md +137 -0
- package/python/xactor/man/browser_activity.md +53 -0
- package/python/xactor/man/browser_attach.md +84 -0
- package/python/xactor/man/browser_close_tab.md +50 -0
- package/python/xactor/man/browser_detach.md +50 -0
- package/python/xactor/man/browser_exec.md +66 -0
- package/python/xactor/man/browser_focus_tab.md +47 -0
- package/python/xactor/man/browser_hybrid.md +47 -0
- package/python/xactor/man/browser_open.md +50 -0
- package/python/xactor/man/browser_open_tab.md +58 -0
- package/python/xactor/man/browser_plumbing.md +107 -0
- package/python/xactor/man/browser_screenshot.md +56 -0
- package/python/xactor/man/browser_snapshot.md +52 -0
- package/python/xactor/man/browser_tabs.md +67 -0
- package/python/xactor/man/bulletin.md +88 -0
- package/python/xactor/man/capture_page.md +54 -0
- package/python/xactor/man/cd.md +64 -0
- package/python/xactor/man/chess_move.md +53 -0
- package/python/xactor/man/children.md +89 -0
- package/python/xactor/man/close_task.md +14 -0
- package/python/xactor/man/compact.md +75 -0
- package/python/xactor/man/drive.md +140 -0
- package/python/xactor/man/drive_space.md +72 -0
- package/python/xactor/man/driving.md +168 -0
- package/python/xactor/man/edit_file.md +112 -0
- package/python/xactor/man/finding.md +194 -0
- package/python/xactor/man/fork_agent.md +45 -0
- package/python/xactor/man/glob.md +83 -0
- package/python/xactor/man/grep.md +115 -0
- package/python/xactor/man/harness.md +165 -0
- package/python/xactor/man/harness_check.md +70 -0
- package/python/xactor/man/harness_format.md +257 -0
- package/python/xactor/man/highlight.md +66 -0
- package/python/xactor/man/instruct.md +111 -0
- package/python/xactor/man/ls.md +63 -0
- package/python/xactor/man/man.md +86 -0
- package/python/xactor/man/merge_wiz.md +40 -0
- package/python/xactor/man/notebook.md +98 -0
- package/python/xactor/man/overview.md +89 -0
- package/python/xactor/man/permissions.md +137 -0
- package/python/xactor/man/phase.md +150 -0
- package/python/xactor/man/powershell.md +74 -0
- package/python/xactor/man/profiles.md +60 -0
- package/python/xactor/man/project_drive.md +49 -0
- package/python/xactor/man/read_bulletin.md +89 -0
- package/python/xactor/man/read_file.md +90 -0
- package/python/xactor/man/relay.md +216 -0
- package/python/xactor/man/reply.md +72 -0
- package/python/xactor/man/reply_message.md +78 -0
- package/python/xactor/man/report_bug.md +118 -0
- package/python/xactor/man/request_answer.md +142 -0
- package/python/xactor/man/request_merge_decisions.md +121 -0
- package/python/xactor/man/request_operator_action.md +152 -0
- package/python/xactor/man/request_review.md +122 -0
- package/python/xactor/man/request_user_action.md +126 -0
- package/python/xactor/man/return_card.md +71 -0
- package/python/xactor/man/run.md +99 -0
- package/python/xactor/man/sandbox.md +144 -0
- package/python/xactor/man/session_fork.md +74 -0
- package/python/xactor/man/session_harness.md +109 -0
- package/python/xactor/man/session_join.md +102 -0
- package/python/xactor/man/session_land.md +73 -0
- package/python/xactor/man/session_nudge.md +73 -0
- package/python/xactor/man/session_rest.md +73 -0
- package/python/xactor/man/session_scope.md +84 -0
- package/python/xactor/man/session_terminate.md +58 -0
- package/python/xactor/man/session_token.md +78 -0
- package/python/xactor/man/sessions.md +93 -0
- package/python/xactor/man/set_harness.md +15 -0
- package/python/xactor/man/spawn_swarm.md +80 -0
- package/python/xactor/man/submit_evidence.md +54 -0
- package/python/xactor/man/swarm_brief.md +30 -0
- package/python/xactor/man/task_status.md +82 -0
- package/python/xactor/man/tasks.md +47 -0
- package/python/xactor/man/todo_write.md +104 -0
- package/python/xactor/man/update_card.md +70 -0
- package/python/xactor/man/use_profile.md +90 -0
- package/python/xactor/man/use_widget.md +82 -0
- package/python/xactor/man/wait_for_task.md +105 -0
- package/python/xactor/man/widget-model.md +284 -0
- package/python/xactor/man/widgets.md +59 -0
- package/python/xactor/man/write_file.md +76 -0
- package/python/xactor/man/writing-a-widget.md +142 -0
- package/python/xactor/man/xylon.md +120 -0
- package/python/xactor/man/xylon_blame.md +80 -0
- package/python/xactor/man/xylon_read.md +131 -0
- package/python/xactor/man/xylon_search.md +125 -0
- package/python/xactor/manual.py +195 -0
- package/python/xactor/parent_watch.py +115 -0
- package/python/xactor/plan_mode.py +350 -0
- package/python/xactor/sandbox.py +856 -0
- package/python/xactor/seals.py +438 -0
- package/python/xactor/shell_hints.py +210 -0
- package/python/xactor/states/__init__.py +13 -0
- package/python/xactor/states/base.py +523 -0
- package/python/xactor/states/registry.py +274 -0
- package/python/xactor/stdlib_api.py +6345 -0
- package/python/xactor/widgets/__init__.py +1 -0
- package/python/xactor/widgets/github_tokens.py +314 -0
- package/python/xactor/widgets/google_aistudio.py +365 -0
- package/settings-schema.json +403 -0
- package/viewer/index.css +160 -0
- package/viewer/index.js +15 -0
- package/wgx.mjs +290659 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""StateRegistry — maps URL patterns to PageState templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .base import PageState, Observation, Transition, Step
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..api import ElementaryAPI
|
|
11
|
+
from ..dom import DOMSnapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StateRegistry:
|
|
15
|
+
"""Singleton registry mapping URL patterns → PageState templates.
|
|
16
|
+
|
|
17
|
+
Uses class-level storage so it works with both import and exec loading.
|
|
18
|
+
|
|
19
|
+
Also the source of truth for "where is the agent right now" — resolve()
|
|
20
|
+
publishes every change over the bridge so the IDE can render it live.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_states: dict[str, PageState] = {}
|
|
24
|
+
_adjacency: dict[str, list[str]] = {}
|
|
25
|
+
|
|
26
|
+
# Live position, published to the IDE on change.
|
|
27
|
+
_current: str | None = None
|
|
28
|
+
_current_url: str | None = None
|
|
29
|
+
# Fingerprint of the last graph we published, so the (large) graph payload
|
|
30
|
+
# rides along only when the registry itself changed.
|
|
31
|
+
_published_graph: tuple[str, ...] | None = None
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def add(cls, state: PageState, transitions_to: list[str] | None = None) -> None:
|
|
35
|
+
"""Register a state + its adjacency list."""
|
|
36
|
+
cls._states[state.name] = state
|
|
37
|
+
if transitions_to:
|
|
38
|
+
cls._adjacency[state.name] = transitions_to
|
|
39
|
+
|
|
40
|
+
# ── Live position reporting ──────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def graph(cls) -> dict[str, Any]:
|
|
44
|
+
"""Serializable machine definition: states plus the adjacency graph."""
|
|
45
|
+
return {
|
|
46
|
+
"states": [s.describe() for s in cls._states.values()],
|
|
47
|
+
"adjacency": {k: list(v) for k, v in cls._adjacency.items()},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _fingerprint(cls) -> tuple[str, ...]:
|
|
52
|
+
return tuple(sorted(cls._states)) + ("|",) + tuple(
|
|
53
|
+
f"{k}>{','.join(v)}" for k, v in sorted(cls._adjacency.items())
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def publish(cls, state_name: str | None = None, url: str | None = None,
|
|
58
|
+
*, force: bool = False) -> None:
|
|
59
|
+
"""Tell the IDE where the agent is. Never raises — reporting must not
|
|
60
|
+
break automation, and the browser backend can run with no bridge.
|
|
61
|
+
"""
|
|
62
|
+
previous = cls._current
|
|
63
|
+
if state_name is not None:
|
|
64
|
+
cls._current = state_name
|
|
65
|
+
if url is not None:
|
|
66
|
+
cls._current_url = url
|
|
67
|
+
|
|
68
|
+
fingerprint = cls._fingerprint()
|
|
69
|
+
graph_changed = fingerprint != cls._published_graph
|
|
70
|
+
if not force and not graph_changed and previous == cls._current:
|
|
71
|
+
return # nothing new to say
|
|
72
|
+
|
|
73
|
+
payload: dict[str, Any] = {
|
|
74
|
+
"event": "xactor.state",
|
|
75
|
+
"state": cls._current,
|
|
76
|
+
"url": cls._current_url,
|
|
77
|
+
"previous": previous if previous != cls._current else None,
|
|
78
|
+
}
|
|
79
|
+
if graph_changed or force:
|
|
80
|
+
payload["graph"] = cls.graph()
|
|
81
|
+
cls._published_graph = fingerprint
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
from .. import bridge
|
|
85
|
+
|
|
86
|
+
bridge._emit(payload)
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def current(cls) -> str | None:
|
|
92
|
+
"""Last resolved state name, if any."""
|
|
93
|
+
return cls._current
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
async def here(cls, api: ElementaryAPI) -> PageState:
|
|
97
|
+
"""The bound PageState for the page as it is right now.
|
|
98
|
+
|
|
99
|
+
Use this. `current()` returns a *name*, which you cannot call anything
|
|
100
|
+
on — getting something callable meant knowing to write
|
|
101
|
+
`await registry.resolve(await api.snapshot(), api)`, which was written
|
|
102
|
+
down nowhere. Agents that could not find it fell back to copying a
|
|
103
|
+
widget's observation JS into `api.evaluate`, which is the thing the
|
|
104
|
+
whole layer exists to make unnecessary.
|
|
105
|
+
|
|
106
|
+
state = await registry.here(api)
|
|
107
|
+
rows = await state.observe("existing_tokens")
|
|
108
|
+
state = await state.execute("create_token")
|
|
109
|
+
"""
|
|
110
|
+
return await cls.resolve(None, api)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
async def enter(cls, api: ElementaryAPI, state_name: str | None = None) -> PageState:
|
|
114
|
+
"""Navigate to a state's `entry` URL and resolve.
|
|
115
|
+
|
|
116
|
+
This is how a widget is entered cold. With no name, the first registered
|
|
117
|
+
state that declares an `entry` is the home.
|
|
118
|
+
|
|
119
|
+
Resolution is deliberately not asserted against `state_name`: arriving
|
|
120
|
+
somewhere other than where you aimed — a login wall, a consent screen —
|
|
121
|
+
is information the caller needs, not an error to raise. Check
|
|
122
|
+
`.name` on what you get back.
|
|
123
|
+
"""
|
|
124
|
+
if state_name is None:
|
|
125
|
+
enterable = [s for s in cls._states.values() if getattr(s, "entry", None)]
|
|
126
|
+
if not enterable:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
"No loaded state declares an entry URL, so there is nowhere to "
|
|
129
|
+
"enter. Add entry=\"https://...\" to the widget's home state."
|
|
130
|
+
)
|
|
131
|
+
target = enterable[0]
|
|
132
|
+
else:
|
|
133
|
+
target = cls._states.get(state_name)
|
|
134
|
+
if target is None:
|
|
135
|
+
raise KeyError(
|
|
136
|
+
f"No state '{state_name}'. Loaded: {list(cls._states)}"
|
|
137
|
+
)
|
|
138
|
+
if not getattr(target, "entry", None):
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"State '{state_name}' has no entry URL — url_pattern is a matcher, "
|
|
141
|
+
"not a destination. Reach it with a transition, or give it entry=."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
await api.navigate(target.entry)
|
|
145
|
+
snap = await api.wait_stable()
|
|
146
|
+
return await cls.resolve(snap, api)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
async def resolve(cls, dom: "DOMSnapshot | None", api: ElementaryAPI) -> PageState:
|
|
150
|
+
"""URL-match -> bind -> return. Falls back to UNKNOWN_STATE.
|
|
151
|
+
|
|
152
|
+
When several states match the same URL -- which is the normal case for a
|
|
153
|
+
site whose dialogs live at one address -- they are disambiguated by
|
|
154
|
+
scoring each state's keywords against the visible page text.
|
|
155
|
+
|
|
156
|
+
`dom` is accepted for the widgets that still pass one, but nothing needs
|
|
157
|
+
it any more: asking the page for its URL costs one cheap call, where a
|
|
158
|
+
snapshot used to pull the whole accessibility tree and every cookie on
|
|
159
|
+
the domain just to read `.url` off it.
|
|
160
|
+
"""
|
|
161
|
+
if dom is not None and getattr(dom, "url", None):
|
|
162
|
+
url = dom.url
|
|
163
|
+
else:
|
|
164
|
+
state = await api.get_state()
|
|
165
|
+
url = (state or {}).get("url", "")
|
|
166
|
+
|
|
167
|
+
candidates = [s for s in cls._states.values() if s.matches_url(url)]
|
|
168
|
+
if not candidates:
|
|
169
|
+
cls.publish(UNKNOWN_STATE.name, url)
|
|
170
|
+
return UNKNOWN_STATE.bind(dom, api)
|
|
171
|
+
if len(candidates) == 1:
|
|
172
|
+
cls.publish(candidates[0].name, url)
|
|
173
|
+
return candidates[0].bind(dom, api)
|
|
174
|
+
|
|
175
|
+
# Several states claim this URL -- a site whose dialogs all live at one
|
|
176
|
+
# address. Score each one's keywords against what is actually rendered.
|
|
177
|
+
try:
|
|
178
|
+
page_text = await api.evaluate("document.body.innerText")
|
|
179
|
+
if not isinstance(page_text, str):
|
|
180
|
+
page_text = ""
|
|
181
|
+
except Exception:
|
|
182
|
+
page_text = ""
|
|
183
|
+
if not page_text and dom is not None:
|
|
184
|
+
page_text = dom.text_content()
|
|
185
|
+
page_text = page_text.lower()
|
|
186
|
+
|
|
187
|
+
best, best_score = candidates[0], -1
|
|
188
|
+
for state in candidates:
|
|
189
|
+
score = sum(1 for kw in state.keywords if kw.lower() in page_text)
|
|
190
|
+
if score > best_score:
|
|
191
|
+
best, best_score = state, score
|
|
192
|
+
cls.publish(best.name, url)
|
|
193
|
+
return best.bind(dom, api)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def all_states(cls) -> list[dict[str, Any]]:
|
|
197
|
+
"""Return descriptions of all registered states."""
|
|
198
|
+
return [s.describe() for s in cls._states.values()]
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def reachable_from(cls, state_name: str) -> list[str]:
|
|
202
|
+
"""Return state names reachable from the given state."""
|
|
203
|
+
return cls._adjacency.get(state_name, [])
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def clear(cls) -> None:
|
|
207
|
+
"""Remove all registered states."""
|
|
208
|
+
cls._states.clear()
|
|
209
|
+
cls._adjacency.clear()
|
|
210
|
+
cls._current = None
|
|
211
|
+
cls._current_url = None
|
|
212
|
+
cls._published_graph = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── UnknownPageState (fallback) ──────────────────────────────────
|
|
216
|
+
|
|
217
|
+
UNKNOWN_STATE = PageState(
|
|
218
|
+
"UnknownPage",
|
|
219
|
+
url_pattern=".*",
|
|
220
|
+
keywords=["unknown", "generic", "fallback"],
|
|
221
|
+
observations=[
|
|
222
|
+
Observation(
|
|
223
|
+
"links",
|
|
224
|
+
description="All links on the page",
|
|
225
|
+
js="Array.from(document.querySelectorAll('a[href]')).map(a => ({text: a.textContent?.trim(), href: a.href})).filter(l => l.text)",
|
|
226
|
+
),
|
|
227
|
+
Observation(
|
|
228
|
+
"buttons",
|
|
229
|
+
description="All buttons on the page",
|
|
230
|
+
js="Array.from(document.querySelectorAll('button, [role=\"button\"], input[type=\"submit\"]')).map(b => ({text: b.textContent?.trim() || b.value || b.getAttribute('aria-label'), type: b.tagName.toLowerCase()}))",
|
|
231
|
+
),
|
|
232
|
+
Observation(
|
|
233
|
+
"inputs",
|
|
234
|
+
description="All input fields on the page",
|
|
235
|
+
js="Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).map(i => ({type: i.type || i.tagName.toLowerCase(), name: i.name, id: i.id, placeholder: i.placeholder, value: i.value}))",
|
|
236
|
+
),
|
|
237
|
+
Observation(
|
|
238
|
+
"headings",
|
|
239
|
+
description="All headings on the page",
|
|
240
|
+
js="Array.from(document.querySelectorAll('h1, h2, h3')).map(h => ({level: h.tagName, text: h.textContent?.trim()}))",
|
|
241
|
+
),
|
|
242
|
+
Observation(
|
|
243
|
+
"title",
|
|
244
|
+
description="Page title",
|
|
245
|
+
js="document.title",
|
|
246
|
+
),
|
|
247
|
+
],
|
|
248
|
+
transitions=[
|
|
249
|
+
Transition(
|
|
250
|
+
"click_element",
|
|
251
|
+
description="Click any element by selector",
|
|
252
|
+
params={"selector": "str"},
|
|
253
|
+
steps=[
|
|
254
|
+
Step("click", "$selector", expects_transition=True),
|
|
255
|
+
],
|
|
256
|
+
),
|
|
257
|
+
Transition(
|
|
258
|
+
"type_into",
|
|
259
|
+
description="Type text into an element",
|
|
260
|
+
params={"selector": "str", "text": "str"},
|
|
261
|
+
steps=[
|
|
262
|
+
Step("clear_and_type", "$selector", text="$text"),
|
|
263
|
+
],
|
|
264
|
+
),
|
|
265
|
+
Transition(
|
|
266
|
+
"navigate_to",
|
|
267
|
+
description="Navigate to a URL",
|
|
268
|
+
params={"url": "str"},
|
|
269
|
+
steps=[
|
|
270
|
+
Step("navigate", "$url"),
|
|
271
|
+
],
|
|
272
|
+
),
|
|
273
|
+
],
|
|
274
|
+
)
|