code-puppy 0.0.177__py3-none-any.whl → 0.0.179__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.
@@ -1,472 +0,0 @@
1
- """JavaScript execution and advanced page manipulation tools."""
2
-
3
- from typing import Any, Dict, Optional
4
-
5
- from pydantic_ai import RunContext
6
-
7
- from code_puppy.messaging import emit_info
8
- from code_puppy.tools.common import generate_group_id
9
-
10
- from .camoufox_manager import get_camoufox_manager
11
-
12
-
13
- async def execute_javascript(
14
- script: str,
15
- timeout: int = 30000,
16
- ) -> Dict[str, Any]:
17
- """Execute JavaScript code in the browser context."""
18
- group_id = generate_group_id("browser_execute_js", script[:100])
19
- emit_info(
20
- f"[bold white on blue] BROWSER EXECUTE JS [/bold white on blue] 📜 script='{script[:100]}{'...' if len(script) > 100 else ''}'",
21
- message_group=group_id,
22
- )
23
- try:
24
- browser_manager = get_camoufox_manager()
25
- page = await browser_manager.get_current_page()
26
-
27
- if not page:
28
- return {"success": False, "error": "No active browser page available"}
29
-
30
- # Execute JavaScript
31
- result = await page.evaluate(script, timeout=timeout)
32
-
33
- emit_info(
34
- "[green]JavaScript executed successfully[/green]", message_group=group_id
35
- )
36
-
37
- return {"success": True, "script": script, "result": result}
38
-
39
- except Exception as e:
40
- emit_info(
41
- f"[red]JavaScript execution failed: {str(e)}[/red]", message_group=group_id
42
- )
43
- return {"success": False, "error": str(e), "script": script}
44
-
45
-
46
- async def scroll_page(
47
- direction: str = "down",
48
- amount: int = 3,
49
- element_selector: Optional[str] = None,
50
- ) -> Dict[str, Any]:
51
- """Scroll the page or a specific element."""
52
- target = element_selector or "page"
53
- group_id = generate_group_id("browser_scroll", f"{direction}_{amount}_{target}")
54
- emit_info(
55
- f"[bold white on blue] BROWSER SCROLL [/bold white on blue] 📋 direction={direction} amount={amount} target='{target}'",
56
- message_group=group_id,
57
- )
58
- try:
59
- browser_manager = get_camoufox_manager()
60
- page = await browser_manager.get_current_page()
61
-
62
- if not page:
63
- return {"success": False, "error": "No active browser page available"}
64
-
65
- if element_selector:
66
- # Scroll specific element
67
- element = page.locator(element_selector)
68
- await element.scroll_into_view_if_needed()
69
-
70
- # Get element's current scroll position and dimensions
71
- scroll_info = await element.evaluate("""
72
- el => {
73
- const rect = el.getBoundingClientRect();
74
- return {
75
- scrollTop: el.scrollTop,
76
- scrollLeft: el.scrollLeft,
77
- scrollHeight: el.scrollHeight,
78
- scrollWidth: el.scrollWidth,
79
- clientHeight: el.clientHeight,
80
- clientWidth: el.clientWidth
81
- };
82
- }
83
- """)
84
-
85
- # Calculate scroll amount based on element size
86
- scroll_amount = scroll_info["clientHeight"] * amount / 3
87
-
88
- if direction.lower() == "down":
89
- await element.evaluate(f"el => el.scrollTop += {scroll_amount}")
90
- elif direction.lower() == "up":
91
- await element.evaluate(f"el => el.scrollTop -= {scroll_amount}")
92
- elif direction.lower() == "left":
93
- await element.evaluate(f"el => el.scrollLeft -= {scroll_amount}")
94
- elif direction.lower() == "right":
95
- await element.evaluate(f"el => el.scrollLeft += {scroll_amount}")
96
-
97
- target = f"element '{element_selector}'"
98
-
99
- else:
100
- # Scroll page
101
- viewport_height = await page.evaluate("() => window.innerHeight")
102
- scroll_amount = viewport_height * amount / 3
103
-
104
- if direction.lower() == "down":
105
- await page.evaluate(f"window.scrollBy(0, {scroll_amount})")
106
- elif direction.lower() == "up":
107
- await page.evaluate(f"window.scrollBy(0, -{scroll_amount})")
108
- elif direction.lower() == "left":
109
- await page.evaluate(f"window.scrollBy(-{scroll_amount}, 0)")
110
- elif direction.lower() == "right":
111
- await page.evaluate(f"window.scrollBy({scroll_amount}, 0)")
112
-
113
- target = "page"
114
-
115
- # Get current scroll position
116
- scroll_pos = await page.evaluate("""
117
- () => ({
118
- x: window.pageXOffset,
119
- y: window.pageYOffset
120
- })
121
- """)
122
-
123
- emit_info(
124
- f"[green]Scrolled {target} {direction}[/green]", message_group=group_id
125
- )
126
-
127
- return {
128
- "success": True,
129
- "direction": direction,
130
- "amount": amount,
131
- "target": target,
132
- "scroll_position": scroll_pos,
133
- }
134
-
135
- except Exception as e:
136
- return {
137
- "success": False,
138
- "error": str(e),
139
- "direction": direction,
140
- "element_selector": element_selector,
141
- }
142
-
143
-
144
- async def scroll_to_element(
145
- selector: str,
146
- timeout: int = 10000,
147
- ) -> Dict[str, Any]:
148
- """Scroll to bring an element into view."""
149
- group_id = generate_group_id("browser_scroll_to_element", selector[:100])
150
- emit_info(
151
- f"[bold white on blue] BROWSER SCROLL TO ELEMENT [/bold white on blue] 🎯 selector='{selector}'",
152
- message_group=group_id,
153
- )
154
- try:
155
- browser_manager = get_camoufox_manager()
156
- page = await browser_manager.get_current_page()
157
-
158
- if not page:
159
- return {"success": False, "error": "No active browser page available"}
160
-
161
- element = page.locator(selector)
162
- await element.wait_for(state="attached", timeout=timeout)
163
- await element.scroll_into_view_if_needed()
164
-
165
- # Check if element is now visible
166
- is_visible = await element.is_visible()
167
-
168
- emit_info(
169
- f"[green]Scrolled to element: {selector}[/green]", message_group=group_id
170
- )
171
-
172
- return {"success": True, "selector": selector, "visible": is_visible}
173
-
174
- except Exception as e:
175
- return {"success": False, "error": str(e), "selector": selector}
176
-
177
-
178
- async def set_viewport_size(
179
- width: int,
180
- height: int,
181
- ) -> Dict[str, Any]:
182
- """Set the viewport size."""
183
- group_id = generate_group_id("browser_set_viewport", f"{width}x{height}")
184
- emit_info(
185
- f"[bold white on blue] BROWSER SET VIEWPORT [/bold white on blue] 🖥️ size={width}x{height}",
186
- message_group=group_id,
187
- )
188
- try:
189
- browser_manager = get_camoufox_manager()
190
- page = await browser_manager.get_current_page()
191
-
192
- if not page:
193
- return {"success": False, "error": "No active browser page available"}
194
-
195
- await page.set_viewport_size({"width": width, "height": height})
196
-
197
- emit_info(
198
- f"[green]Set viewport size to {width}x{height}[/green]",
199
- message_group=group_id,
200
- )
201
-
202
- return {"success": True, "width": width, "height": height}
203
-
204
- except Exception as e:
205
- return {"success": False, "error": str(e), "width": width, "height": height}
206
-
207
-
208
- async def wait_for_element(
209
- selector: str,
210
- state: str = "visible",
211
- timeout: int = 30000,
212
- ) -> Dict[str, Any]:
213
- """Wait for an element to reach a specific state."""
214
- group_id = generate_group_id("browser_wait_for_element", f"{selector[:50]}_{state}")
215
- emit_info(
216
- f"[bold white on blue] BROWSER WAIT FOR ELEMENT [/bold white on blue] ⏱️ selector='{selector}' state={state} timeout={timeout}ms",
217
- message_group=group_id,
218
- )
219
- try:
220
- browser_manager = get_camoufox_manager()
221
- page = await browser_manager.get_current_page()
222
-
223
- if not page:
224
- return {"success": False, "error": "No active browser page available"}
225
-
226
- element = page.locator(selector)
227
- await element.wait_for(state=state, timeout=timeout)
228
-
229
- emit_info(
230
- f"[green]Element {selector} is now {state}[/green]", message_group=group_id
231
- )
232
-
233
- return {"success": True, "selector": selector, "state": state}
234
-
235
- except Exception as e:
236
- return {"success": False, "error": str(e), "selector": selector, "state": state}
237
-
238
-
239
- async def highlight_element(
240
- selector: str,
241
- color: str = "red",
242
- timeout: int = 10000,
243
- ) -> Dict[str, Any]:
244
- """Highlight an element with a colored border."""
245
- group_id = generate_group_id(
246
- "browser_highlight_element", f"{selector[:50]}_{color}"
247
- )
248
- emit_info(
249
- f"[bold white on blue] BROWSER HIGHLIGHT ELEMENT [/bold white on blue] 🔦 selector='{selector}' color={color}",
250
- message_group=group_id,
251
- )
252
- try:
253
- browser_manager = get_camoufox_manager()
254
- page = await browser_manager.get_current_page()
255
-
256
- if not page:
257
- return {"success": False, "error": "No active browser page available"}
258
-
259
- element = page.locator(selector)
260
- await element.wait_for(state="visible", timeout=timeout)
261
-
262
- # Add highlight style
263
- highlight_script = f"""
264
- el => {{
265
- el.style.outline = '3px solid {color}';
266
- el.style.outlineOffset = '2px';
267
- el.style.backgroundColor = '{color}20'; // 20% opacity
268
- el.setAttribute('data-highlighted', 'true');
269
- }}
270
- """
271
-
272
- await element.evaluate(highlight_script)
273
-
274
- emit_info(
275
- f"[green]Highlighted element: {selector}[/green]", message_group=group_id
276
- )
277
-
278
- return {"success": True, "selector": selector, "color": color}
279
-
280
- except Exception as e:
281
- return {"success": False, "error": str(e), "selector": selector}
282
-
283
-
284
- async def clear_highlights() -> Dict[str, Any]:
285
- """Clear all element highlights."""
286
- group_id = generate_group_id("browser_clear_highlights")
287
- emit_info(
288
- "[bold white on blue] BROWSER CLEAR HIGHLIGHTS [/bold white on blue] 🧹",
289
- message_group=group_id,
290
- )
291
- try:
292
- browser_manager = get_camoufox_manager()
293
- page = await browser_manager.get_current_page()
294
-
295
- if not page:
296
- return {"success": False, "error": "No active browser page available"}
297
-
298
- # Remove all highlights
299
- clear_script = """
300
- () => {
301
- const highlighted = document.querySelectorAll('[data-highlighted="true"]');
302
- highlighted.forEach(el => {
303
- el.style.outline = '';
304
- el.style.outlineOffset = '';
305
- el.style.backgroundColor = '';
306
- el.removeAttribute('data-highlighted');
307
- });
308
- return highlighted.length;
309
- }
310
- """
311
-
312
- count = await page.evaluate(clear_script)
313
-
314
- emit_info(f"[green]Cleared {count} highlights[/green]", message_group=group_id)
315
-
316
- return {"success": True, "cleared_count": count}
317
-
318
- except Exception as e:
319
- return {"success": False, "error": str(e)}
320
-
321
-
322
- # Tool registration functions
323
- def register_execute_javascript(agent):
324
- """Register the JavaScript execution tool."""
325
-
326
- @agent.tool
327
- async def browser_execute_js(
328
- context: RunContext,
329
- script: str,
330
- timeout: int = 30000,
331
- ) -> Dict[str, Any]:
332
- """
333
- Execute JavaScript code in the browser context.
334
-
335
- Args:
336
- script: JavaScript code to execute
337
- timeout: Timeout in milliseconds
338
-
339
- Returns:
340
- Dict with execution results
341
- """
342
- return await execute_javascript(script, timeout)
343
-
344
-
345
- def register_scroll_page(agent):
346
- """Register the scroll page tool."""
347
-
348
- @agent.tool
349
- async def browser_scroll(
350
- context: RunContext,
351
- direction: str = "down",
352
- amount: int = 3,
353
- element_selector: Optional[str] = None,
354
- ) -> Dict[str, Any]:
355
- """
356
- Scroll the page or a specific element.
357
-
358
- Args:
359
- direction: Scroll direction (up, down, left, right)
360
- amount: Scroll amount multiplier (1-10)
361
- element_selector: Optional selector to scroll specific element
362
-
363
- Returns:
364
- Dict with scroll results
365
- """
366
- return await scroll_page(direction, amount, element_selector)
367
-
368
-
369
- def register_scroll_to_element(agent):
370
- """Register the scroll to element tool."""
371
-
372
- @agent.tool
373
- async def browser_scroll_to_element(
374
- context: RunContext,
375
- selector: str,
376
- timeout: int = 10000,
377
- ) -> Dict[str, Any]:
378
- """
379
- Scroll to bring an element into view.
380
-
381
- Args:
382
- selector: CSS or XPath selector for the element
383
- timeout: Timeout in milliseconds
384
-
385
- Returns:
386
- Dict with scroll results
387
- """
388
- return await scroll_to_element(selector, timeout)
389
-
390
-
391
- def register_set_viewport_size(agent):
392
- """Register the viewport size tool."""
393
-
394
- @agent.tool
395
- async def browser_set_viewport(
396
- context: RunContext,
397
- width: int,
398
- height: int,
399
- ) -> Dict[str, Any]:
400
- """
401
- Set the browser viewport size.
402
-
403
- Args:
404
- width: Viewport width in pixels
405
- height: Viewport height in pixels
406
-
407
- Returns:
408
- Dict with viewport size results
409
- """
410
- return await set_viewport_size(width, height)
411
-
412
-
413
- def register_wait_for_element(agent):
414
- """Register the wait for element tool."""
415
-
416
- @agent.tool
417
- async def browser_wait_for_element(
418
- context: RunContext,
419
- selector: str,
420
- state: str = "visible",
421
- timeout: int = 30000,
422
- ) -> Dict[str, Any]:
423
- """
424
- Wait for an element to reach a specific state.
425
-
426
- Args:
427
- selector: CSS or XPath selector for the element
428
- state: State to wait for (visible, hidden, attached, detached)
429
- timeout: Timeout in milliseconds
430
-
431
- Returns:
432
- Dict with wait results
433
- """
434
- return await wait_for_element(selector, state, timeout)
435
-
436
-
437
- def register_browser_highlight_element(agent):
438
- """Register the element highlighting tool."""
439
-
440
- @agent.tool
441
- async def browser_highlight_element(
442
- context: RunContext,
443
- selector: str,
444
- color: str = "red",
445
- timeout: int = 10000,
446
- ) -> Dict[str, Any]:
447
- """
448
- Highlight an element with a colored border for visual identification.
449
-
450
- Args:
451
- selector: CSS or XPath selector for the element
452
- color: Highlight color (red, blue, green, yellow, etc.)
453
- timeout: Timeout in milliseconds
454
-
455
- Returns:
456
- Dict with highlight results
457
- """
458
- return await highlight_element(selector, color, timeout)
459
-
460
-
461
- def register_browser_clear_highlights(agent):
462
- """Register the clear highlights tool."""
463
-
464
- @agent.tool
465
- async def browser_clear_highlights(context: RunContext) -> Dict[str, Any]:
466
- """
467
- Clear all element highlights from the page.
468
-
469
- Returns:
470
- Dict with clear results
471
- """
472
- return await clear_highlights()
@@ -1,223 +0,0 @@
1
- """Browser workflow management tools for saving and reusing automation patterns."""
2
-
3
- from pathlib import Path
4
- from typing import Any, Dict
5
-
6
- from pydantic_ai import RunContext
7
-
8
- from code_puppy.messaging import emit_info
9
- from code_puppy.tools.common import generate_group_id
10
-
11
-
12
- def get_workflows_directory() -> Path:
13
- """Get the browser workflows directory, creating it if it doesn't exist."""
14
- home_dir = Path.home()
15
- workflows_dir = home_dir / ".code_puppy" / "browser_workflows"
16
- workflows_dir.mkdir(parents=True, exist_ok=True)
17
- return workflows_dir
18
-
19
-
20
- async def save_workflow(name: str, content: str) -> Dict[str, Any]:
21
- """Save a browser workflow as a markdown file."""
22
- group_id = generate_group_id("save_workflow", name)
23
- emit_info(
24
- f"[bold white on blue] SAVE WORKFLOW [/bold white on blue] 💾 name='{name}'",
25
- message_group=group_id,
26
- )
27
-
28
- try:
29
- workflows_dir = get_workflows_directory()
30
-
31
- # Clean up the filename - remove spaces, special chars, etc.
32
- safe_name = "".join(c for c in name if c.isalnum() or c in ("-", "_")).lower()
33
- if not safe_name:
34
- safe_name = "workflow"
35
-
36
- # Ensure .md extension
37
- if not safe_name.endswith(".md"):
38
- safe_name += ".md"
39
-
40
- workflow_path = workflows_dir / safe_name
41
-
42
- # Write the workflow content
43
- with open(workflow_path, "w", encoding="utf-8") as f:
44
- f.write(content)
45
-
46
- emit_info(
47
- f"[green]✅ Workflow saved successfully: {workflow_path}[/green]",
48
- message_group=group_id,
49
- )
50
-
51
- return {
52
- "success": True,
53
- "path": str(workflow_path),
54
- "name": safe_name,
55
- "size": len(content),
56
- }
57
-
58
- except Exception as e:
59
- emit_info(
60
- f"[red]❌ Failed to save workflow: {e}[/red]",
61
- message_group=group_id,
62
- )
63
- return {"success": False, "error": str(e), "name": name}
64
-
65
-
66
- async def list_workflows() -> Dict[str, Any]:
67
- """List all available browser workflows."""
68
- group_id = generate_group_id("list_workflows")
69
- emit_info(
70
- "[bold white on blue] LIST WORKFLOWS [/bold white on blue] 📋",
71
- message_group=group_id,
72
- )
73
-
74
- try:
75
- workflows_dir = get_workflows_directory()
76
-
77
- # Find all .md files in the workflows directory
78
- workflow_files = list(workflows_dir.glob("*.md"))
79
-
80
- workflows = []
81
- for workflow_file in workflow_files:
82
- try:
83
- stat = workflow_file.stat()
84
- workflows.append(
85
- {
86
- "name": workflow_file.name,
87
- "path": str(workflow_file),
88
- "size": stat.st_size,
89
- "modified": stat.st_mtime,
90
- }
91
- )
92
- except Exception as e:
93
- emit_info(
94
- f"[yellow]Warning: Could not read {workflow_file}: {e}[/yellow]"
95
- )
96
-
97
- # Sort by modification time (newest first)
98
- workflows.sort(key=lambda x: x["modified"], reverse=True)
99
-
100
- emit_info(
101
- f"[green]✅ Found {len(workflows)} workflow(s)[/green]",
102
- message_group=group_id,
103
- )
104
-
105
- return {
106
- "success": True,
107
- "workflows": workflows,
108
- "count": len(workflows),
109
- "directory": str(workflows_dir),
110
- }
111
-
112
- except Exception as e:
113
- emit_info(
114
- f"[red]❌ Failed to list workflows: {e}[/red]",
115
- message_group=group_id,
116
- )
117
- return {"success": False, "error": str(e)}
118
-
119
-
120
- async def read_workflow(name: str) -> Dict[str, Any]:
121
- """Read a saved browser workflow."""
122
- group_id = generate_group_id("read_workflow", name)
123
- emit_info(
124
- f"[bold white on blue] READ WORKFLOW [/bold white on blue] 📖 name='{name}'",
125
- message_group=group_id,
126
- )
127
-
128
- try:
129
- workflows_dir = get_workflows_directory()
130
-
131
- # Handle both with and without .md extension
132
- if not name.endswith(".md"):
133
- name += ".md"
134
-
135
- workflow_path = workflows_dir / name
136
-
137
- if not workflow_path.exists():
138
- emit_info(
139
- f"[red]❌ Workflow not found: {name}[/red]",
140
- message_group=group_id,
141
- )
142
- return {
143
- "success": False,
144
- "error": f"Workflow '{name}' not found",
145
- "name": name,
146
- }
147
-
148
- # Read the workflow content
149
- with open(workflow_path, "r", encoding="utf-8") as f:
150
- content = f.read()
151
-
152
- emit_info(
153
- f"[green]✅ Workflow read successfully: {len(content)} characters[/green]",
154
- message_group=group_id,
155
- )
156
-
157
- return {
158
- "success": True,
159
- "name": name,
160
- "content": content,
161
- "path": str(workflow_path),
162
- "size": len(content),
163
- }
164
-
165
- except Exception as e:
166
- emit_info(
167
- f"[red]❌ Failed to read workflow: {e}[/red]",
168
- message_group=group_id,
169
- )
170
- return {"success": False, "error": str(e), "name": name}
171
-
172
-
173
- def register_save_workflow(agent):
174
- """Register the save workflow tool."""
175
-
176
- async def save_workflow_tool(
177
- context: RunContext,
178
- name: str,
179
- content: str,
180
- ) -> Dict[str, Any]:
181
- """
182
- Save a browser automation workflow as a markdown file.
183
-
184
- Args:
185
- name: Name for the workflow (will be sanitized for filename)
186
- content: Markdown content describing the workflow steps
187
-
188
- Returns:
189
- Dict with success status and file path
190
- """
191
- return await save_workflow(name, content)
192
-
193
-
194
- def register_list_workflows(agent):
195
- """Register the list workflows tool."""
196
-
197
- async def list_workflows_tool(context: RunContext) -> Dict[str, Any]:
198
- """
199
- List all saved browser automation workflows.
200
-
201
- Returns:
202
- Dict with list of available workflows and their metadata
203
- """
204
- return await list_workflows()
205
-
206
-
207
- def register_read_workflow(agent):
208
- """Register the read workflow tool."""
209
-
210
- async def read_workflow_tool(
211
- context: RunContext,
212
- name: str,
213
- ) -> Dict[str, Any]:
214
- """
215
- Read a saved browser automation workflow.
216
-
217
- Args:
218
- name: Name of the workflow to read (with or without .md extension)
219
-
220
- Returns:
221
- Dict with workflow content and metadata
222
- """
223
- return await read_workflow(name)