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.
Files changed (58) hide show
  1. loom_code/__init__.py +22 -0
  2. loom_code/_post_commit.py +119 -0
  3. loom_code/agent.py +544 -0
  4. loom_code/approval.py +616 -0
  5. loom_code/browse/__init__.py +291 -0
  6. loom_code/browse/act.py +467 -0
  7. loom_code/browse/observe.py +249 -0
  8. loom_code/browse/session.py +96 -0
  9. loom_code/browse/verify.py +194 -0
  10. loom_code/checkpoint.py +283 -0
  11. loom_code/cli.py +495 -0
  12. loom_code/code_index.py +703 -0
  13. loom_code/compact.py +143 -0
  14. loom_code/consent.py +47 -0
  15. loom_code/credentials.py +527 -0
  16. loom_code/edit_tool.py +635 -0
  17. loom_code/extensions.py +522 -0
  18. loom_code/file_history.py +322 -0
  19. loom_code/file_tools.py +93 -0
  20. loom_code/git_hook.py +200 -0
  21. loom_code/grep_tool.py +430 -0
  22. loom_code/hooks.py +297 -0
  23. loom_code/loominit/__init__.py +23 -0
  24. loom_code/loominit/_ast_walk.py +429 -0
  25. loom_code/loominit/_files.py +284 -0
  26. loom_code/loominit/_graph.py +141 -0
  27. loom_code/loominit/_resolve.py +392 -0
  28. loom_code/loominit/_tests_map.py +108 -0
  29. loom_code/loominit/extractor.py +332 -0
  30. loom_code/loominit/repomap.py +225 -0
  31. loom_code/loominit/schema.py +242 -0
  32. loom_code/lsp_tools.py +396 -0
  33. loom_code/mcp_host.py +79 -0
  34. loom_code/operator.py +449 -0
  35. loom_code/paste.py +97 -0
  36. loom_code/paths.py +52 -0
  37. loom_code/permissions.py +177 -0
  38. loom_code/project.py +104 -0
  39. loom_code/prompts.py +451 -0
  40. loom_code/render.py +783 -0
  41. loom_code/repl.py +4080 -0
  42. loom_code/rules.py +267 -0
  43. loom_code/sandboxed_bash.py +176 -0
  44. loom_code/scribe.py +88 -0
  45. loom_code/skills/__init__.py +16 -0
  46. loom_code/skills/graphify/SKILL.md +97 -0
  47. loom_code/skills/graphify/tools.py +570 -0
  48. loom_code/trust.py +216 -0
  49. loom_code/turn.py +169 -0
  50. loom_code/web_fetch.py +370 -0
  51. loom_code/workers.py +758 -0
  52. loom_code/worktree.py +134 -0
  53. loom_code-0.1.1.dist-info/METADATA +224 -0
  54. loom_code-0.1.1.dist-info/RECORD +58 -0
  55. loom_code-0.1.1.dist-info/WHEEL +5 -0
  56. loom_code-0.1.1.dist-info/entry_points.txt +2 -0
  57. loom_code-0.1.1.dist-info/licenses/LICENSE +21 -0
  58. 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"]