gemlings-browser 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +105 -0
- data/README.md +114 -0
- data/exe/gemlings-browser +22 -0
- data/lib/gemlings/browser.rb +21 -0
- data/lib/gemlings_browser/browser_manager.rb +153 -0
- data/lib/gemlings_browser/chrome_discovery.rb +98 -0
- data/lib/gemlings_browser/configuration.rb +59 -0
- data/lib/gemlings_browser/installer.rb +35 -0
- data/lib/gemlings_browser/page_registry.rb +99 -0
- data/lib/gemlings_browser/temp_files.rb +52 -0
- data/lib/gemlings_browser/tools/browse_tool.rb +118 -0
- data/lib/gemlings_browser/tools/close_page_tool.rb +23 -0
- data/lib/gemlings_browser/tools/list_pages_tool.rb +27 -0
- data/lib/gemlings_browser/tools/screenshot_tool.rb +24 -0
- data/lib/gemlings_browser/version.rb +5 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cef53274076111e67c881d45ebeb7805e97bd3dafc99cee1a8b6e0a1379eb414
|
|
4
|
+
data.tar.gz: b876597b34cde7a45c0c6ace5f90efe489d125478794d6d94fd20cebe8ac58c5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9e623e7c85927529955ac58eb5ae89a5f4f11bf162e566db501de1d82a800d0d2d0755233a1255b8241566112f713d376292fdb82e9fdbc812886c43c3a79520
|
|
7
|
+
data.tar.gz: 81037dcbcfad32530b4f6e5cb6833e8e75b14d98d2d968467fbfacdd511c9dce8ce5f72d32d0ccd7a9f133bea9448b16d5d84f6edee6cd1344a80a13c32f291f
|
data/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-Apache-2.0
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Abhishek Parolkar
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
Abhishek Parolkar and his affiliated entities, the party offering the Software and Content under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software and Content
|
|
18
|
+
|
|
19
|
+
The "Software and Content" refers to each version of the software, documentation, approaches, strategies, methodologies, and all other materials that we make available under these Terms and Conditions, as indicated by our inclusion of these Terms and Conditions with the Software and Content.
|
|
20
|
+
|
|
21
|
+
### License Grant
|
|
22
|
+
|
|
23
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
24
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
25
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
26
|
+
and redistribute the Software and Content for any Permitted Purpose identified below.
|
|
27
|
+
|
|
28
|
+
### Permitted Purpose
|
|
29
|
+
|
|
30
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
31
|
+
means making the Software and Content available to others in a commercial product or
|
|
32
|
+
service that:
|
|
33
|
+
|
|
34
|
+
1. substitutes for the Software and Content;
|
|
35
|
+
|
|
36
|
+
2. substitutes for any other product or service we offer using the Software and Content
|
|
37
|
+
that exists as of the date we make the Software and Content available; or
|
|
38
|
+
|
|
39
|
+
3. offers the same or substantially similar functionality as the Software and Content.
|
|
40
|
+
|
|
41
|
+
Permitted Purposes specifically include using the Software and Content:
|
|
42
|
+
|
|
43
|
+
1. for your internal use and access;
|
|
44
|
+
|
|
45
|
+
2. for non-commercial education;
|
|
46
|
+
|
|
47
|
+
3. for non-commercial research; and
|
|
48
|
+
|
|
49
|
+
4. in connection with professional services that you provide to a licensee
|
|
50
|
+
using the Software and Content in accordance with these Terms and Conditions.
|
|
51
|
+
|
|
52
|
+
### Patents
|
|
53
|
+
|
|
54
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
55
|
+
patents, the license grant above includes a license under our patents. If you
|
|
56
|
+
make a claim against any party that the Software and Content infringes or contributes to
|
|
57
|
+
the infringement of any patent, then your patent license to the Software and Content ends
|
|
58
|
+
immediately.
|
|
59
|
+
|
|
60
|
+
### Redistribution
|
|
61
|
+
|
|
62
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
63
|
+
the Software and Content.
|
|
64
|
+
|
|
65
|
+
If you redistribute any copies, modifications or derivatives of the Software and Content,
|
|
66
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
67
|
+
remove any copyright notices provided in or with the Software and Content.
|
|
68
|
+
|
|
69
|
+
### Disclaimer
|
|
70
|
+
|
|
71
|
+
THE SOFTWARE AND CONTENT IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
72
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
73
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
74
|
+
|
|
75
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
76
|
+
SOFTWARE AND CONTENT, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
77
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
78
|
+
|
|
79
|
+
### Trademarks and Domain Names
|
|
80
|
+
|
|
81
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
82
|
+
the Software and Content, you have no right under these Terms and Conditions to use our
|
|
83
|
+
trademarks, trade names, service marks or product names.
|
|
84
|
+
|
|
85
|
+
The fully qualified name "https://github.com/parolkar" and the domain names associated with it are the exclusive property of Abhishek Parolkar. No right or license is granted to use these names or domains in any manner whatsoever without the express written permission of Abhishek Parolkar. Any unauthorized use of these names or domains is strictly prohibited.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software and Content under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software and Content available. On or after that date, you may use the
|
|
92
|
+
Software and Content under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
data/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# gemlings-browser
|
|
2
|
+
|
|
3
|
+
Browser automation for [Gemlings](https://github.com/parolkar/jrubyagents) agents, powered by Playwright.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Install the gem
|
|
9
|
+
gem install gemlings-browser
|
|
10
|
+
|
|
11
|
+
# 2. Install Playwright + Chromium
|
|
12
|
+
gemlings-browser install
|
|
13
|
+
|
|
14
|
+
# 3. Use with an agent
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "gemlings"
|
|
19
|
+
require "gemlings/browser"
|
|
20
|
+
|
|
21
|
+
agent = Gemlings::CodeAgent.new(
|
|
22
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
23
|
+
tools: [Gemlings::Browser::BrowseTool]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
agent.run("Go to example.com and get the page title")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
### `browser_action`
|
|
32
|
+
|
|
33
|
+
The primary tool. Executes Ruby code with browser access.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# The agent writes code like this:
|
|
37
|
+
page = browser.get_page("main")
|
|
38
|
+
page.goto("https://news.ycombinator.com")
|
|
39
|
+
titles = page.query_selector_all(".titleline a").map(&:text_content)
|
|
40
|
+
puts titles.first(5).join("\n")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Available in scripts:
|
|
44
|
+
- `browser.get_page("name")` — Named page (persists across calls)
|
|
45
|
+
- `browser.new_page` — Anonymous page
|
|
46
|
+
- `browser.list_pages` — List all tabs
|
|
47
|
+
- `browser.close_page("name")` — Close a tab
|
|
48
|
+
- `save_screenshot(page, "file.png")` — Save screenshot
|
|
49
|
+
- `write_file("name", data)` / `read_file("name")` — Sandboxed file I/O
|
|
50
|
+
|
|
51
|
+
### `browser_screenshot`
|
|
52
|
+
|
|
53
|
+
Takes a screenshot of a named page.
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
tools: [Gemlings::Browser::BrowseTool, Gemlings::Browser::ScreenshotTool]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `browser_list_pages`
|
|
60
|
+
|
|
61
|
+
Lists all open pages with names, URLs, and titles.
|
|
62
|
+
|
|
63
|
+
### `browser_close_page`
|
|
64
|
+
|
|
65
|
+
Closes a named page.
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
GemlingsBrowser.configure do |c|
|
|
71
|
+
c.headless = true # default: false
|
|
72
|
+
c.timeout = 60 # default: 30 seconds
|
|
73
|
+
c.browser_type = :chromium # default: :chromium
|
|
74
|
+
c.base_dir = "~/.my-dir" # default: ~/.gemlings-browser
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Connecting to Existing Chrome
|
|
79
|
+
|
|
80
|
+
Launch Chrome with remote debugging:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
google-chrome --remote-debugging-port=9222
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `BrowseTool` with auto-connect:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
tool = Gemlings::Browser::BrowseTool.new(connect: true)
|
|
90
|
+
agent = Gemlings::CodeAgent.new(
|
|
91
|
+
model: "anthropic/claude-sonnet-4-20250514",
|
|
92
|
+
tools: [tool]
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The gem auto-discovers Chrome via DevToolsActivePort files and port probing (9222–9229).
|
|
97
|
+
|
|
98
|
+
## Privacy
|
|
99
|
+
|
|
100
|
+
gemlings-browser makes **zero outbound network requests** on its own. All browser activity is local and under your control.
|
|
101
|
+
|
|
102
|
+
- Playwright telemetry is disabled (`PLAYWRIGHT_TELEMETRY_DISABLED=1`)
|
|
103
|
+
- No analytics, tracking, or phone-home code
|
|
104
|
+
- Temp files are sandboxed to `~/.gemlings-browser/tmp/`
|
|
105
|
+
- No cloud dependencies or API keys required
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
- Ruby 3.2+ or JRuby 10+
|
|
110
|
+
- Node.js (for Playwright driver)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/gemlings_browser/version"
|
|
5
|
+
|
|
6
|
+
case ARGV[0]
|
|
7
|
+
when "install"
|
|
8
|
+
require_relative "../lib/gemlings/browser"
|
|
9
|
+
GemlingsBrowser::Installer.run
|
|
10
|
+
when "status"
|
|
11
|
+
require_relative "../lib/gemlings/browser"
|
|
12
|
+
GemlingsBrowser::Installer.status
|
|
13
|
+
when "version", "-v", "--version"
|
|
14
|
+
puts "gemlings-browser #{GemlingsBrowser::VERSION}"
|
|
15
|
+
else
|
|
16
|
+
puts "gemlings-browser v#{GemlingsBrowser::VERSION}"
|
|
17
|
+
puts ""
|
|
18
|
+
puts "Usage:"
|
|
19
|
+
puts " gemlings-browser install Install Playwright + Chromium"
|
|
20
|
+
puts " gemlings-browser status Show installation status"
|
|
21
|
+
puts " gemlings-browser version Show gem version"
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Main entry point for gemlings-browser.
|
|
4
|
+
# Usage:
|
|
5
|
+
# require "gemlings/browser"
|
|
6
|
+
|
|
7
|
+
# Disable Playwright telemetry — no data leaves the machine.
|
|
8
|
+
ENV["PLAYWRIGHT_TELEMETRY_DISABLED"] = "1"
|
|
9
|
+
|
|
10
|
+
require "gemlings"
|
|
11
|
+
require_relative "../gemlings_browser/version"
|
|
12
|
+
require_relative "../gemlings_browser/configuration"
|
|
13
|
+
require_relative "../gemlings_browser/page_registry"
|
|
14
|
+
require_relative "../gemlings_browser/chrome_discovery"
|
|
15
|
+
require_relative "../gemlings_browser/temp_files"
|
|
16
|
+
require_relative "../gemlings_browser/browser_manager"
|
|
17
|
+
require_relative "../gemlings_browser/installer"
|
|
18
|
+
require_relative "../gemlings_browser/tools/browse_tool"
|
|
19
|
+
require_relative "../gemlings_browser/tools/screenshot_tool"
|
|
20
|
+
require_relative "../gemlings_browser/tools/list_pages_tool"
|
|
21
|
+
require_relative "../gemlings_browser/tools/close_page_tool"
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "playwright"
|
|
4
|
+
|
|
5
|
+
module GemlingsBrowser
|
|
6
|
+
class BrowserManager
|
|
7
|
+
# Holds a Playwright connection, browser, context, and its page registry.
|
|
8
|
+
BrowserEntry = Struct.new(:playwright, :browser, :context, :registry, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@browsers = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Launch or reuse a managed Chromium instance.
|
|
16
|
+
def ensure_browser(name, headless: nil, ignore_https_errors: false)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
entry = @browsers[name]
|
|
19
|
+
return entry if entry && browser_alive?(entry)
|
|
20
|
+
|
|
21
|
+
cleanup_entry(entry) if entry
|
|
22
|
+
@browsers[name] = launch_browser(headless: headless, ignore_https_errors: ignore_https_errors)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Connect to a running browser via CDP WebSocket endpoint.
|
|
27
|
+
def connect_browser(name, endpoint)
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
entry = @browsers[name]
|
|
30
|
+
return entry if entry && browser_alive?(entry)
|
|
31
|
+
|
|
32
|
+
cleanup_entry(entry) if entry
|
|
33
|
+
@browsers[name] = connect_cdp(endpoint)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Auto-discover a running Chrome and connect to it.
|
|
38
|
+
# Returns the BrowserEntry, or nil if nothing found.
|
|
39
|
+
def auto_connect(name)
|
|
40
|
+
endpoint = ChromeDiscovery.new.discover
|
|
41
|
+
return nil unless endpoint
|
|
42
|
+
|
|
43
|
+
connect_browser(name, endpoint)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get or create a named page for the given browser.
|
|
47
|
+
# The page persists across calls until explicitly closed.
|
|
48
|
+
def get_page(browser_name, page_name)
|
|
49
|
+
entry = fetch_entry!(browser_name)
|
|
50
|
+
page = entry.registry.get(page_name)
|
|
51
|
+
return page if page
|
|
52
|
+
|
|
53
|
+
page = entry.context.new_page
|
|
54
|
+
entry.registry.set(page_name, page)
|
|
55
|
+
page
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create an anonymous page (caller is responsible for closing it).
|
|
59
|
+
def new_page(browser_name)
|
|
60
|
+
entry = fetch_entry!(browser_name)
|
|
61
|
+
entry.context.new_page
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# List all tracked pages for a browser.
|
|
65
|
+
def list_pages(browser_name)
|
|
66
|
+
entry = fetch_entry!(browser_name)
|
|
67
|
+
entry.registry.all
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Close a named page.
|
|
71
|
+
def close_page(browser_name, page_name)
|
|
72
|
+
entry = fetch_entry!(browser_name)
|
|
73
|
+
entry.registry.remove(page_name)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Stop a specific browser and clean up its resources.
|
|
77
|
+
def stop_browser(name)
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
entry = @browsers.delete(name)
|
|
80
|
+
cleanup_entry(entry) if entry
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Shutdown all browsers and Playwright connections.
|
|
85
|
+
def stop_all
|
|
86
|
+
@mutex.synchronize do
|
|
87
|
+
@browsers.each_value { |entry| cleanup_entry(entry) }
|
|
88
|
+
@browsers.clear
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def fetch_entry!(browser_name)
|
|
95
|
+
entry = @mutex.synchronize { @browsers[browser_name] }
|
|
96
|
+
raise "No browser named '#{browser_name}'. Call ensure_browser or auto_connect first." unless entry
|
|
97
|
+
|
|
98
|
+
entry
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def launch_browser(headless:, ignore_https_errors:)
|
|
102
|
+
config = GemlingsBrowser.configuration
|
|
103
|
+
headless = config.headless if headless.nil?
|
|
104
|
+
|
|
105
|
+
playwright = Playwright.create(
|
|
106
|
+
playwright_cli_executable_path: config.playwright_cli_path
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
browser = playwright.playwright.chromium.launch(headless: headless)
|
|
110
|
+
context = browser.new_context(ignoreHTTPSErrors: ignore_https_errors)
|
|
111
|
+
|
|
112
|
+
BrowserEntry.new(
|
|
113
|
+
playwright: playwright,
|
|
114
|
+
browser: browser,
|
|
115
|
+
context: context,
|
|
116
|
+
registry: PageRegistry.new
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def connect_cdp(endpoint)
|
|
121
|
+
config = GemlingsBrowser.configuration
|
|
122
|
+
|
|
123
|
+
playwright = Playwright.create(
|
|
124
|
+
playwright_cli_executable_path: config.playwright_cli_path
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
browser = playwright.playwright.chromium.connect_over_cdp(endpoint)
|
|
128
|
+
context = browser.contexts.first || browser.new_context
|
|
129
|
+
|
|
130
|
+
BrowserEntry.new(
|
|
131
|
+
playwright: playwright,
|
|
132
|
+
browser: browser,
|
|
133
|
+
context: context,
|
|
134
|
+
registry: PageRegistry.new
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def browser_alive?(entry)
|
|
139
|
+
entry.browser.connected?
|
|
140
|
+
rescue StandardError
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def cleanup_entry(entry)
|
|
145
|
+
return unless entry
|
|
146
|
+
|
|
147
|
+
entry.registry.clear
|
|
148
|
+
entry.context&.close rescue nil
|
|
149
|
+
entry.browser&.close rescue nil
|
|
150
|
+
entry.playwright&.stop rescue nil
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module GemlingsBrowser
|
|
7
|
+
class ChromeDiscovery
|
|
8
|
+
DISCOVERY_PORTS = (9222..9229).to_a.freeze
|
|
9
|
+
PROBE_TIMEOUT = 0.75 # seconds
|
|
10
|
+
|
|
11
|
+
# Attempt to discover a running Chrome instance.
|
|
12
|
+
# Returns a CDP WebSocket URL string, or nil.
|
|
13
|
+
def discover
|
|
14
|
+
endpoint = read_devtools_active_port
|
|
15
|
+
return endpoint if endpoint
|
|
16
|
+
|
|
17
|
+
DISCOVERY_PORTS.each do |port|
|
|
18
|
+
endpoint = probe_port(port)
|
|
19
|
+
return endpoint if endpoint
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# --- DevToolsActivePort file parsing ---
|
|
28
|
+
|
|
29
|
+
def read_devtools_active_port
|
|
30
|
+
devtools_active_port_candidates.each do |path|
|
|
31
|
+
next unless File.exist?(path)
|
|
32
|
+
|
|
33
|
+
lines = File.read(path).strip.split("\n")
|
|
34
|
+
next if lines.size < 2
|
|
35
|
+
|
|
36
|
+
port = lines[0].strip.to_i
|
|
37
|
+
ws_path = lines[1].strip
|
|
38
|
+
next if port.zero? || ws_path.empty?
|
|
39
|
+
|
|
40
|
+
return "ws://127.0.0.1:#{port}#{ws_path}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
rescue StandardError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def devtools_active_port_candidates
|
|
49
|
+
home = Dir.home
|
|
50
|
+
case host_os
|
|
51
|
+
when /darwin/i
|
|
52
|
+
[
|
|
53
|
+
File.join(home, "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort"),
|
|
54
|
+
File.join(home, "Library", "Application Support", "Google", "Chrome Canary", "DevToolsActivePort"),
|
|
55
|
+
File.join(home, "Library", "Application Support", "Chromium", "DevToolsActivePort"),
|
|
56
|
+
File.join(home, "Library", "Application Support", "BraveSoftware", "Brave-Browser", "DevToolsActivePort")
|
|
57
|
+
]
|
|
58
|
+
when /linux/i
|
|
59
|
+
[
|
|
60
|
+
File.join(home, ".config", "google-chrome", "DevToolsActivePort"),
|
|
61
|
+
File.join(home, ".config", "google-chrome-unstable", "DevToolsActivePort"),
|
|
62
|
+
File.join(home, ".config", "chromium", "DevToolsActivePort"),
|
|
63
|
+
File.join(home, ".config", "BraveSoftware", "Brave-Browser", "DevToolsActivePort")
|
|
64
|
+
]
|
|
65
|
+
when /mingw|mswin|cygwin/i
|
|
66
|
+
local_app_data = ENV["LOCALAPPDATA"] || File.join(home, "AppData", "Local")
|
|
67
|
+
[
|
|
68
|
+
File.join(local_app_data, "Google", "Chrome", "User Data", "DevToolsActivePort"),
|
|
69
|
+
File.join(local_app_data, "Chromium", "User Data", "DevToolsActivePort"),
|
|
70
|
+
File.join(local_app_data, "BraveSoftware", "Brave-Browser", "User Data", "DevToolsActivePort")
|
|
71
|
+
]
|
|
72
|
+
else
|
|
73
|
+
[]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# --- Port probing ---
|
|
78
|
+
|
|
79
|
+
def probe_port(port)
|
|
80
|
+
uri = URI("http://127.0.0.1:#{port}/json/version")
|
|
81
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
82
|
+
http.open_timeout = PROBE_TIMEOUT
|
|
83
|
+
http.read_timeout = PROBE_TIMEOUT
|
|
84
|
+
|
|
85
|
+
response = http.get(uri.path)
|
|
86
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
87
|
+
|
|
88
|
+
data = JSON.parse(response.body)
|
|
89
|
+
data["webSocketDebuggerUrl"]
|
|
90
|
+
rescue StandardError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def host_os
|
|
95
|
+
RbConfig::CONFIG["host_os"]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemlingsBrowser
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :headless, :timeout, :base_dir, :browser_type, :playwright_cli_path
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@headless = false
|
|
9
|
+
@timeout = 30
|
|
10
|
+
@base_dir = File.join(Dir.home, ".gemlings-browser")
|
|
11
|
+
@browser_type = :chromium
|
|
12
|
+
@playwright_cli_path = detect_playwright_cli
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def detect_playwright_cli
|
|
18
|
+
candidates = %w[playwright]
|
|
19
|
+
candidates.each do |cmd|
|
|
20
|
+
return cmd if command_exists?(cmd)
|
|
21
|
+
end
|
|
22
|
+
"npx playwright"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def command_exists?(cmd)
|
|
26
|
+
exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
|
|
27
|
+
ENV["PATH"].to_s.split(File::PATH_SEPARATOR).each do |dir|
|
|
28
|
+
exts.each do |ext|
|
|
29
|
+
path = File.join(dir, "#{cmd}#{ext}")
|
|
30
|
+
return true if File.executable?(path) && !File.directory?(path)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
def configuration
|
|
39
|
+
@configuration ||= Configuration.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def configure
|
|
43
|
+
yield(configuration)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset_configuration!
|
|
47
|
+
@configuration = Configuration.new
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def manager
|
|
51
|
+
@manager ||= BrowserManager.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reset_manager!
|
|
55
|
+
@manager&.stop_all
|
|
56
|
+
@manager = nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemlingsBrowser
|
|
4
|
+
module Installer
|
|
5
|
+
class << self
|
|
6
|
+
# Install Playwright and Chromium browser.
|
|
7
|
+
def run
|
|
8
|
+
puts "Installing Playwright browsers..."
|
|
9
|
+
cli = GemlingsBrowser.configuration.playwright_cli_path
|
|
10
|
+
|
|
11
|
+
success = system("#{cli} install chromium")
|
|
12
|
+
if success
|
|
13
|
+
puts "Chromium installed successfully."
|
|
14
|
+
else
|
|
15
|
+
warn "Failed to install Chromium. Ensure Node.js is installed and 'npx' is available."
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Print installation status.
|
|
21
|
+
def status
|
|
22
|
+
cli = GemlingsBrowser.configuration.playwright_cli_path
|
|
23
|
+
puts "gemlings-browser v#{GemlingsBrowser::VERSION}"
|
|
24
|
+
puts "Playwright CLI: #{cli}"
|
|
25
|
+
puts ""
|
|
26
|
+
puts "Checking installed browsers..."
|
|
27
|
+
system("#{cli} install --dry-run chromium 2>&1") || puts("(Could not determine browser status)")
|
|
28
|
+
puts ""
|
|
29
|
+
puts "Base directory: #{GemlingsBrowser.configuration.base_dir}"
|
|
30
|
+
puts "Headless: #{GemlingsBrowser.configuration.headless}"
|
|
31
|
+
puts "Timeout: #{GemlingsBrowser.configuration.timeout}s"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemlingsBrowser
|
|
4
|
+
class PageRegistry
|
|
5
|
+
def initialize
|
|
6
|
+
@pages = {}
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Retrieve a named page, or nil if not found / closed.
|
|
11
|
+
def get(name)
|
|
12
|
+
@mutex.synchronize do
|
|
13
|
+
page = @pages[name]
|
|
14
|
+
return nil unless page
|
|
15
|
+
|
|
16
|
+
if page_closed?(page)
|
|
17
|
+
@pages.delete(name)
|
|
18
|
+
return nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
page
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Store a named page.
|
|
26
|
+
def set(name, page)
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
@pages[name] = page
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Return metadata for all live pages.
|
|
33
|
+
def all
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
prune_closed_internal
|
|
36
|
+
@pages.map do |name, page|
|
|
37
|
+
{
|
|
38
|
+
name: name,
|
|
39
|
+
url: safe_page_attr(page, :url),
|
|
40
|
+
title: safe_page_attr(page, :title)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Close and remove a named page. Returns true if found.
|
|
47
|
+
def remove(name)
|
|
48
|
+
@mutex.synchronize do
|
|
49
|
+
page = @pages.delete(name)
|
|
50
|
+
return false unless page
|
|
51
|
+
|
|
52
|
+
safe_close(page)
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Remove entries whose pages have been closed externally.
|
|
58
|
+
def prune_closed
|
|
59
|
+
@mutex.synchronize { prune_closed_internal }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Number of tracked pages.
|
|
63
|
+
def size
|
|
64
|
+
@mutex.synchronize { @pages.size }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Close all pages and clear the registry.
|
|
68
|
+
def clear
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@pages.each_value { |page| safe_close(page) }
|
|
71
|
+
@pages.clear
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def prune_closed_internal
|
|
78
|
+
@pages.delete_if { |_name, page| page_closed?(page) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def page_closed?(page)
|
|
82
|
+
page.closed?
|
|
83
|
+
rescue StandardError
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def safe_close(page)
|
|
88
|
+
page.close unless page_closed?(page)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
# Page may already be gone — ignore.
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def safe_page_attr(page, attr)
|
|
94
|
+
page.public_send(attr)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module GemlingsBrowser
|
|
6
|
+
module TempFiles
|
|
7
|
+
class << self
|
|
8
|
+
# Resolve a safe path within the sandboxed temp directory.
|
|
9
|
+
# Uses File.basename to prevent directory traversal attacks.
|
|
10
|
+
def path_for(name)
|
|
11
|
+
safe_name = File.basename(name.to_s)
|
|
12
|
+
raise ArgumentError, "Invalid filename" if safe_name.empty? || safe_name == "."
|
|
13
|
+
|
|
14
|
+
FileUtils.mkdir_p(temp_dir)
|
|
15
|
+
File.join(temp_dir, safe_name)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Write data to a sandboxed file. Returns the full path.
|
|
19
|
+
def write(name, data)
|
|
20
|
+
path = path_for(name)
|
|
21
|
+
File.binwrite(path, data)
|
|
22
|
+
path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Read a sandboxed file's contents.
|
|
26
|
+
def read(name)
|
|
27
|
+
path = path_for(name)
|
|
28
|
+
raise Errno::ENOENT, "File not found: #{name}" unless File.exist?(path)
|
|
29
|
+
|
|
30
|
+
File.binread(path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convenience: return a path suitable for a screenshot.
|
|
34
|
+
def screenshot_path(name = nil)
|
|
35
|
+
name ||= "screenshot_#{Time.now.to_i}.png"
|
|
36
|
+
name = "#{name}.png" unless name.end_with?(".png", ".jpg", ".jpeg", ".webp")
|
|
37
|
+
path_for(name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Remove all temp files.
|
|
41
|
+
def cleanup!
|
|
42
|
+
FileUtils.rm_rf(temp_dir)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def temp_dir
|
|
48
|
+
File.join(GemlingsBrowser.configuration.base_dir, "tmp")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stringio"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module Gemlings
|
|
7
|
+
module Browser
|
|
8
|
+
class BrowseTool < Gemlings::Tool
|
|
9
|
+
tool_name "browser_action"
|
|
10
|
+
description <<~DESC
|
|
11
|
+
Controls a browser using Playwright. Execute Ruby code with access to:
|
|
12
|
+
browser.get_page("name") - Get/create a named page (persists across calls)
|
|
13
|
+
browser.new_page - Create an anonymous page
|
|
14
|
+
browser.list_pages - List all tabs: [{name:, url:, title:}]
|
|
15
|
+
browser.close_page("name") - Close a named page
|
|
16
|
+
save_screenshot(page, "name.png") - Save screenshot, returns path
|
|
17
|
+
write_file("name", data) - Write to sandboxed temp dir, returns path
|
|
18
|
+
read_file("name") - Read from sandboxed temp dir
|
|
19
|
+
|
|
20
|
+
Pages are Playwright Page objects with full API:
|
|
21
|
+
page.goto(url), page.click(selector), page.fill(selector, value),
|
|
22
|
+
page.title, page.url, page.text_content(selector),
|
|
23
|
+
page.screenshot(path: "..."), page.evaluate(js),
|
|
24
|
+
page.query_selector_all(selector), page.wait_for_selector(selector), etc.
|
|
25
|
+
|
|
26
|
+
Named pages persist across calls — no need to re-navigate.
|
|
27
|
+
DESC
|
|
28
|
+
input :script, type: :string, description: "Ruby code to execute with browser access"
|
|
29
|
+
output_type :string
|
|
30
|
+
|
|
31
|
+
def initialize(browser_name: "default", headless: nil, connect: nil)
|
|
32
|
+
@browser_name = browser_name
|
|
33
|
+
@headless = headless
|
|
34
|
+
@connect = connect
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(script:)
|
|
38
|
+
manager = GemlingsBrowser.manager
|
|
39
|
+
|
|
40
|
+
if @connect
|
|
41
|
+
unless manager.auto_connect(@browser_name)
|
|
42
|
+
manager.ensure_browser(@browser_name, headless: @headless)
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
manager.ensure_browser(@browser_name, headless: @headless)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context = build_context(manager)
|
|
49
|
+
execute_script(context, script)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def execute_script(context, script)
|
|
55
|
+
output = StringIO.new
|
|
56
|
+
old_stdout = $stdout
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
$stdout = output
|
|
60
|
+
result = Timeout.timeout(GemlingsBrowser.configuration.timeout) do
|
|
61
|
+
context.instance_eval(script, "(browser-script)", 1)
|
|
62
|
+
end
|
|
63
|
+
$stdout = old_stdout
|
|
64
|
+
|
|
65
|
+
parts = []
|
|
66
|
+
parts << output.string unless output.string.empty?
|
|
67
|
+
parts << result.inspect unless result.nil?
|
|
68
|
+
parts.empty? ? "" : parts.join("\n")
|
|
69
|
+
rescue Timeout::Error
|
|
70
|
+
$stdout = old_stdout
|
|
71
|
+
"Error: Script timed out after #{GemlingsBrowser.configuration.timeout}s"
|
|
72
|
+
rescue => e
|
|
73
|
+
$stdout = old_stdout
|
|
74
|
+
"Error: #{e.class}: #{e.message}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_context(manager)
|
|
79
|
+
ctx = Object.new
|
|
80
|
+
browser_name = @browser_name
|
|
81
|
+
|
|
82
|
+
# Build the browser API object
|
|
83
|
+
browser_api = Object.new
|
|
84
|
+
browser_api.define_singleton_method(:get_page) do |name|
|
|
85
|
+
manager.get_page(browser_name, name)
|
|
86
|
+
end
|
|
87
|
+
browser_api.define_singleton_method(:new_page) do
|
|
88
|
+
manager.new_page(browser_name)
|
|
89
|
+
end
|
|
90
|
+
browser_api.define_singleton_method(:list_pages) do
|
|
91
|
+
manager.list_pages(browser_name)
|
|
92
|
+
end
|
|
93
|
+
browser_api.define_singleton_method(:close_page) do |name|
|
|
94
|
+
manager.close_page(browser_name, name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
ctx.define_singleton_method(:browser) { browser_api }
|
|
98
|
+
|
|
99
|
+
# File I/O helpers
|
|
100
|
+
ctx.define_singleton_method(:save_screenshot) do |page, name|
|
|
101
|
+
path = GemlingsBrowser::TempFiles.screenshot_path(name)
|
|
102
|
+
page.screenshot(path: path)
|
|
103
|
+
path
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
ctx.define_singleton_method(:write_file) do |name, data|
|
|
107
|
+
GemlingsBrowser::TempFiles.write(name, data)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
ctx.define_singleton_method(:read_file) do |name|
|
|
111
|
+
GemlingsBrowser::TempFiles.read(name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
ctx
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemlings
|
|
4
|
+
module Browser
|
|
5
|
+
class ClosePageTool < Gemlings::Tool
|
|
6
|
+
tool_name "browser_close_page"
|
|
7
|
+
description "Closes a named browser page."
|
|
8
|
+
input :page_name, type: :string, description: "Name of the page to close"
|
|
9
|
+
output_type :string
|
|
10
|
+
|
|
11
|
+
def call(page_name:)
|
|
12
|
+
manager = GemlingsBrowser.manager
|
|
13
|
+
if manager.close_page("default", page_name)
|
|
14
|
+
"Page '#{page_name}' closed."
|
|
15
|
+
else
|
|
16
|
+
"No page named '#{page_name}' found."
|
|
17
|
+
end
|
|
18
|
+
rescue => e
|
|
19
|
+
"Error: #{e.class}: #{e.message}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemlings
|
|
4
|
+
module Browser
|
|
5
|
+
class ListPagesTool < Gemlings::Tool
|
|
6
|
+
tool_name "browser_list_pages"
|
|
7
|
+
description "Lists all open browser pages/tabs with their names, URLs, and titles."
|
|
8
|
+
output_type :string
|
|
9
|
+
|
|
10
|
+
def call(**_kwargs)
|
|
11
|
+
manager = GemlingsBrowser.manager
|
|
12
|
+
pages = manager.list_pages("default")
|
|
13
|
+
|
|
14
|
+
if pages.empty?
|
|
15
|
+
"No open pages."
|
|
16
|
+
else
|
|
17
|
+
lines = pages.map do |info|
|
|
18
|
+
"#{info[:name]}: #{info[:url] || '(blank)'} - #{info[:title] || '(untitled)'}"
|
|
19
|
+
end
|
|
20
|
+
lines.join("\n")
|
|
21
|
+
end
|
|
22
|
+
rescue => e
|
|
23
|
+
"Error: #{e.class}: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemlings
|
|
4
|
+
module Browser
|
|
5
|
+
class ScreenshotTool < Gemlings::Tool
|
|
6
|
+
tool_name "browser_screenshot"
|
|
7
|
+
description "Takes a screenshot of a named browser page. Returns the file path."
|
|
8
|
+
input :page_name, type: :string, description: "Name of the page to screenshot"
|
|
9
|
+
input :filename, type: :string, description: "Output filename (e.g. 'debug.png')", required: false
|
|
10
|
+
output_type :string
|
|
11
|
+
|
|
12
|
+
def call(page_name:, filename: nil)
|
|
13
|
+
manager = GemlingsBrowser.manager
|
|
14
|
+
page = manager.get_page("default", page_name)
|
|
15
|
+
|
|
16
|
+
path = GemlingsBrowser::TempFiles.screenshot_path(filename || "#{page_name}.png")
|
|
17
|
+
page.screenshot(path: path)
|
|
18
|
+
path
|
|
19
|
+
rescue => e
|
|
20
|
+
"Error: #{e.class}: #{e.message}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gemlings-browser
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Abhishek Parolkar
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: gemlings
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 0.3.0
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 0.3.0
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: playwright-ruby-client
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.50'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.50'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
description: Gives Gemlings agents browser control via Playwright. Named persistent
|
|
69
|
+
pages, auto-connect to Chrome, screenshots, and sandboxed file I/O. Zero telemetry.
|
|
70
|
+
email:
|
|
71
|
+
- abhishek@parolkar.com
|
|
72
|
+
executables:
|
|
73
|
+
- gemlings-browser
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- exe/gemlings-browser
|
|
80
|
+
- lib/gemlings/browser.rb
|
|
81
|
+
- lib/gemlings_browser/browser_manager.rb
|
|
82
|
+
- lib/gemlings_browser/chrome_discovery.rb
|
|
83
|
+
- lib/gemlings_browser/configuration.rb
|
|
84
|
+
- lib/gemlings_browser/installer.rb
|
|
85
|
+
- lib/gemlings_browser/page_registry.rb
|
|
86
|
+
- lib/gemlings_browser/temp_files.rb
|
|
87
|
+
- lib/gemlings_browser/tools/browse_tool.rb
|
|
88
|
+
- lib/gemlings_browser/tools/close_page_tool.rb
|
|
89
|
+
- lib/gemlings_browser/tools/list_pages_tool.rb
|
|
90
|
+
- lib/gemlings_browser/tools/screenshot_tool.rb
|
|
91
|
+
- lib/gemlings_browser/version.rb
|
|
92
|
+
homepage: https://github.com/parolkar/gemlings-browser
|
|
93
|
+
licenses:
|
|
94
|
+
- nonstandard
|
|
95
|
+
metadata:
|
|
96
|
+
homepage_uri: https://github.com/parolkar/gemlings-browser
|
|
97
|
+
source_code_uri: https://github.com/parolkar/gemlings-browser
|
|
98
|
+
changelog_uri: https://github.com/parolkar/gemlings-browser/blob/main/CHANGELOG.md
|
|
99
|
+
rdoc_options: []
|
|
100
|
+
require_paths:
|
|
101
|
+
- lib
|
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: 3.2.0
|
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
requirements: []
|
|
113
|
+
rubygems_version: 3.7.2
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: Browser automation tool for Gemlings agents
|
|
116
|
+
test_files: []
|