browserctl 0.11.0 → 0.12.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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +1 -0
- data/bin/browserctl +143 -94
- data/bin/browserd +8 -1
- data/lib/browserctl/client.rb +3 -3
- data/lib/browserctl/commands/cli_output.rb +21 -1
- data/lib/browserctl/commands/migrate.rb +94 -0
- data/lib/browserctl/commands/trace.rb +187 -0
- data/lib/browserctl/constants.rb +3 -1
- data/lib/browserctl/crash_report.rb +96 -0
- data/lib/browserctl/error/codes.rb +44 -0
- data/lib/browserctl/error/exit_codes.rb +54 -0
- data/lib/browserctl/error/suggested_actions.rb +41 -0
- data/lib/browserctl/errors.rb +44 -14
- data/lib/browserctl/format_version.rb +37 -0
- data/lib/browserctl/logger.rb +102 -9
- data/lib/browserctl/migrations.rb +216 -0
- data/lib/browserctl/recording.rb +34 -2
- data/lib/browserctl/redactor.rb +58 -0
- data/lib/browserctl/rubocop/cops/typed_error.rb +69 -0
- data/lib/browserctl/runner.rb +12 -6
- data/lib/browserctl/secret_resolver_registry.rb +23 -4
- data/lib/browserctl/server/command_dispatcher.rb +3 -0
- data/lib/browserctl/server/handlers/daemon_control.rb +5 -1
- data/lib/browserctl/server/handlers/error_payload.rb +27 -0
- data/lib/browserctl/server/handlers/interaction.rb +21 -3
- data/lib/browserctl/server/handlers/navigation.rb +19 -3
- data/lib/browserctl/session.rb +1 -1
- data/lib/browserctl/state/bundle.rb +43 -2
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow/flow_wrapper.rb +1 -1
- data/lib/browserctl/workflow.rb +56 -1
- metadata +15 -7
|
@@ -21,7 +21,11 @@ module Browserctl
|
|
|
21
21
|
def cmd_fetch(req)
|
|
22
22
|
key = req[:key].to_s
|
|
23
23
|
found = @kv_mutex.synchronize { @kv_store.key?(key) ? { ok: true, value: @kv_store[key] } : nil }
|
|
24
|
-
found ||
|
|
24
|
+
found || error_payload(
|
|
25
|
+
code: Browserctl::Error::Codes::KEY_NOT_FOUND,
|
|
26
|
+
message: "key '#{key}' not found",
|
|
27
|
+
context: { key: key }
|
|
28
|
+
)
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
31
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../errors"
|
|
4
|
+
|
|
5
|
+
module Browserctl
|
|
6
|
+
class CommandDispatcher
|
|
7
|
+
module Handlers
|
|
8
|
+
# Centralised structured-error builder for daemon JSON-RPC responses.
|
|
9
|
+
# Each handler returns `{ error:, code:, context:, suggested_action: }`
|
|
10
|
+
# for any failure carrying a stable {Browserctl::Error::Codes} code.
|
|
11
|
+
module ErrorPayload
|
|
12
|
+
# @param code [String] a SCREAMING_SNAKE code from {Codes}
|
|
13
|
+
# @param message [String] human-readable error
|
|
14
|
+
# @param context [Hash] free-form structured fields (selector, path, ...)
|
|
15
|
+
# @return [Hash{Symbol => Object}]
|
|
16
|
+
def error_payload(code:, message:, context: {})
|
|
17
|
+
{
|
|
18
|
+
error: message,
|
|
19
|
+
code: code,
|
|
20
|
+
context: context,
|
|
21
|
+
suggested_action: Browserctl::Error::SuggestedActions.for(code)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -27,7 +27,13 @@ module Browserctl
|
|
|
27
27
|
"return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; " \
|
|
28
28
|
"})(#{sel.to_json})"
|
|
29
29
|
)
|
|
30
|
-
|
|
30
|
+
unless coords
|
|
31
|
+
return error_payload(
|
|
32
|
+
code: Browserctl::Error::Codes::SELECTOR_NOT_FOUND,
|
|
33
|
+
message: "selector not found: #{sel}",
|
|
34
|
+
context: { selector: sel }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
31
37
|
|
|
32
38
|
session.page.mouse.move(x: coords["x"], y: coords["y"])
|
|
33
39
|
{ ok: true }
|
|
@@ -43,7 +49,13 @@ module Browserctl
|
|
|
43
49
|
return sel if sel.is_a?(Hash)
|
|
44
50
|
|
|
45
51
|
el = session.page.at_css(sel)
|
|
46
|
-
|
|
52
|
+
unless el
|
|
53
|
+
return error_payload(
|
|
54
|
+
code: Browserctl::Error::Codes::SELECTOR_NOT_FOUND,
|
|
55
|
+
message: "selector not found: #{sel}",
|
|
56
|
+
context: { selector: sel }
|
|
57
|
+
)
|
|
58
|
+
end
|
|
47
59
|
|
|
48
60
|
el.select_file(path)
|
|
49
61
|
{ ok: true }
|
|
@@ -56,7 +68,13 @@ module Browserctl
|
|
|
56
68
|
return sel if sel.is_a?(Hash)
|
|
57
69
|
|
|
58
70
|
el = session.page.at_css(sel)
|
|
59
|
-
|
|
71
|
+
unless el
|
|
72
|
+
return error_payload(
|
|
73
|
+
code: Browserctl::Error::Codes::SELECTOR_NOT_FOUND,
|
|
74
|
+
message: "selector not found: #{sel}",
|
|
75
|
+
context: { selector: sel }
|
|
76
|
+
)
|
|
77
|
+
end
|
|
60
78
|
|
|
61
79
|
el.evaluate(
|
|
62
80
|
"this.value = #{req[:value].to_json}; " \
|
|
@@ -8,7 +8,11 @@ module Browserctl
|
|
|
8
8
|
|
|
9
9
|
def cmd_navigate(req)
|
|
10
10
|
unless Policy.allowed_navigation?(req[:url].to_s)
|
|
11
|
-
return
|
|
11
|
+
return error_payload(
|
|
12
|
+
code: Browserctl::Error::Codes::DOMAIN_NOT_ALLOWED,
|
|
13
|
+
message: "navigation to '#{req[:url]}' blocked by domain policy",
|
|
14
|
+
context: { url: req[:url].to_s }
|
|
15
|
+
)
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
with_page(req[:name]) do |session|
|
|
@@ -81,7 +85,13 @@ module Browserctl
|
|
|
81
85
|
|
|
82
86
|
def type_into(page, selector, value)
|
|
83
87
|
el = page.at_css(selector)
|
|
84
|
-
|
|
88
|
+
unless el
|
|
89
|
+
return error_payload(
|
|
90
|
+
code: Browserctl::Error::Codes::SELECTOR_NOT_FOUND,
|
|
91
|
+
message: "selector not found: #{selector}",
|
|
92
|
+
context: { selector: selector }
|
|
93
|
+
)
|
|
94
|
+
end
|
|
85
95
|
|
|
86
96
|
el.focus
|
|
87
97
|
el.evaluate("this.select()")
|
|
@@ -91,7 +101,13 @@ module Browserctl
|
|
|
91
101
|
|
|
92
102
|
def click_element(page, selector)
|
|
93
103
|
el = page.at_css(selector)
|
|
94
|
-
|
|
104
|
+
unless el
|
|
105
|
+
return error_payload(
|
|
106
|
+
code: Browserctl::Error::Codes::SELECTOR_NOT_FOUND,
|
|
107
|
+
message: "selector not found: #{selector}",
|
|
108
|
+
context: { selector: selector }
|
|
109
|
+
)
|
|
110
|
+
end
|
|
95
111
|
|
|
96
112
|
# Use the DOM native click() so JS-only event listeners fire.
|
|
97
113
|
# CDP mouse simulation (el.click) dispatches events at screen coordinates
|
data/lib/browserctl/session.rb
CHANGED
|
@@ -71,7 +71,7 @@ module Browserctl
|
|
|
71
71
|
def self.load(session_name)
|
|
72
72
|
validate_name!(session_name)
|
|
73
73
|
dir = path(session_name)
|
|
74
|
-
raise "session '#{session_name}' not found" unless Dir.exist?(dir)
|
|
74
|
+
raise Browserctl::Error, "session '#{session_name}' not found" unless Dir.exist?(dir)
|
|
75
75
|
|
|
76
76
|
meta = JSON.parse(File.read(File.join(dir, "metadata.json")), symbolize_names: true)
|
|
77
77
|
|
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "openssl"
|
|
5
5
|
require "securerandom"
|
|
6
6
|
require_relative "../errors"
|
|
7
|
+
require_relative "../error/codes"
|
|
7
8
|
|
|
8
9
|
module Browserctl
|
|
9
10
|
module State
|
|
@@ -42,6 +43,12 @@ module Browserctl
|
|
|
42
43
|
class Bundle
|
|
43
44
|
MAGIC = "BCTL\x00".b.freeze
|
|
44
45
|
VERSION = 1
|
|
46
|
+
# Manifest-level format version, written as `format_version` and
|
|
47
|
+
# validated on decode. Distinct from the wire-format byte `VERSION`
|
|
48
|
+
# above (which gates the binary envelope shape) — this gates the
|
|
49
|
+
# manifest schema. See docs/reference/format-versions.md.
|
|
50
|
+
BUNDLE_FORMAT_VERSION = 1
|
|
51
|
+
SUPPORTED_FORMAT_VERSIONS = [BUNDLE_FORMAT_VERSION].freeze
|
|
45
52
|
FLAG_ENCRYPTED = 0x01
|
|
46
53
|
HEADER_SIZE = MAGIC.bytesize + 3 # version + flags + reserved
|
|
47
54
|
LEN_SIZE = 4
|
|
@@ -63,6 +70,7 @@ module Browserctl
|
|
|
63
70
|
# the footer is an HMAC. When nil, payload is plaintext and the
|
|
64
71
|
# footer is a SHA-256 digest.
|
|
65
72
|
def self.encode(manifest:, payload:, passphrase: nil)
|
|
73
|
+
manifest = stamp_format_version(manifest)
|
|
66
74
|
manifest_bytes = JSON.generate(manifest).b
|
|
67
75
|
payload_json = JSON.generate(payload).b
|
|
68
76
|
flags = 0
|
|
@@ -96,7 +104,8 @@ module Browserctl
|
|
|
96
104
|
verify_footer!(body, footer, encrypted: encrypted, passphrase: passphrase)
|
|
97
105
|
|
|
98
106
|
manifest = JSON.parse(manifest_bytes, symbolize_names: true)
|
|
99
|
-
|
|
107
|
+
verify_format_version!(manifest)
|
|
108
|
+
payload = decode_payload(payload_bytes, encrypted: encrypted, passphrase: passphrase)
|
|
100
109
|
|
|
101
110
|
{ manifest: manifest, payload: payload, magic: magic, version: version, encrypted: encrypted }
|
|
102
111
|
end
|
|
@@ -108,8 +117,40 @@ module Browserctl
|
|
|
108
117
|
raise BundleError, "unsupported bundle version #{version}" unless version == VERSION
|
|
109
118
|
|
|
110
119
|
manifest_bytes, = read_sections!(blob)
|
|
111
|
-
JSON.parse(manifest_bytes, symbolize_names: true)
|
|
120
|
+
manifest = JSON.parse(manifest_bytes, symbolize_names: true)
|
|
121
|
+
verify_format_version!(manifest)
|
|
122
|
+
manifest
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns the manifest with `format_version` set as the first key. When
|
|
126
|
+
# the caller already provided a value we keep it (so encoders can stamp
|
|
127
|
+
# a future version explicitly); otherwise we stamp the current one.
|
|
128
|
+
def self.stamp_format_version(manifest)
|
|
129
|
+
existing = manifest[:format_version] || manifest["format_version"]
|
|
130
|
+
version = existing || BUNDLE_FORMAT_VERSION
|
|
131
|
+
rest = manifest.except(:format_version, "format_version")
|
|
132
|
+
{ format_version: version }.merge(rest)
|
|
133
|
+
end
|
|
134
|
+
private_class_method :stamp_format_version
|
|
135
|
+
|
|
136
|
+
# Raises Browserctl::ProtocolMismatch when the manifest declares no
|
|
137
|
+
# format_version or one this build does not support. The error carries
|
|
138
|
+
# the canonical PROTOCOL_MISMATCH code from the v0.12 error taxonomy.
|
|
139
|
+
def self.verify_format_version!(manifest, path: nil)
|
|
140
|
+
version = manifest[:format_version] || manifest["format_version"]
|
|
141
|
+
return if version && SUPPORTED_FORMAT_VERSIONS.include?(version)
|
|
142
|
+
|
|
143
|
+
where = path ? " at #{path}" : ""
|
|
144
|
+
msg = if version.nil?
|
|
145
|
+
"bundle manifest#{where} is missing format_version " \
|
|
146
|
+
"(supported: #{SUPPORTED_FORMAT_VERSIONS.inspect})"
|
|
147
|
+
else
|
|
148
|
+
"bundle manifest#{where} declares format_version=#{version.inspect}, " \
|
|
149
|
+
"this build supports #{SUPPORTED_FORMAT_VERSIONS.inspect}"
|
|
150
|
+
end
|
|
151
|
+
raise Browserctl::ProtocolMismatch.new(msg, code: Browserctl::Error::Codes::PROTOCOL_MISMATCH)
|
|
112
152
|
end
|
|
153
|
+
private_class_method :verify_format_version!
|
|
113
154
|
|
|
114
155
|
def self.build_body(flags, manifest_bytes, payload_bytes)
|
|
115
156
|
header = MAGIC + [VERSION, flags, 0].pack("CCC")
|
data/lib/browserctl/version.rb
CHANGED
|
@@ -59,7 +59,7 @@ module Browserctl
|
|
|
59
59
|
def write(defn, overwrite: true, dir: nil)
|
|
60
60
|
path = dir ? File.join(dir, "#{defn.name}.rb") : target_path(defn.name)
|
|
61
61
|
if File.exist?(path) && !overwrite
|
|
62
|
-
raise "flow wrapper already exists at #{path} (pass overwrite: true to replace)"
|
|
62
|
+
raise Browserctl::WorkflowError, "flow wrapper already exists at #{path} (pass overwrite: true to replace)"
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
FileUtils.mkdir_p(File.dirname(path))
|
data/lib/browserctl/workflow.rb
CHANGED
|
@@ -11,6 +11,61 @@ require_relative "secret_resolvers"
|
|
|
11
11
|
require_relative "session"
|
|
12
12
|
|
|
13
13
|
module Browserctl
|
|
14
|
+
# Workflow-file format version. Workflows are Ruby files; the schema gate
|
|
15
|
+
# is a top-of-file comment header:
|
|
16
|
+
#
|
|
17
|
+
# # format_version: 1
|
|
18
|
+
#
|
|
19
|
+
# Unlike bundles and recordings, an unsupported or missing version on a
|
|
20
|
+
# workflow file is a *warning*, not a hard failure. Workflows are
|
|
21
|
+
# human-authored Ruby — the loader prefers to surface drift via stderr
|
|
22
|
+
# and let the file run, rather than block execution. See
|
|
23
|
+
# docs/reference/format-versions.md.
|
|
24
|
+
WORKFLOW_FORMAT_VERSION = 1
|
|
25
|
+
SUPPORTED_WORKFLOW_FORMAT_VERSIONS = [WORKFLOW_FORMAT_VERSION].freeze
|
|
26
|
+
|
|
27
|
+
# Matches a leading-line comment of the form `# format_version: <int>`.
|
|
28
|
+
# Tolerates leading whitespace inside the comment body and ignores the
|
|
29
|
+
# `# frozen_string_literal: true` magic comment that conventionally
|
|
30
|
+
# precedes it.
|
|
31
|
+
WORKFLOW_FORMAT_VERSION_HEADER = /^\s*#\s*format_version:\s*(\d+)\s*$/
|
|
32
|
+
|
|
33
|
+
# Parses the `# format_version: N` header from a workflow file's source.
|
|
34
|
+
# Scans only the contiguous leading comment block (and blank lines) so
|
|
35
|
+
# the header cannot be smuggled in mid-file. Returns the integer if
|
|
36
|
+
# present, or nil if the file has no version header.
|
|
37
|
+
def self.parse_workflow_format_version(source)
|
|
38
|
+
source.each_line do |line|
|
|
39
|
+
stripped = line.strip
|
|
40
|
+
next if stripped.empty?
|
|
41
|
+
break unless stripped.start_with?("#")
|
|
42
|
+
|
|
43
|
+
if (m = line.match(WORKFLOW_FORMAT_VERSION_HEADER))
|
|
44
|
+
return Integer(m[1])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Reads a workflow file and warns to stderr when the `format_version:`
|
|
51
|
+
# header is missing or declares an unsupported version. Always returns
|
|
52
|
+
# the parsed integer (or nil) — never raises. Callers should still
|
|
53
|
+
# `load` the file regardless.
|
|
54
|
+
def self.verify_workflow_format_version!(path)
|
|
55
|
+
source = File.read(path)
|
|
56
|
+
version = parse_workflow_format_version(source)
|
|
57
|
+
|
|
58
|
+
if version.nil?
|
|
59
|
+
warn "[browserctl] workflow #{path} is missing a `# format_version: N` header " \
|
|
60
|
+
"(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway"
|
|
61
|
+
elsif !SUPPORTED_WORKFLOW_FORMAT_VERSIONS.include?(version)
|
|
62
|
+
warn "[browserctl] workflow #{path} format_version=#{version} is not supported " \
|
|
63
|
+
"(expected #{WORKFLOW_FORMAT_VERSION}); proceeding anyway"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
version
|
|
67
|
+
end
|
|
68
|
+
|
|
14
69
|
ParamDef = Struct.new(:name, :required, :secret, :default, :secret_ref, keyword_init: true)
|
|
15
70
|
StepResult = Struct.new(:name, :ok, :error, keyword_init: true)
|
|
16
71
|
StepDef = Struct.new(:label, :block, :retry_count, :timeout, keyword_init: true)
|
|
@@ -391,7 +446,7 @@ module Browserctl
|
|
|
391
446
|
end
|
|
392
447
|
|
|
393
448
|
def selector_not_found?(res)
|
|
394
|
-
res.is_a?(Hash) && res[:code] ==
|
|
449
|
+
res.is_a?(Hash) && res[:code] == Browserctl::Error::Codes::SELECTOR_NOT_FOUND
|
|
395
450
|
end
|
|
396
451
|
|
|
397
452
|
def log_rematch(cmd, selector, match)
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browserctl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: ferrum
|
|
@@ -127,8 +126,8 @@ description: Named browser sessions, Ruby workflow DSL, and a token-efficient DO
|
|
|
127
126
|
email:
|
|
128
127
|
- patrick204nqh@gmail.com
|
|
129
128
|
executables:
|
|
130
|
-
- browserd
|
|
131
129
|
- browserctl
|
|
130
|
+
- browserd
|
|
132
131
|
extensions: []
|
|
133
132
|
extra_rdoc_files: []
|
|
134
133
|
files:
|
|
@@ -176,6 +175,7 @@ files:
|
|
|
176
175
|
- lib/browserctl/commands/fill.rb
|
|
177
176
|
- lib/browserctl/commands/flow.rb
|
|
178
177
|
- lib/browserctl/commands/init.rb
|
|
178
|
+
- lib/browserctl/commands/migrate.rb
|
|
179
179
|
- lib/browserctl/commands/page.rb
|
|
180
180
|
- lib/browserctl/commands/record.rb
|
|
181
181
|
- lib/browserctl/commands/resume.rb
|
|
@@ -184,14 +184,19 @@ files:
|
|
|
184
184
|
- lib/browserctl/commands/snapshot.rb
|
|
185
185
|
- lib/browserctl/commands/state.rb
|
|
186
186
|
- lib/browserctl/commands/storage.rb
|
|
187
|
+
- lib/browserctl/commands/trace.rb
|
|
187
188
|
- lib/browserctl/commands/workflow.rb
|
|
188
189
|
- lib/browserctl/constants.rb
|
|
190
|
+
- lib/browserctl/crash_report.rb
|
|
189
191
|
- lib/browserctl/detectors.rb
|
|
190
192
|
- lib/browserctl/detectors/auth_required.rb
|
|
191
193
|
- lib/browserctl/driver.rb
|
|
192
194
|
- lib/browserctl/driver/base.rb
|
|
193
195
|
- lib/browserctl/driver/cdp.rb
|
|
194
196
|
- lib/browserctl/driver/cdp_page.rb
|
|
197
|
+
- lib/browserctl/error/codes.rb
|
|
198
|
+
- lib/browserctl/error/exit_codes.rb
|
|
199
|
+
- lib/browserctl/error/suggested_actions.rb
|
|
195
200
|
- lib/browserctl/errors.rb
|
|
196
201
|
- lib/browserctl/flow.rb
|
|
197
202
|
- lib/browserctl/flow_registry.rb
|
|
@@ -201,13 +206,17 @@ files:
|
|
|
201
206
|
- lib/browserctl/flows/stdlib/oauth_github.rb
|
|
202
207
|
- lib/browserctl/flows/stdlib/oauth_google.rb
|
|
203
208
|
- lib/browserctl/flows/stdlib/totp_2fa.rb
|
|
209
|
+
- lib/browserctl/format_version.rb
|
|
204
210
|
- lib/browserctl/logger.rb
|
|
211
|
+
- lib/browserctl/migrations.rb
|
|
205
212
|
- lib/browserctl/policy.rb
|
|
206
213
|
- lib/browserctl/recording.rb
|
|
214
|
+
- lib/browserctl/redactor.rb
|
|
207
215
|
- lib/browserctl/replay/context.rb
|
|
208
216
|
- lib/browserctl/replay/fingerprint_matcher.rb
|
|
209
217
|
- lib/browserctl/replay/snapshot_diff.rb
|
|
210
218
|
- lib/browserctl/replay/telemetry.rb
|
|
219
|
+
- lib/browserctl/rubocop/cops/typed_error.rb
|
|
211
220
|
- lib/browserctl/runner.rb
|
|
212
221
|
- lib/browserctl/secret_resolver_registry.rb
|
|
213
222
|
- lib/browserctl/secret_resolvers.rb
|
|
@@ -220,6 +229,7 @@ files:
|
|
|
220
229
|
- lib/browserctl/server/handlers/cookies.rb
|
|
221
230
|
- lib/browserctl/server/handlers/daemon_control.rb
|
|
222
231
|
- lib/browserctl/server/handlers/devtools.rb
|
|
232
|
+
- lib/browserctl/server/handlers/error_payload.rb
|
|
223
233
|
- lib/browserctl/server/handlers/hitl.rb
|
|
224
234
|
- lib/browserctl/server/handlers/interaction.rb
|
|
225
235
|
- lib/browserctl/server/handlers/navigation.rb
|
|
@@ -258,7 +268,6 @@ metadata:
|
|
|
258
268
|
bug_tracker_uri: https://github.com/patrick204nqh/browserctl/issues
|
|
259
269
|
documentation_uri: https://github.com/patrick204nqh/browserctl/tree/main/docs
|
|
260
270
|
rubygems_mfa_required: 'true'
|
|
261
|
-
post_install_message:
|
|
262
271
|
rdoc_options: []
|
|
263
272
|
require_paths:
|
|
264
273
|
- lib
|
|
@@ -273,8 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
273
282
|
- !ruby/object:Gem::Version
|
|
274
283
|
version: '0'
|
|
275
284
|
requirements: []
|
|
276
|
-
rubygems_version: 3.
|
|
277
|
-
signing_key:
|
|
285
|
+
rubygems_version: 3.6.9
|
|
278
286
|
specification_version: 4
|
|
279
287
|
summary: Persistent browser automation daemon and CLI for AI agents and developer
|
|
280
288
|
workflows
|