loom-code 0.1.1__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.
- loom_code/__init__.py +22 -0
- loom_code/_post_commit.py +119 -0
- loom_code/agent.py +544 -0
- loom_code/approval.py +616 -0
- loom_code/browse/__init__.py +291 -0
- loom_code/browse/act.py +467 -0
- loom_code/browse/observe.py +249 -0
- loom_code/browse/session.py +96 -0
- loom_code/browse/verify.py +194 -0
- loom_code/checkpoint.py +283 -0
- loom_code/cli.py +495 -0
- loom_code/code_index.py +703 -0
- loom_code/compact.py +143 -0
- loom_code/consent.py +47 -0
- loom_code/credentials.py +527 -0
- loom_code/edit_tool.py +635 -0
- loom_code/extensions.py +522 -0
- loom_code/file_history.py +322 -0
- loom_code/file_tools.py +93 -0
- loom_code/git_hook.py +200 -0
- loom_code/grep_tool.py +430 -0
- loom_code/hooks.py +297 -0
- loom_code/loominit/__init__.py +23 -0
- loom_code/loominit/_ast_walk.py +429 -0
- loom_code/loominit/_files.py +284 -0
- loom_code/loominit/_graph.py +141 -0
- loom_code/loominit/_resolve.py +392 -0
- loom_code/loominit/_tests_map.py +108 -0
- loom_code/loominit/extractor.py +332 -0
- loom_code/loominit/repomap.py +225 -0
- loom_code/loominit/schema.py +242 -0
- loom_code/lsp_tools.py +396 -0
- loom_code/mcp_host.py +79 -0
- loom_code/operator.py +449 -0
- loom_code/paste.py +97 -0
- loom_code/paths.py +52 -0
- loom_code/permissions.py +177 -0
- loom_code/project.py +104 -0
- loom_code/prompts.py +451 -0
- loom_code/render.py +783 -0
- loom_code/repl.py +4080 -0
- loom_code/rules.py +267 -0
- loom_code/sandboxed_bash.py +176 -0
- loom_code/scribe.py +88 -0
- loom_code/skills/__init__.py +16 -0
- loom_code/skills/graphify/SKILL.md +97 -0
- loom_code/skills/graphify/tools.py +570 -0
- loom_code/trust.py +216 -0
- loom_code/turn.py +169 -0
- loom_code/web_fetch.py +370 -0
- loom_code/workers.py +758 -0
- loom_code/worktree.py +134 -0
- loom_code-0.1.1.dist-info/METADATA +224 -0
- loom_code-0.1.1.dist-info/RECORD +58 -0
- loom_code-0.1.1.dist-info/WHEEL +5 -0
- loom_code-0.1.1.dist-info/entry_points.txt +2 -0
- loom_code-0.1.1.dist-info/licenses/LICENSE +21 -0
- loom_code-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""loom-code's focused browser engine for ``/computer`` mode.
|
|
2
|
+
|
|
3
|
+
Exposes ``browse_tools(model)`` — a list of loom-code tools backed by ONE
|
|
4
|
+
shared headed-Chromium :class:`BrowserSession`. The agent drives the web
|
|
5
|
+
through these instead of the Playwright MCP server (which had ephemeral
|
|
6
|
+
refs that broke on dynamic pages). Reliability comes from stable
|
|
7
|
+
``data-loom-id`` handles (observe), re-resolve-fresh + overlay-safe
|
|
8
|
+
acting (act), and vision verification (check).
|
|
9
|
+
|
|
10
|
+
Tools:
|
|
11
|
+
page_open(url) navigate; launches the visible browser
|
|
12
|
+
page_observe() list interactive elements with stable [ids]
|
|
13
|
+
page_act(id, action, text) click/type/select on an element by id
|
|
14
|
+
page_check(question) vision-verify what's on screen
|
|
15
|
+
page_back() browser back
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from loomflow import tool
|
|
21
|
+
from loomflow.tools.registry import Tool
|
|
22
|
+
|
|
23
|
+
from .act import act, fill_combobox, press_key, scroll, set_date
|
|
24
|
+
from .observe import observe, read_text
|
|
25
|
+
from .session import BrowserSession
|
|
26
|
+
from .verify import look, verify
|
|
27
|
+
|
|
28
|
+
# Live browser sessions created this process. The REPL closes them all on
|
|
29
|
+
# exit so the headed Chromium window doesn't linger after loom-code quits.
|
|
30
|
+
_LIVE_SESSIONS: list[BrowserSession] = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def close_all_browsers() -> None:
|
|
34
|
+
"""Close every browser session opened by /computer. Best-effort;
|
|
35
|
+
called from the REPL's exit teardown. Never raises."""
|
|
36
|
+
for s in _LIVE_SESSIONS:
|
|
37
|
+
try:
|
|
38
|
+
await s.close()
|
|
39
|
+
except Exception: # noqa: BLE001 — teardown must not fail exit
|
|
40
|
+
pass
|
|
41
|
+
_LIVE_SESSIONS.clear()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def browse_tools(model: str | None = None) -> list[Tool]:
|
|
45
|
+
"""Build the page_* tools over a single shared browser session. The
|
|
46
|
+
session launches lazily on the first page_open and persists across
|
|
47
|
+
calls so observe → act → observe walks the same evolving page."""
|
|
48
|
+
session = BrowserSession()
|
|
49
|
+
_LIVE_SESSIONS.append(session) # so the REPL can close it on exit
|
|
50
|
+
|
|
51
|
+
async def page_open(url: str) -> str:
|
|
52
|
+
"""Open a URL in the visible browser, then list what's on it."""
|
|
53
|
+
try:
|
|
54
|
+
await session.goto(url)
|
|
55
|
+
except Exception as exc: # noqa: BLE001
|
|
56
|
+
return f"could not open {url}: {exc}"
|
|
57
|
+
_els, rendered = await observe(session.page)
|
|
58
|
+
return f"opened {url}\n\n{rendered}"
|
|
59
|
+
|
|
60
|
+
async def page_observe() -> str:
|
|
61
|
+
"""Re-read the current page: list interactive elements + their
|
|
62
|
+
stable ids. Call this before EVERY act (ids change as the page
|
|
63
|
+
changes) and after navigation."""
|
|
64
|
+
try:
|
|
65
|
+
_els, rendered = await observe(session.page)
|
|
66
|
+
except RuntimeError:
|
|
67
|
+
return "no page open yet — call page_open(url) first."
|
|
68
|
+
return rendered
|
|
69
|
+
|
|
70
|
+
async def page_act(id: int, action: str, text: str = "") -> str:
|
|
71
|
+
"""Act on an element by its [id] from the latest page_observe.
|
|
72
|
+
action: click | type (with text) | clear | press_enter | select
|
|
73
|
+
(with text=option). After acting the page may change — call
|
|
74
|
+
page_observe again to get fresh ids."""
|
|
75
|
+
try:
|
|
76
|
+
page = session.page
|
|
77
|
+
except RuntimeError:
|
|
78
|
+
return "no page open yet — call page_open(url) first."
|
|
79
|
+
result = await act(page, id, action, text)
|
|
80
|
+
# Auto re-observe so the agent always sees the post-action state
|
|
81
|
+
# with fresh ids (this is what keeps ids from going stale).
|
|
82
|
+
_els, rendered = await observe(page)
|
|
83
|
+
return f"{result}\n\n{rendered}"
|
|
84
|
+
|
|
85
|
+
async def page_fill(id: int, value: str) -> str:
|
|
86
|
+
"""Fill an AUTOCOMPLETE / combobox field by id (origin/destination
|
|
87
|
+
on flight + map sites, address fields, search-with-suggestions).
|
|
88
|
+
Types, waits for the suggestion dropdown, and selects the first
|
|
89
|
+
match via keyboard — the robust way these widgets commit a value
|
|
90
|
+
(plain page_act 'type' reverts on them). Use this for any field
|
|
91
|
+
that shows a suggestion list. After it, page_check the value."""
|
|
92
|
+
try:
|
|
93
|
+
page = session.page
|
|
94
|
+
except RuntimeError:
|
|
95
|
+
return "no page open yet — call page_open(url) first."
|
|
96
|
+
result = await fill_combobox(page, id, value)
|
|
97
|
+
_els, rendered = await observe(page)
|
|
98
|
+
return f"{result}\n\n{rendered}"
|
|
99
|
+
|
|
100
|
+
async def page_set_date(id: int, date: str) -> str:
|
|
101
|
+
"""Pick a date in a calendar/date-picker by id (flight dates,
|
|
102
|
+
booking dates). Opens the picker and clicks the day matching the
|
|
103
|
+
date — pages forward through months if needed. date forms:
|
|
104
|
+
'2026-06-09' or 'June 9 2026'. Use this for ANY calendar widget;
|
|
105
|
+
do NOT guess day-cell ids with page_act."""
|
|
106
|
+
try:
|
|
107
|
+
page = session.page
|
|
108
|
+
except RuntimeError:
|
|
109
|
+
return "no page open yet — call page_open(url) first."
|
|
110
|
+
result = await set_date(page, id, date)
|
|
111
|
+
_els, rendered = await observe(page)
|
|
112
|
+
return f"{result}\n\n{rendered}"
|
|
113
|
+
|
|
114
|
+
async def page_scroll(direction: str = "down", amount: int = 1) -> str:
|
|
115
|
+
"""Scroll the page so lazy-loaded content (search results, flight
|
|
116
|
+
listings, feeds) renders. direction: down | up | top | bottom.
|
|
117
|
+
After scrolling, call page_read to capture the newly-loaded text.
|
|
118
|
+
Args: direction; amount (viewport-heights, default 1)."""
|
|
119
|
+
try:
|
|
120
|
+
page = session.page
|
|
121
|
+
except RuntimeError:
|
|
122
|
+
return "no page open yet — call page_open(url) first."
|
|
123
|
+
return await scroll(page, direction, amount)
|
|
124
|
+
|
|
125
|
+
async def page_read() -> str:
|
|
126
|
+
"""READ the page's visible text — prices, listings, results,
|
|
127
|
+
article text. Use this to extract OUTCOMES (e.g. flight prices,
|
|
128
|
+
product prices, search results). page_observe lists clickable
|
|
129
|
+
elements; page_read gives you the actual CONTENT to report."""
|
|
130
|
+
try:
|
|
131
|
+
page = session.page
|
|
132
|
+
except RuntimeError:
|
|
133
|
+
return "no page open yet — call page_open(url) first."
|
|
134
|
+
return await read_text(page)
|
|
135
|
+
|
|
136
|
+
async def page_look(question: str) -> str:
|
|
137
|
+
"""SEE the page — take a screenshot (with numbered [id] boxes on
|
|
138
|
+
elements) and have the vision model answer about it. Use when DOM
|
|
139
|
+
text isn't enough: reading prices/results, understanding layout,
|
|
140
|
+
confirming what a complex widget shows. Costs more than page_read
|
|
141
|
+
(sends an image), so use when you need to actually SEE. Arg:
|
|
142
|
+
question (what to look for)."""
|
|
143
|
+
try:
|
|
144
|
+
page = session.page
|
|
145
|
+
except RuntimeError:
|
|
146
|
+
return "no page open yet — call page_open(url) first."
|
|
147
|
+
return await look(page, question, model=model)
|
|
148
|
+
|
|
149
|
+
async def page_check(question: str) -> str:
|
|
150
|
+
"""Visually verify the page: screenshot + ask a yes/no question
|
|
151
|
+
(e.g. 'is Delhi the origin?'). Use after typing to confirm it
|
|
152
|
+
stuck, or when the DOM text is ambiguous."""
|
|
153
|
+
try:
|
|
154
|
+
page = session.page
|
|
155
|
+
except RuntimeError:
|
|
156
|
+
return "no page open yet — call page_open(url) first."
|
|
157
|
+
return await verify(page, question, model=model)
|
|
158
|
+
|
|
159
|
+
async def page_back() -> str:
|
|
160
|
+
"""Go back one page in history, then list what's on it."""
|
|
161
|
+
try:
|
|
162
|
+
page = session.page
|
|
163
|
+
except RuntimeError:
|
|
164
|
+
return "no page open yet — call page_open(url) first."
|
|
165
|
+
try:
|
|
166
|
+
await page.go_back(wait_until="domcontentloaded")
|
|
167
|
+
except Exception as exc: # noqa: BLE001
|
|
168
|
+
return f"could not go back: {exc}"
|
|
169
|
+
_els, rendered = await observe(page)
|
|
170
|
+
return rendered
|
|
171
|
+
|
|
172
|
+
async def page_press(key: str) -> str:
|
|
173
|
+
"""Press a global key — most useful: Escape to close an overlay /
|
|
174
|
+
dropdown that's blocking a click. Then re-observe."""
|
|
175
|
+
try:
|
|
176
|
+
page = session.page
|
|
177
|
+
except RuntimeError:
|
|
178
|
+
return "no page open yet — call page_open(url) first."
|
|
179
|
+
result = await press_key(page, key)
|
|
180
|
+
_els, rendered = await observe(page)
|
|
181
|
+
return f"{result}\n\n{rendered}"
|
|
182
|
+
|
|
183
|
+
tools: list[Tool] = [
|
|
184
|
+
tool(
|
|
185
|
+
name="page_open",
|
|
186
|
+
description=(
|
|
187
|
+
"Open a URL in the user's VISIBLE browser and list the "
|
|
188
|
+
"interactive elements on it. Start every web task here. "
|
|
189
|
+
"Arg: url."
|
|
190
|
+
),
|
|
191
|
+
)(page_open),
|
|
192
|
+
tool(
|
|
193
|
+
name="page_observe",
|
|
194
|
+
description=(
|
|
195
|
+
"List the current page's interactive elements with stable "
|
|
196
|
+
"[ids] + their values. Call before EVERY page_act (ids can "
|
|
197
|
+
"change as the page updates) and after any navigation."
|
|
198
|
+
),
|
|
199
|
+
)(page_observe),
|
|
200
|
+
tool(
|
|
201
|
+
name="page_act",
|
|
202
|
+
description=(
|
|
203
|
+
"Act on an element by its [id] from the latest "
|
|
204
|
+
"page_observe. Args: id (int); action (click | type | clear "
|
|
205
|
+
"| press_enter | select); text (for type/select). For an "
|
|
206
|
+
"autocomplete: type a few chars, page_observe, then click "
|
|
207
|
+
"the matching suggestion. Always set BOTH origin and "
|
|
208
|
+
"destination explicitly for travel sites."
|
|
209
|
+
),
|
|
210
|
+
)(page_act),
|
|
211
|
+
tool(
|
|
212
|
+
name="page_fill",
|
|
213
|
+
description=(
|
|
214
|
+
"Fill an AUTOCOMPLETE/combobox field (the kind that shows a "
|
|
215
|
+
"suggestion dropdown — flight origin/destination, maps, "
|
|
216
|
+
"address, search-with-suggestions). Types, waits for the "
|
|
217
|
+
"dropdown, and selects the first match via keyboard so the "
|
|
218
|
+
"value actually COMMITS (plain page_act 'type' reverts on "
|
|
219
|
+
"these). Use this instead of page_act type for any field "
|
|
220
|
+
"with suggestions. Args: id; value. Then page_check it."
|
|
221
|
+
),
|
|
222
|
+
)(page_fill),
|
|
223
|
+
tool(
|
|
224
|
+
name="page_scroll",
|
|
225
|
+
description=(
|
|
226
|
+
"Scroll the page so lazy-loaded content (search results, "
|
|
227
|
+
"flight/product listings, feeds) renders — then page_read "
|
|
228
|
+
"to capture it. Args: direction (down|up|top|bottom); "
|
|
229
|
+
"amount (default 1)."
|
|
230
|
+
),
|
|
231
|
+
)(page_scroll),
|
|
232
|
+
tool(
|
|
233
|
+
name="page_look",
|
|
234
|
+
description=(
|
|
235
|
+
"SEE the page with vision — screenshots it (numbered [id] "
|
|
236
|
+
"boxes on elements) and a vision model answers your "
|
|
237
|
+
"question. Use when DOM text isn't enough: reading "
|
|
238
|
+
"prices/results, understanding layout, or when page_read/"
|
|
239
|
+
"page_observe seem to miss content. Costs more (sends an "
|
|
240
|
+
"image) — use when you must actually SEE. Arg: question."
|
|
241
|
+
),
|
|
242
|
+
)(page_look),
|
|
243
|
+
tool(
|
|
244
|
+
name="page_read",
|
|
245
|
+
description=(
|
|
246
|
+
"READ the page's visible text content — prices, search "
|
|
247
|
+
"results, listings, article body. Use this to EXTRACT and "
|
|
248
|
+
"report outcomes (flight prices, product prices, results). "
|
|
249
|
+
"page_observe lists clickable elements; page_read gives the "
|
|
250
|
+
"actual content. No args."
|
|
251
|
+
),
|
|
252
|
+
)(page_read),
|
|
253
|
+
tool(
|
|
254
|
+
name="page_set_date",
|
|
255
|
+
description=(
|
|
256
|
+
"Pick a date in a calendar/date-picker by id (flight dates, "
|
|
257
|
+
"booking calendars). Opens the picker + clicks the matching "
|
|
258
|
+
"day, paging forward through months as needed. NEVER guess "
|
|
259
|
+
"day-cell ids with page_act — use this. Args: id; date "
|
|
260
|
+
"('2026-06-09' or 'June 9 2026')."
|
|
261
|
+
),
|
|
262
|
+
)(page_set_date),
|
|
263
|
+
tool(
|
|
264
|
+
name="page_check",
|
|
265
|
+
description=(
|
|
266
|
+
"Verify the page: returns the REAL current values of the "
|
|
267
|
+
"page's fields so you can confirm what landed where (e.g. "
|
|
268
|
+
"'is Delhi in Where from?'). Use after filling. Arg: "
|
|
269
|
+
"question."
|
|
270
|
+
),
|
|
271
|
+
)(page_check),
|
|
272
|
+
tool(
|
|
273
|
+
name="page_back",
|
|
274
|
+
description="Go back one page in browser history, then list it.",
|
|
275
|
+
)(page_back),
|
|
276
|
+
tool(
|
|
277
|
+
name="page_press",
|
|
278
|
+
description=(
|
|
279
|
+
"Press a global key — usually Escape, to dismiss an overlay/"
|
|
280
|
+
"dropdown blocking a click. Then the page is re-listed. "
|
|
281
|
+
"Arg: key (e.g. 'Escape')."
|
|
282
|
+
),
|
|
283
|
+
)(page_press),
|
|
284
|
+
]
|
|
285
|
+
# Stash the session so the REPL can close it on exit / mode-off.
|
|
286
|
+
for t in tools:
|
|
287
|
+
t._loom_browser_session = session
|
|
288
|
+
return tools
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
__all__ = ["browse_tools", "BrowserSession"]
|