b4n1-web 0.16.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/EULA.txt +79 -0
- data/LICENSE +95 -0
- data/README.md +66 -0
- data/bin/b4n1web +17 -0
- data/lib/b4n1web/agent_browser.rb +137 -0
- data/lib/b4n1web/binary.rb +73 -0
- data/lib/b4n1web/errors.rb +11 -0
- data/lib/b4n1web/page.rb +65 -0
- data/lib/b4n1web/version.rb +5 -0
- data/lib/b4n1web.rb +17 -0
- metadata +59 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1bca04f5554abef830a51a42c237da9da3eb184f3feecc24c6c9ab6e4873077e
|
|
4
|
+
data.tar.gz: 88e3f08e1ef4ef23984e86a7d0bdb71a8bc56377456705392b5e75cbabf1e850
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 37baa9b36f5ec03ff363ce45f5402ce9ad155856ca4b8e5ad78536ce2663c46e400e40b85c2c1b7efb6ba5e822db7459424378af2dcfb7d41fd2f719abfd1b19
|
|
7
|
+
data.tar.gz: 6c0fd5f2d3eae01dbbd274ce1623f3bdf100f185b06f794173d21fb43fe56d2c998b4361e9bce201d46e23038ab4eccca77749c41ad4b0aefbe11e84e8a04e76
|
data/EULA.txt
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
B4N1-WEB — END USER LICENSE AGREEMENT (BINARIES)
|
|
2
|
+
==================================================
|
|
3
|
+
|
|
4
|
+
Version: 1.0 | Effective: 2026-09-27 | Applies to: b4n1-web 0.13.0+
|
|
5
|
+
|
|
6
|
+
This End User License Agreement ("EULA") governs your use of the B4N1
|
|
7
|
+
BINARY ARTIFACTS of b4n1-web — the bundled per-platform `b4n1web`
|
|
8
|
+
executables (`b4n1web-linux-amd64`, `b4n1web-macos-arm64`,
|
|
9
|
+
`b4n1web-windows-amd64.exe` and the remaining platform builds shipped
|
|
10
|
+
under each SDK's `bin/` or `native/` directory), the packaged
|
|
11
|
+
per-language bindings (`*.jar`, `*.nupkg`, `*.whl`, npm tarball),
|
|
12
|
+
and the `b4n1web` standalone executable.
|
|
13
|
+
|
|
14
|
+
The SDK wrapper source files (the thin per-language bindings) are licensed
|
|
15
|
+
separately under the Business Source License 1.1 — see the `LICENSE` file
|
|
16
|
+
and each registry's license metadata.
|
|
17
|
+
|
|
18
|
+
1. DEFINITIONS
|
|
19
|
+
"Binaries" means the compiled artifacts described above.
|
|
20
|
+
"SDK" means the BSL 1.1 licensed wrapper source distributed for each language.
|
|
21
|
+
"Source" means the Rust implementation, build scripts, internal tests and
|
|
22
|
+
internal documentation of the B4N1 engine. Source is NOT distributed under
|
|
23
|
+
this EULA or any other license and remains proprietary.
|
|
24
|
+
|
|
25
|
+
2. GRANT OF LICENSE
|
|
26
|
+
Subject to your compliance with this EULA and payment of any applicable
|
|
27
|
+
subscription, B4N1 grants you a limited, non-exclusive, non-transferable,
|
|
28
|
+
revocable license to install and use the Binaries within your own products
|
|
29
|
+
and internal tooling, in object form only.
|
|
30
|
+
|
|
31
|
+
3. RESTRICTIONS (strictly enforced)
|
|
32
|
+
a. NO REDISTRIBUTION OF SOURCE. You may not publish, disclose, sublicense
|
|
33
|
+
or otherwise make the B4N1 Rust Source available, in whole or in part,
|
|
34
|
+
modified or unmodified, by any means, including in public repositories.
|
|
35
|
+
b. NO DECOMPILATION OR REVERSE ENGINEERING. You may not decompile,
|
|
36
|
+
disassemble, deobfuscate, or otherwise reverse engineer the Binaries
|
|
37
|
+
except to the extent such restriction is prohibited by applicable law
|
|
38
|
+
that cannot be waived.
|
|
39
|
+
c. NO REMOVAL OF NOTICES. You may not remove, alter, or obscure any
|
|
40
|
+
copyright, license, watermark, or proprietary notices in the Binaries.
|
|
41
|
+
d. NO RESALE AS A STANDALONE PRODUCT. The Binaries may not be resold,
|
|
42
|
+
sublicensed, or offered as a competing product.
|
|
43
|
+
e. COMPLIANCE AND AUDIT. You will cooperate with reasonable verification of
|
|
44
|
+
license status, including honoring embedded license validation.
|
|
45
|
+
|
|
46
|
+
4. SUBSCRIPTION TIERS
|
|
47
|
+
The Binaries operate under the applicable tier, validated offline against a
|
|
48
|
+
license file (`.b4n1lic`):
|
|
49
|
+
- Community: free, with attribution/watermark obligations.
|
|
50
|
+
- Pro / Corporate / Enterprise: paid tiers, no watermark, per the
|
|
51
|
+
commercial license purchased from B4N1.
|
|
52
|
+
License files are validated fully offline; no network calls are made at
|
|
53
|
+
runtime to verify entitlement.
|
|
54
|
+
|
|
55
|
+
5. TERMINATION
|
|
56
|
+
This EULA terminates automatically if you breach any term. Upon
|
|
57
|
+
termination you must cease all use and destroy the Binaries and copies.
|
|
58
|
+
|
|
59
|
+
6. DISCLAIMER OF WARRANTY
|
|
60
|
+
The Binaries are provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or
|
|
61
|
+
implied, including merchantability, fitness for a particular purpose, and
|
|
62
|
+
non-infringement. B4N1 does not warrant that the Binaries will be
|
|
63
|
+
uninterrupted or error-free.
|
|
64
|
+
|
|
65
|
+
7. LIMITATION OF LIABILITY
|
|
66
|
+
To the maximum extent permitted by law, B4N1 shall not be liable for any
|
|
67
|
+
indirect, incidental, special, consequential, or punitive damages, or for
|
|
68
|
+
any loss of profits, revenue, data, or goodwill, arising out of your use
|
|
69
|
+
of the Binaries, regardless of the legal theory, even if advised of the
|
|
70
|
+
possibility of such damages.
|
|
71
|
+
|
|
72
|
+
8. GOVERNING LAW AND SEVERABILITY
|
|
73
|
+
This EULA is governed by applicable law. If any provision is held
|
|
74
|
+
unenforceable, the remaining provisions stay in full force.
|
|
75
|
+
|
|
76
|
+
9. CONTACT
|
|
77
|
+
Licensing questions: licensing@b4n1.com | https://b4n1.com
|
|
78
|
+
|
|
79
|
+
B4N1 — Todos los derechos reservados. Fuente proprietary: privada y secreta.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
5
|
+
|
|
6
|
+
-----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
|
|
10
|
+
Licensor: B4N1 (b4n1.com)
|
|
11
|
+
Licensed Work: b4n1-web — Agentic browser engine for web automation,
|
|
12
|
+
testing, and AI-powered browsing tasks.
|
|
13
|
+
Additional Use Grant: You may use the Licensed Work in production, free of
|
|
14
|
+
charge, provided that your organization's total annual
|
|
15
|
+
gross revenue does not exceed USD $100,000. Organizations
|
|
16
|
+
whose annual gross revenue exceeds USD $100,000 must
|
|
17
|
+
obtain a commercial license from the Licensor before any
|
|
18
|
+
production use. You may not resell or provide the
|
|
19
|
+
Licensed Work as a managed commercial service without
|
|
20
|
+
authorization from the Licensor.
|
|
21
|
+
Change Date: Four years after the first publicly available
|
|
22
|
+
distribution of the Licensed Work under this License.
|
|
23
|
+
Change License: Apache License, Version 2.0
|
|
24
|
+
|
|
25
|
+
For information about alternative licensing arrangements for the Licensed
|
|
26
|
+
Work, please contact b4n1@b4n1.com.
|
|
27
|
+
|
|
28
|
+
-----------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
Terms
|
|
31
|
+
|
|
32
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
33
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
34
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
35
|
+
production use.
|
|
36
|
+
|
|
37
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
38
|
+
available distribution of a specific version of the Licensed Work under this
|
|
39
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
40
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
41
|
+
above terminate.
|
|
42
|
+
|
|
43
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
44
|
+
currently in effect as described in this License, you must purchase a
|
|
45
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
46
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
47
|
+
|
|
48
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
49
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
50
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
51
|
+
for each version of the Licensed Work released by Licensor.
|
|
52
|
+
|
|
53
|
+
You must conspicuously display this License on each original or modified copy
|
|
54
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
55
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
56
|
+
License apply to your use of that work.
|
|
57
|
+
|
|
58
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
59
|
+
terminate your rights under this License for the current and all other
|
|
60
|
+
versions of the Licensed Work.
|
|
61
|
+
|
|
62
|
+
This License does not grant you any right in any trademark or logo of
|
|
63
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
64
|
+
Licensor as expressly required by this License).
|
|
65
|
+
|
|
66
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
67
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
68
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
69
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
70
|
+
TITLE. MariaDB hereby grants you permission to use this License's text to
|
|
71
|
+
license your works, and to refer to it using the trademark "Business Source
|
|
72
|
+
License", as long as you comply with the Covenants of Licensor below.
|
|
73
|
+
|
|
74
|
+
Covenants of Licensor
|
|
75
|
+
|
|
76
|
+
In consideration of the right to use this License's text and the "Business
|
|
77
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
78
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
79
|
+
|
|
80
|
+
To specify as the Change License a license that is compatible with GPL
|
|
81
|
+
Version 2.0 or a later version, where "compatible" means that software
|
|
82
|
+
provided under the Change License can be included in a program with software
|
|
83
|
+
provided under GPL Version 2.0 or a later version. Licensor may specify
|
|
84
|
+
additional Change Licenses without limitation.
|
|
85
|
+
|
|
86
|
+
To either: (a) specify an additional grant of rights to use that does not
|
|
87
|
+
impose any additional restriction on the right granted in this License, as
|
|
88
|
+
the Additional Use Grant; or (b) insert the text "None" to specify a Change
|
|
89
|
+
License. Not to modify this License in any other way.
|
|
90
|
+
|
|
91
|
+
Notice
|
|
92
|
+
|
|
93
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
94
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
95
|
+
under an Open Source License, as stated in this License.
|
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# b4n1-web Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ultra-lightweight agentic browser for AI agents — Ruby gem wrapping the
|
|
4
|
+
`b4n1web` binary.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
gem install b4n1-web
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The gem resolves the browser binary in this order:
|
|
13
|
+
|
|
14
|
+
1. `B4N1WEB_BIN_PATH` (explicit override)
|
|
15
|
+
2. bundled binary inside the gem (when shipped)
|
|
16
|
+
3. well-known user/system locations (`~/.local/bin`, `~/.b4n1web/bin`, …)
|
|
17
|
+
4. `PATH`
|
|
18
|
+
|
|
19
|
+
If nothing resolves, it fails loudly (no silent fallback) with an actionable
|
|
20
|
+
install hint.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require "b4n1web"
|
|
26
|
+
|
|
27
|
+
browser = B4n1Web::AgentBrowser.new(mode: B4n1Web::BrowserMode::LIGHT)
|
|
28
|
+
page = browser.goto("https://example.com")
|
|
29
|
+
|
|
30
|
+
puts page.markdown # extracted markdown
|
|
31
|
+
puts page.links # array of links (JSON-parsed)
|
|
32
|
+
puts page.get_main_content # main content heuristic
|
|
33
|
+
puts page.find_links_by_text("docs")
|
|
34
|
+
browser.close # no-op (binary is stateless)
|
|
35
|
+
|
|
36
|
+
# Execute JavaScript in the page context
|
|
37
|
+
result = browser.evaluate("https://example.com", "document.title")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Modes
|
|
41
|
+
|
|
42
|
+
| Mode | Purpose |
|
|
43
|
+
|------|---------|
|
|
44
|
+
| `B4n1Web::BrowserMode::LIGHT` | Fast HTTP fetch, no JS (default) |
|
|
45
|
+
| `B4n1Web::BrowserMode::JS` | Headless Chromium with JavaScript |
|
|
46
|
+
| `B4n1Web::BrowserMode::RENDER` | Full render + wait-for selector |
|
|
47
|
+
|
|
48
|
+
### CLI wrapper
|
|
49
|
+
|
|
50
|
+
The gem installs a `b4n1web` executable that passes arguments straight to the
|
|
51
|
+
resolved binary:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
b4n1web goto https://example.com --mode light
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Options
|
|
58
|
+
|
|
59
|
+
- `mode:` — browser mode (default `light`)
|
|
60
|
+
- `timeout:` — seconds before killing the binary (default `30`)
|
|
61
|
+
- `binary:` — explicit binary path (skips auto-resolution)
|
|
62
|
+
- `runner:` — injectable process runner (tests only)
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
BUSL-1.1 (see `LICENSE`). Binary use is subject to `EULA.txt`.
|
data/bin/b4n1web
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# CLI wrapper: resolves the bundled/system b4n1web binary and passes all
|
|
5
|
+
# arguments through (e.g. `b4n1web goto https://example.com --mode light`).
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/b4n1web"
|
|
8
|
+
|
|
9
|
+
binary = B4n1Web::Binary.resolve
|
|
10
|
+
if binary.nil?
|
|
11
|
+
warn "ERROR: b4n1web binary not found."
|
|
12
|
+
warn " Install: curl -sL https://raw.githubusercontent.com/B4N1-com/b4n1-web/master/scripts/install.sh | bash"
|
|
13
|
+
warn " Or set B4N1WEB_BIN_PATH to an existing binary."
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
exec(binary, *ARGV)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
require_relative "binary"
|
|
6
|
+
require_relative "page"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "version"
|
|
9
|
+
|
|
10
|
+
module B4n1Web
|
|
11
|
+
# Browser modes accepted by the binary.
|
|
12
|
+
module BrowserMode
|
|
13
|
+
LIGHT = "light"
|
|
14
|
+
JS = "js"
|
|
15
|
+
RENDER = "render"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Thin async-free wrapper over the b4n1web binary.
|
|
19
|
+
#
|
|
20
|
+
# browser = B4n1Web::AgentBrowser.new(mode: B4n1Web::BrowserMode::LIGHT)
|
|
21
|
+
# page = browser.goto("https://example.com")
|
|
22
|
+
# puts page.markdown
|
|
23
|
+
# browser.close
|
|
24
|
+
class AgentBrowser
|
|
25
|
+
attr_reader :mode, :timeout
|
|
26
|
+
|
|
27
|
+
# runner: injectable process runner (object responding to
|
|
28
|
+
# run(argv, timeout:) -> [stdout, stderr, status_int]). Tests inject a
|
|
29
|
+
# fake; production uses the real Open3-based runner below.
|
|
30
|
+
# binary: explicit binary path override (skips auto-resolution).
|
|
31
|
+
def initialize(mode: BrowserMode::LIGHT, timeout: 30, runner: nil, binary: nil)
|
|
32
|
+
@mode = mode
|
|
33
|
+
@timeout = timeout
|
|
34
|
+
@runner = runner || RealRunner.new
|
|
35
|
+
@binary = binary || Binary.resolve!
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Binary path resolved at construction (parity with the other SDKs).
|
|
39
|
+
def binary_path
|
|
40
|
+
@binary
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Navigate + extract. wait_for: CSS selector (render mode).
|
|
44
|
+
def goto(url, wait_for: nil)
|
|
45
|
+
argv = [@binary, "goto", url, "--mode", @mode]
|
|
46
|
+
argv.push("--wait-for", wait_for) if wait_for
|
|
47
|
+
stdout, stderr, status = @runner.run(argv, timeout: @timeout)
|
|
48
|
+
raise NavigationError, "b4n1web timed out after #{@timeout}s" if status.nil?
|
|
49
|
+
unless status.zero?
|
|
50
|
+
raise NavigationError, "b4n1web exited #{status}: #{stderr.to_s.strip}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Page.parse(url, stdout)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Execute JavaScript in the page context, return raw result string.
|
|
57
|
+
def evaluate(url, js)
|
|
58
|
+
argv = [@binary, "evaluate", url, js]
|
|
59
|
+
stdout, stderr, status = @runner.run(argv, timeout: @timeout)
|
|
60
|
+
raise NavigationError, "b4n1web timed out after #{@timeout}s" if status.nil?
|
|
61
|
+
unless status.zero?
|
|
62
|
+
raise NavigationError, "b4n1web exited #{status}: #{stderr.to_s.strip}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
stdout.to_s.strip
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The binary is stateless: close is a no-op kept for API parity.
|
|
69
|
+
def close; end
|
|
70
|
+
|
|
71
|
+
# Default runner: Open3 with hard timeout. On timeout the whole process
|
|
72
|
+
# GROUP is killed (the binary spawns Chromium children that inherit
|
|
73
|
+
# stdout; killing only the leader would leave them holding the pipe and
|
|
74
|
+
# the readers below would block past the timeout) and status is nil
|
|
75
|
+
# (so callers raise NavigationError). The group is also released after a
|
|
76
|
+
# NORMAL exit so a lingering descendant can never hang the reads (parity
|
|
77
|
+
# with the Go SDK's cmd.WaitDelay).
|
|
78
|
+
class RealRunner
|
|
79
|
+
def run(argv, timeout:)
|
|
80
|
+
stdout = +""
|
|
81
|
+
stderr = +""
|
|
82
|
+
status = nil
|
|
83
|
+
Open3.popen3(*argv, pgroup: true) do |pin, pout, perr, wait_thr|
|
|
84
|
+
pid = wait_thr.pid
|
|
85
|
+
reader = Thread.new { pout.read }
|
|
86
|
+
err_reader = Thread.new { perr.read }
|
|
87
|
+
begin
|
|
88
|
+
Timeout.timeout(timeout) { status = wait_thr.value.exitstatus }
|
|
89
|
+
rescue Timeout::Error
|
|
90
|
+
status = nil
|
|
91
|
+
end
|
|
92
|
+
# Leader done (exit or timeout): kill the whole group so no
|
|
93
|
+
# descendant keeps stdout/stderr open and blocks the drains.
|
|
94
|
+
begin
|
|
95
|
+
Process.kill("KILL", -pid) # negative pid = whole process group
|
|
96
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
97
|
+
Process.kill("KILL", pid) rescue nil
|
|
98
|
+
end
|
|
99
|
+
await_group_gone(pid)
|
|
100
|
+
stdout = drain(reader)
|
|
101
|
+
stderr = drain(err_reader)
|
|
102
|
+
pin.close rescue nil
|
|
103
|
+
end
|
|
104
|
+
[stdout, stderr, status]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Join with a hard bound: after the group kill the pipes hit EOF
|
|
110
|
+
# immediately; a pathological holder cannot hang the SDK.
|
|
111
|
+
def drain(thread, seconds: 2)
|
|
112
|
+
return thread.value if thread.join(seconds)
|
|
113
|
+
thread.kill
|
|
114
|
+
+""
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Signal delivery is asynchronous: after the KILL above the group is
|
|
118
|
+
# signalled, not necessarily dead yet. Wait (bounded) until the group
|
|
119
|
+
# is actually gone so callers get a deterministic "dead on return"
|
|
120
|
+
# contract (parity with Go's cmd.WaitDelay). A killed orphan that is
|
|
121
|
+
# still awaiting reaping answers kill(0, ...) — the bound keeps this
|
|
122
|
+
# honest under any load.
|
|
123
|
+
def await_group_gone(pgid, seconds: 2)
|
|
124
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
125
|
+
loop do
|
|
126
|
+
begin
|
|
127
|
+
Process.kill(0, -pgid)
|
|
128
|
+
rescue Errno::ESRCH
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
return if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
132
|
+
sleep 0.01
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module B4n1Web
|
|
7
|
+
# Resolves the b4n1web binary. Order mirrors the Python/JS/C#/Java SDKs:
|
|
8
|
+
# 1. B4N1WEB_BIN_PATH (explicit override, must exist + be executable)
|
|
9
|
+
# 2. bundled binary shipped inside the gem (lib/b4n1web/bin/*)
|
|
10
|
+
# well-known user/system locations
|
|
11
|
+
# 4. PATH scan
|
|
12
|
+
module Binary
|
|
13
|
+
PLATFORM_BINARIES = {
|
|
14
|
+
["linux", "x86_64"] => "b4n1web-linux-amd64",
|
|
15
|
+
["linux", "aarch64"] => "b4n1web-linux-arm64",
|
|
16
|
+
["linux", "i686"] => "b4n1web-linux-ia32",
|
|
17
|
+
["darwin", "x86_64"] => "b4n1web-macos-x64",
|
|
18
|
+
["darwin", "arm64"] => "b4n1web-macos-arm64",
|
|
19
|
+
["mingw", "x64"] => "b4n1web-windows-amd64.exe",
|
|
20
|
+
["mswin", "x64"] => "b4n1web-windows-amd64.exe"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def platform_binary_name
|
|
26
|
+
os = RbConfig::CONFIG["host_os"]
|
|
27
|
+
arch = RbConfig::CONFIG["host_cpu"]
|
|
28
|
+
arch = "x86_64" if arch == "amd64"
|
|
29
|
+
arch = "i686" if arch == "i386"
|
|
30
|
+
plat = case os
|
|
31
|
+
when /linux/ then "linux"
|
|
32
|
+
when /darwin|mac os/ then "darwin"
|
|
33
|
+
when /mingw|cygwin/ then "mingw"
|
|
34
|
+
when /mswin|windows/ then "mswin"
|
|
35
|
+
end
|
|
36
|
+
return nil unless plat
|
|
37
|
+
PLATFORM_BINARIES[[plat, arch]]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the absolute path of a runnable b4n1web binary or nil.
|
|
41
|
+
def resolve
|
|
42
|
+
env = ENV["B4N1WEB_BIN_PATH"]
|
|
43
|
+
return env if env && File.file?(env) && File.executable?(env)
|
|
44
|
+
|
|
45
|
+
name = platform_binary_name
|
|
46
|
+
if name
|
|
47
|
+
bundled = File.expand_path(File.join(__dir__, "bin", name))
|
|
48
|
+
return bundled if File.file?(bundled) && File.executable?(bundled)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
candidates = []
|
|
52
|
+
candidates << ENV["B4N1WEB_BINARY"] if ENV["B4N1WEB_BINARY"]
|
|
53
|
+
home = Dir.home
|
|
54
|
+
candidates << File.join(home, ".local", "bin", "b4n1web")
|
|
55
|
+
candidates << File.join(home, ".b4n1web", "bin", "b4n1web")
|
|
56
|
+
candidates << "/usr/local/bin/b4n1web"
|
|
57
|
+
candidates << "/usr/bin/b4n1web"
|
|
58
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
59
|
+
candidates << File.join(dir, "b4n1web")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
candidates.compact.find { |p| File.file?(p) && File.executable?(p) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Same as resolve but raises loudly (no silent nil).
|
|
66
|
+
def resolve!
|
|
67
|
+
resolve || raise(BinaryNotFoundError,
|
|
68
|
+
"b4n1web binary not found. Install it (curl -sL " \
|
|
69
|
+
"https://raw.githubusercontent.com/B4N1-com/b4n1-web/" \
|
|
70
|
+
"master/scripts/install.sh | bash) or set B4N1WEB_BIN_PATH.")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module B4n1Web
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised when no b4n1web binary can be resolved (bundled/system/PATH).
|
|
7
|
+
class BinaryNotFoundError < Error; end
|
|
8
|
+
|
|
9
|
+
# Raised when the binary exits non-zero or times out.
|
|
10
|
+
class NavigationError < Error; end
|
|
11
|
+
end
|
data/lib/b4n1web/page.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module B4n1Web
|
|
6
|
+
# Structured page data returned by the b4n1web binary.
|
|
7
|
+
class Page
|
|
8
|
+
attr_reader :url, :markdown, :links, :screenshot
|
|
9
|
+
|
|
10
|
+
def initialize(url:, markdown:, links:, screenshot: nil)
|
|
11
|
+
@url = url
|
|
12
|
+
@markdown = markdown
|
|
13
|
+
@links = links
|
|
14
|
+
@screenshot = screenshot
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Parses the binary's text protocol:
|
|
18
|
+
# URL: <url>
|
|
19
|
+
# Markdown:
|
|
20
|
+
# <free-form markdown lines...>
|
|
21
|
+
# Links: ["https://...", ...]
|
|
22
|
+
# Screenshot: <path> (optional)
|
|
23
|
+
def self.parse(url, output)
|
|
24
|
+
markdown = +""
|
|
25
|
+
links = []
|
|
26
|
+
screenshot = nil
|
|
27
|
+
in_markdown = false
|
|
28
|
+
|
|
29
|
+
output.to_s.each_line do |line|
|
|
30
|
+
line = line.chomp
|
|
31
|
+
if line.start_with?("URL:")
|
|
32
|
+
next
|
|
33
|
+
elsif line.start_with?("Markdown:")
|
|
34
|
+
in_markdown = true
|
|
35
|
+
rest = line.delete_prefix("Markdown:").strip
|
|
36
|
+
markdown << "#{rest}\n" unless rest.empty?
|
|
37
|
+
elsif line.start_with?("Links:")
|
|
38
|
+
in_markdown = false
|
|
39
|
+
links = JSON.parse(line.delete_prefix("Links:").strip) || []
|
|
40
|
+
elsif line.start_with?("Screenshot:")
|
|
41
|
+
in_markdown = false
|
|
42
|
+
shot = line.delete_prefix("Screenshot:").strip
|
|
43
|
+
screenshot = shot.empty? ? nil : shot
|
|
44
|
+
else
|
|
45
|
+
markdown << "#{line}\n" if in_markdown
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
new(url: url, markdown: markdown.strip, links: links, screenshot: screenshot)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Main content heuristics: drop the first two lines (title + empty line).
|
|
53
|
+
def get_main_content
|
|
54
|
+
lines = markdown.split("\n")
|
|
55
|
+
content = lines.length > 2 ? lines[2..] : lines
|
|
56
|
+
content.join("\n").strip
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_links_by_text(text)
|
|
60
|
+
return [] if text.nil? || text.empty?
|
|
61
|
+
needle = text.downcase
|
|
62
|
+
links.select { |link| link.downcase.include?(needle) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/b4n1web.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "b4n1web/version"
|
|
4
|
+
require_relative "b4n1web/errors"
|
|
5
|
+
require_relative "b4n1web/binary"
|
|
6
|
+
require_relative "b4n1web/page"
|
|
7
|
+
require_relative "b4n1web/agent_browser"
|
|
8
|
+
|
|
9
|
+
# B4n1Web Ruby SDK — ultra-lightweight agentic browser for AI agents.
|
|
10
|
+
#
|
|
11
|
+
# require "b4n1web"
|
|
12
|
+
# browser = B4n1Web::AgentBrowser.new
|
|
13
|
+
# page = browser.goto("https://example.com")
|
|
14
|
+
# puts page.markdown
|
|
15
|
+
# browser.close
|
|
16
|
+
module B4n1Web
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: b4n1-web
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.16.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Bani Montoya
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ultra-lightweight headless browser with bundled binary for AI agents.
|
|
14
|
+
Supports Light/JS/Render modes.
|
|
15
|
+
email:
|
|
16
|
+
- b4n1@b4n1.com
|
|
17
|
+
executables:
|
|
18
|
+
- b4n1web
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- EULA.txt
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- bin/b4n1web
|
|
26
|
+
- lib/b4n1web.rb
|
|
27
|
+
- lib/b4n1web/agent_browser.rb
|
|
28
|
+
- lib/b4n1web/binary.rb
|
|
29
|
+
- lib/b4n1web/errors.rb
|
|
30
|
+
- lib/b4n1web/page.rb
|
|
31
|
+
- lib/b4n1web/version.rb
|
|
32
|
+
homepage: https://b4n1.com
|
|
33
|
+
licenses:
|
|
34
|
+
- BUSL-1.1
|
|
35
|
+
metadata:
|
|
36
|
+
homepage_uri: https://b4n1.com
|
|
37
|
+
source_code_uri: https://github.com/B4N1-com/b4n1-web
|
|
38
|
+
changelog_uri: https://github.com/B4N1-com/b4n1-web/blob/master/CHANGELOG.md
|
|
39
|
+
bug_tracker_uri: https://github.com/B4N1-com/b4n1-web/issues
|
|
40
|
+
post_install_message:
|
|
41
|
+
rdoc_options: []
|
|
42
|
+
require_paths:
|
|
43
|
+
- lib
|
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '3.0'
|
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
requirements: []
|
|
55
|
+
rubygems_version: 3.3.5
|
|
56
|
+
signing_key:
|
|
57
|
+
specification_version: 4
|
|
58
|
+
summary: B4n1Web - Agentic Browser Engine for AI agents
|
|
59
|
+
test_files: []
|