brainzlab 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +30 -0
  4. data/lib/brainzlab/beacon/client.rb +209 -0
  5. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  6. data/lib/brainzlab/beacon.rb +215 -0
  7. data/lib/brainzlab/configuration.rb +341 -3
  8. data/lib/brainzlab/cortex/cache.rb +59 -0
  9. data/lib/brainzlab/cortex/client.rb +141 -0
  10. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  11. data/lib/brainzlab/cortex.rb +227 -0
  12. data/lib/brainzlab/dendrite/client.rb +232 -0
  13. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  14. data/lib/brainzlab/dendrite.rb +195 -0
  15. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  16. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  17. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  18. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  19. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  20. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  21. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  22. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  23. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  24. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  25. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  26. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  27. data/lib/brainzlab/devtools.rb +75 -0
  28. data/lib/brainzlab/flux/buffer.rb +96 -0
  29. data/lib/brainzlab/flux/client.rb +70 -0
  30. data/lib/brainzlab/flux/provisioner.rb +57 -0
  31. data/lib/brainzlab/flux.rb +174 -0
  32. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  33. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  34. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  35. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  36. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  37. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  38. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  39. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  40. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  41. data/lib/brainzlab/instrumentation.rb +72 -0
  42. data/lib/brainzlab/nerve/client.rb +217 -0
  43. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  44. data/lib/brainzlab/nerve.rb +219 -0
  45. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  46. data/lib/brainzlab/pulse/propagation.rb +1 -1
  47. data/lib/brainzlab/pulse/tracer.rb +1 -1
  48. data/lib/brainzlab/pulse.rb +1 -1
  49. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  50. data/lib/brainzlab/rails/railtie.rb +36 -3
  51. data/lib/brainzlab/recall/provisioner.rb +17 -0
  52. data/lib/brainzlab/recall.rb +6 -1
  53. data/lib/brainzlab/reflex.rb +20 -5
  54. data/lib/brainzlab/sentinel/client.rb +218 -0
  55. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  56. data/lib/brainzlab/sentinel.rb +165 -0
  57. data/lib/brainzlab/signal/client.rb +62 -0
  58. data/lib/brainzlab/signal/provisioner.rb +55 -0
  59. data/lib/brainzlab/signal.rb +136 -0
  60. data/lib/brainzlab/synapse/client.rb +290 -0
  61. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  62. data/lib/brainzlab/synapse.rb +270 -0
  63. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  64. data/lib/brainzlab/utilities/health_check.rb +296 -0
  65. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  66. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  67. data/lib/brainzlab/utilities.rb +17 -0
  68. data/lib/brainzlab/vault/cache.rb +80 -0
  69. data/lib/brainzlab/vault/client.rb +198 -0
  70. data/lib/brainzlab/vault/provisioner.rb +49 -0
  71. data/lib/brainzlab/vault.rb +268 -0
  72. data/lib/brainzlab/version.rb +1 -1
  73. data/lib/brainzlab/vision/client.rb +128 -0
  74. data/lib/brainzlab/vision/provisioner.rb +136 -0
  75. data/lib/brainzlab/vision.rb +157 -0
  76. data/lib/brainzlab.rb +101 -0
  77. metadata +62 -2
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module BrainzLab
8
+ module Vault
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ @base_url = config.vault_url || "https://vault.brainzlab.ai"
13
+ end
14
+
15
+ def get(key, environment:)
16
+ response = request(
17
+ :get,
18
+ "/api/v1/secrets/#{CGI.escape(key)}",
19
+ headers: { "X-Vault-Environment" => environment }
20
+ )
21
+
22
+ return nil unless response.is_a?(Net::HTTPSuccess)
23
+
24
+ data = JSON.parse(response.body, symbolize_names: true)
25
+ data[:value]
26
+ rescue StandardError => e
27
+ log_error("get", e)
28
+ nil
29
+ end
30
+
31
+ def set(key, value, environment:, description: nil, note: nil)
32
+ body = {
33
+ key: key,
34
+ value: value,
35
+ description: description,
36
+ note: note
37
+ }.compact
38
+
39
+ response = request(
40
+ :post,
41
+ "/api/v1/secrets",
42
+ headers: { "X-Vault-Environment" => environment },
43
+ body: body
44
+ )
45
+
46
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
47
+ rescue StandardError => e
48
+ log_error("set", e)
49
+ false
50
+ end
51
+
52
+ def list(environment:)
53
+ response = request(
54
+ :get,
55
+ "/api/v1/secrets",
56
+ headers: { "X-Vault-Environment" => environment }
57
+ )
58
+
59
+ return [] unless response.is_a?(Net::HTTPSuccess)
60
+
61
+ data = JSON.parse(response.body, symbolize_names: true)
62
+ data[:secrets] || []
63
+ rescue StandardError => e
64
+ log_error("list", e)
65
+ []
66
+ end
67
+
68
+ def delete(key)
69
+ response = request(:delete, "/api/v1/secrets/#{CGI.escape(key)}")
70
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
71
+ rescue StandardError => e
72
+ log_error("delete", e)
73
+ false
74
+ end
75
+
76
+ def export(environment:, format:)
77
+ params = { format: format }
78
+ response = request(
79
+ :get,
80
+ "/api/v1/sync/export",
81
+ headers: { "X-Vault-Environment" => environment },
82
+ params: params
83
+ )
84
+
85
+ return {} unless response.is_a?(Net::HTTPSuccess)
86
+
87
+ case format
88
+ when :json
89
+ data = JSON.parse(response.body, symbolize_names: true)
90
+ data[:secrets] || {}
91
+ else
92
+ response.body
93
+ end
94
+ rescue StandardError => e
95
+ log_error("export", e)
96
+ format == :json ? {} : ""
97
+ end
98
+
99
+ def provision(project_id:, app_name:)
100
+ response = request(
101
+ :post,
102
+ "/api/v1/projects/provision",
103
+ body: { project_id: project_id, app_name: app_name },
104
+ use_service_key: true
105
+ )
106
+
107
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
108
+ rescue StandardError => e
109
+ log_error("provision", e)
110
+ false
111
+ end
112
+
113
+ # Get all provider keys for the current project
114
+ # Returns a hash of provider => decrypted_key
115
+ def get_provider_keys
116
+ response = request(:get, "/api/v1/provider_keys/bulk")
117
+
118
+ return {} unless response.is_a?(Net::HTTPSuccess)
119
+
120
+ data = JSON.parse(response.body, symbolize_names: true)
121
+ # Convert to simple hash: { openai: "sk-...", anthropic: "sk-..." }
122
+ keys = {}
123
+ (data[:keys] || []).each do |key_data|
124
+ keys[key_data[:provider].to_sym] = key_data[:key]
125
+ end
126
+ keys
127
+ rescue StandardError => e
128
+ log_error("get_provider_keys", e)
129
+ {}
130
+ end
131
+
132
+ # Get a specific provider key
133
+ def get_provider_key(provider:, model_type: "llm")
134
+ response = request(
135
+ :get,
136
+ "/api/v1/provider_keys/resolve",
137
+ params: { provider: provider, model_type: model_type }
138
+ )
139
+
140
+ return nil unless response.is_a?(Net::HTTPSuccess)
141
+
142
+ data = JSON.parse(response.body, symbolize_names: true)
143
+ data[:key]
144
+ rescue StandardError => e
145
+ log_error("get_provider_key", e)
146
+ nil
147
+ end
148
+
149
+ private
150
+
151
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
152
+ uri = URI.parse("#{@base_url}#{path}")
153
+
154
+ if params
155
+ uri.query = URI.encode_www_form(params)
156
+ end
157
+
158
+ http = Net::HTTP.new(uri.host, uri.port)
159
+ http.use_ssl = uri.scheme == "https"
160
+ http.open_timeout = 10
161
+ http.read_timeout = 30
162
+
163
+ request = case method
164
+ when :get
165
+ Net::HTTP::Get.new(uri)
166
+ when :post
167
+ Net::HTTP::Post.new(uri)
168
+ when :put
169
+ Net::HTTP::Put.new(uri)
170
+ when :delete
171
+ Net::HTTP::Delete.new(uri)
172
+ end
173
+
174
+ # Set headers
175
+ request["Content-Type"] = "application/json"
176
+ request["Accept"] = "application/json"
177
+
178
+ if use_service_key
179
+ request["X-Service-Key"] = @config.vault_master_key || @config.secret_key
180
+ else
181
+ auth_key = @config.vault_api_key || @config.secret_key
182
+ request["Authorization"] = "Bearer #{auth_key}" if auth_key
183
+ end
184
+
185
+ headers.each { |k, v| request[k] = v }
186
+
187
+ # Set body
188
+ request.body = body.to_json if body
189
+
190
+ http.request(request)
191
+ end
192
+
193
+ def log_error(operation, error)
194
+ BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{error.message}")
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Vault
5
+ class Provisioner
6
+ def initialize(config)
7
+ @config = config
8
+ @provisioned = false
9
+ end
10
+
11
+ def ensure_project!
12
+ return if @provisioned
13
+ return unless @config.vault_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ # Try to provision with Platform project ID
19
+ project_id = detect_project_id
20
+ return unless project_id
21
+
22
+ client = Client.new(@config)
23
+ client.provision(
24
+ project_id: project_id,
25
+ app_name: @config.app_name || @config.service
26
+ )
27
+
28
+ BrainzLab.debug_log("[Vault::Provisioner] Project provisioned: #{project_id}")
29
+ rescue StandardError => e
30
+ BrainzLab.debug_log("[Vault::Provisioner] Provisioning failed: #{e.message}")
31
+ end
32
+
33
+ private
34
+
35
+ def valid_auth?
36
+ key = @config.vault_api_key || @config.vault_master_key || @config.secret_key
37
+ !key.nil? && !key.empty?
38
+ end
39
+
40
+ def detect_project_id
41
+ # Try environment variable first
42
+ return ENV["BRAINZLAB_PROJECT_ID"] if ENV["BRAINZLAB_PROJECT_ID"]
43
+
44
+ # Could also detect from Platform API if we have a secret key
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vault/client"
4
+ require_relative "vault/cache"
5
+ require_relative "vault/provisioner"
6
+
7
+ module BrainzLab
8
+ module Vault
9
+ class << self
10
+ # Load all secrets into ENV like dotenv
11
+ # This is the main method to use at app startup
12
+ #
13
+ # @param environment [String, Symbol] Environment to load (defaults to current)
14
+ # @param overwrite [Boolean] Whether to overwrite existing ENV vars (default: false)
15
+ # @param provider_keys [Boolean] Also load provider keys like OPENAI_API_KEY (default: true)
16
+ # @return [Hash] The secrets that were loaded
17
+ #
18
+ # @example
19
+ # # In config/application.rb or an initializer
20
+ # BrainzLab::Vault.load!
21
+ #
22
+ # # Load with options
23
+ # BrainzLab::Vault.load!(environment: :production, overwrite: true)
24
+ #
25
+ def load!(environment: nil, overwrite: false, provider_keys: true)
26
+ return {} unless enabled?
27
+
28
+ ensure_provisioned!
29
+ return {} unless BrainzLab.configuration.vault_valid?
30
+
31
+ env = environment&.to_s || BrainzLab.configuration.environment
32
+ loaded = {}
33
+
34
+ # Load regular secrets
35
+ secrets = export(environment: env, format: :json)
36
+ secrets.each do |key, value|
37
+ key_str = key.to_s
38
+ if overwrite || !ENV.key?(key_str)
39
+ ENV[key_str] = value.to_s
40
+ loaded[key_str] = value
41
+ BrainzLab.debug_log("[Vault] Loaded #{key_str}")
42
+ end
43
+ end
44
+
45
+ # Load provider keys (OpenAI, Anthropic, etc.)
46
+ if provider_keys
47
+ provider_secrets = load_provider_keys!(overwrite: overwrite)
48
+ loaded.merge!(provider_secrets)
49
+ end
50
+
51
+ BrainzLab.debug_log("[Vault] Loaded #{loaded.size} secrets into ENV")
52
+ loaded
53
+ rescue StandardError => e
54
+ BrainzLab.debug_log("[Vault] Failed to load secrets: #{e.message}")
55
+ {}
56
+ end
57
+
58
+ # Load provider keys (API keys for LLMs, etc.) into ENV
59
+ #
60
+ # @param overwrite [Boolean] Whether to overwrite existing ENV vars
61
+ # @return [Hash] Provider keys that were loaded
62
+ def load_provider_keys!(overwrite: false)
63
+ return {} unless enabled? && BrainzLab.configuration.vault_valid?
64
+
65
+ loaded = {}
66
+ provider_keys = client.get_provider_keys
67
+
68
+ provider_keys.each do |provider, key|
69
+ env_var = "#{provider.to_s.upcase}_API_KEY"
70
+ if overwrite || !ENV.key?(env_var)
71
+ ENV[env_var] = key
72
+ loaded[env_var] = key
73
+ BrainzLab.debug_log("[Vault] Loaded provider key: #{env_var}")
74
+ end
75
+ end
76
+
77
+ loaded
78
+ rescue StandardError => e
79
+ BrainzLab.debug_log("[Vault] Failed to load provider keys: #{e.message}")
80
+ {}
81
+ end
82
+
83
+ # Get a specific provider key
84
+ # @param provider [String, Symbol] Provider name (openai, anthropic, etc.)
85
+ # @param model_type [String] Model type (llm, embedding, etc.)
86
+ # @return [String, nil] The API key
87
+ def provider_key(provider, model_type: "llm")
88
+ return nil unless enabled?
89
+
90
+ ensure_provisioned!
91
+ return nil unless BrainzLab.configuration.vault_valid?
92
+
93
+ # Check ENV first
94
+ env_var = "#{provider.to_s.upcase}_API_KEY"
95
+ return ENV[env_var] if ENV[env_var] && !ENV[env_var].empty?
96
+
97
+ # Fetch from Vault
98
+ client.get_provider_key(provider: provider.to_s, model_type: model_type)
99
+ end
100
+
101
+ # Get a secret value
102
+ # @param key [String] The secret key
103
+ # @param environment [String, Symbol] Optional environment (defaults to current environment)
104
+ # @param default [Object] Default value if secret not found
105
+ # @return [String, nil] The secret value
106
+ def get(key, environment: nil, default: nil)
107
+ return default unless enabled?
108
+
109
+ ensure_provisioned!
110
+ return default unless BrainzLab.configuration.vault_valid?
111
+
112
+ env = environment&.to_s || BrainzLab.configuration.environment
113
+ cache_key = "#{env}:#{key}"
114
+
115
+ # Check cache first
116
+ if BrainzLab.configuration.vault_cache_enabled && cache.has?(cache_key)
117
+ return cache.get(cache_key)
118
+ end
119
+
120
+ value = client.get(key, environment: env)
121
+
122
+ if value.nil?
123
+ default
124
+ else
125
+ cache.set(cache_key, value) if BrainzLab.configuration.vault_cache_enabled
126
+ value
127
+ end
128
+ end
129
+
130
+ # Set a secret value
131
+ # @param key [String] The secret key
132
+ # @param value [String] The secret value
133
+ # @param environment [String, Symbol] Optional environment (defaults to current environment)
134
+ # @param description [String] Optional description
135
+ # @param note [String] Optional version note
136
+ # @return [Boolean] True if successful
137
+ def set(key, value, environment: nil, description: nil, note: nil)
138
+ return false unless enabled?
139
+
140
+ ensure_provisioned!
141
+ return false unless BrainzLab.configuration.vault_valid?
142
+
143
+ env = environment&.to_s || BrainzLab.configuration.environment
144
+ result = client.set(key, value, environment: env, description: description, note: note)
145
+
146
+ # Invalidate cache
147
+ if result && BrainzLab.configuration.vault_cache_enabled
148
+ cache.delete("#{env}:#{key}")
149
+ end
150
+
151
+ result
152
+ end
153
+
154
+ # List all secret keys
155
+ # @param environment [String, Symbol] Optional environment
156
+ # @return [Array<Hash>] List of secret metadata
157
+ def list(environment: nil)
158
+ return [] unless enabled?
159
+
160
+ ensure_provisioned!
161
+ return [] unless BrainzLab.configuration.vault_valid?
162
+
163
+ env = environment&.to_s || BrainzLab.configuration.environment
164
+ client.list(environment: env)
165
+ end
166
+
167
+ # Delete (archive) a secret
168
+ # @param key [String] The secret key
169
+ # @return [Boolean] True if successful
170
+ def delete(key)
171
+ return false unless enabled?
172
+
173
+ ensure_provisioned!
174
+ return false unless BrainzLab.configuration.vault_valid?
175
+
176
+ result = client.delete(key)
177
+
178
+ # Invalidate all environment caches for this key
179
+ if result && BrainzLab.configuration.vault_cache_enabled
180
+ cache.delete_pattern("*:#{key}")
181
+ end
182
+
183
+ result
184
+ end
185
+
186
+ # Export all secrets for an environment
187
+ # @param environment [String, Symbol] Environment to export
188
+ # @param format [Symbol] Output format (:json, :dotenv, :shell)
189
+ # @return [Hash, String] Exported secrets
190
+ def export(environment: nil, format: :json)
191
+ return {} unless enabled?
192
+
193
+ ensure_provisioned!
194
+ return {} unless BrainzLab.configuration.vault_valid?
195
+
196
+ env = environment&.to_s || BrainzLab.configuration.environment
197
+ client.export(environment: env, format: format)
198
+ end
199
+
200
+ # Fetch a secret with automatic fallback
201
+ # @param key [String] The secret key
202
+ # @param env_var [String] Environment variable to fall back to
203
+ # @return [String, nil] The secret value
204
+ def fetch(key, env_var: nil)
205
+ value = get(key)
206
+ return value if value && !value.to_s.empty?
207
+
208
+ # Fall back to environment variable
209
+ if env_var
210
+ ENV[env_var]
211
+ else
212
+ ENV[key]
213
+ end
214
+ end
215
+
216
+ # Clear the secret cache
217
+ def clear_cache!
218
+ cache.clear!
219
+ end
220
+
221
+ # Warm the cache with all secrets
222
+ def warm_cache!(environment: nil)
223
+ return unless enabled? && BrainzLab.configuration.vault_cache_enabled
224
+
225
+ env = environment&.to_s || BrainzLab.configuration.environment
226
+ secrets = export(environment: env, format: :json)
227
+
228
+ secrets.each do |key, value|
229
+ cache.set("#{env}:#{key}", value)
230
+ end
231
+ end
232
+
233
+ # === INTERNAL ===
234
+
235
+ def ensure_provisioned!
236
+ return if @provisioned
237
+
238
+ @provisioned = true
239
+ provisioner.ensure_project!
240
+ end
241
+
242
+ def provisioner
243
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
244
+ end
245
+
246
+ def client
247
+ @client ||= Client.new(BrainzLab.configuration)
248
+ end
249
+
250
+ def cache
251
+ @cache ||= Cache.new(BrainzLab.configuration.vault_cache_ttl)
252
+ end
253
+
254
+ def reset!
255
+ @client = nil
256
+ @provisioner = nil
257
+ @cache = nil
258
+ @provisioned = false
259
+ end
260
+
261
+ private
262
+
263
+ def enabled?
264
+ BrainzLab.configuration.vault_enabled
265
+ end
266
+ end
267
+ end
268
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrainzLab
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module BrainzLab
8
+ module Vision
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # Execute an autonomous AI task
15
+ def execute_task(instruction:, start_url:, model: nil, browser_provider: nil, max_steps: 50, timeout: 300)
16
+ payload = {
17
+ instruction: instruction,
18
+ start_url: start_url,
19
+ max_steps: max_steps,
20
+ timeout: timeout
21
+ }
22
+ payload[:model] = model if model
23
+ payload[:browser_provider] = browser_provider if browser_provider
24
+
25
+ post("/mcp/tools/vision_task", payload)
26
+ end
27
+
28
+ # Create a browser session
29
+ def create_session(url: nil, viewport: nil, browser_provider: nil)
30
+ payload = {}
31
+ payload[:url] = url if url
32
+ payload[:viewport] = viewport if viewport
33
+ payload[:browser_provider] = browser_provider if browser_provider
34
+
35
+ post("/mcp/tools/vision_session_create", payload)
36
+ end
37
+
38
+ # Perform an AI-powered action
39
+ def ai_action(session_id:, instruction:, model: nil)
40
+ payload = {
41
+ session_id: session_id,
42
+ instruction: instruction
43
+ }
44
+ payload[:model] = model if model
45
+
46
+ post("/mcp/tools/vision_ai_action", payload)
47
+ end
48
+
49
+ # Perform a direct browser action
50
+ def perform(session_id:, action:, selector: nil, value: nil)
51
+ payload = {
52
+ session_id: session_id,
53
+ action: action.to_s
54
+ }
55
+ payload[:selector] = selector if selector
56
+ payload[:value] = value if value
57
+
58
+ post("/mcp/tools/vision_perform", payload)
59
+ end
60
+
61
+ # Extract structured data
62
+ def extract(session_id:, schema:, instruction: nil)
63
+ payload = {
64
+ session_id: session_id,
65
+ schema: schema
66
+ }
67
+ payload[:instruction] = instruction if instruction
68
+
69
+ post("/mcp/tools/vision_extract", payload)
70
+ end
71
+
72
+ # Close a session
73
+ def close_session(session_id:)
74
+ post("/mcp/tools/vision_session_close", { session_id: session_id })
75
+ end
76
+
77
+ # Take a screenshot
78
+ def screenshot(session_id:, full_page: true)
79
+ post("/mcp/tools/vision_screenshot", {
80
+ session_id: session_id,
81
+ full_page: full_page
82
+ })
83
+ end
84
+
85
+ private
86
+
87
+ def post(path, payload)
88
+ uri = URI.parse("#{@config.vision_url}#{path}")
89
+ request = Net::HTTP::Post.new(uri)
90
+ request["Content-Type"] = "application/json"
91
+ request["Authorization"] = "Bearer #{auth_key}"
92
+ request["User-Agent"] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
93
+ request.body = JSON.generate(payload)
94
+
95
+ response = execute(uri, request)
96
+
97
+ case response
98
+ when Net::HTTPSuccess
99
+ JSON.parse(response.body, symbolize_names: true)
100
+ when Net::HTTPUnauthorized
101
+ { error: "Unauthorized: Invalid API key" }
102
+ when Net::HTTPForbidden
103
+ { error: "Forbidden: Vision is not enabled for this project" }
104
+ when Net::HTTPNotFound
105
+ { error: "Not found: #{path}" }
106
+ else
107
+ { error: "HTTP #{response.code}: #{response.message}" }
108
+ end
109
+ rescue JSON::ParserError => e
110
+ { error: "Invalid JSON response: #{e.message}" }
111
+ rescue StandardError => e
112
+ { error: "Request failed: #{e.message}" }
113
+ end
114
+
115
+ def auth_key
116
+ @config.vision_ingest_key || @config.vision_api_key || @config.secret_key
117
+ end
118
+
119
+ def execute(uri, request)
120
+ http = Net::HTTP.new(uri.host, uri.port)
121
+ http.use_ssl = uri.scheme == "https"
122
+ http.open_timeout = 10
123
+ http.read_timeout = 300 # Long timeout for AI tasks
124
+ http.request(request)
125
+ end
126
+ end
127
+ end
128
+ end