fluyenta-ruby 0.1.14

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ # Debug module for SDK operation logging
5
+ #
6
+ # Provides pretty-printed debug output for all SDK operations when debug mode is enabled.
7
+ # Includes timing information and request/response details.
8
+ #
9
+ # @example Enable debug mode
10
+ # BrainzLab.configure do |config|
11
+ # config.debug = true
12
+ # end
13
+ #
14
+ # @example Use custom logger
15
+ # BrainzLab.configure do |config|
16
+ # config.debug = true
17
+ # config.logger = Logger.new(STDOUT)
18
+ # end
19
+ #
20
+ # @example Manual debug logging
21
+ # BrainzLab::Debug.log("Custom message", level: :info)
22
+ #
23
+ module Debug
24
+ COLORS = {
25
+ reset: "\e[0m",
26
+ bold: "\e[1m",
27
+ dim: "\e[2m",
28
+ red: "\e[31m",
29
+ green: "\e[32m",
30
+ yellow: "\e[33m",
31
+ blue: "\e[34m",
32
+ magenta: "\e[35m",
33
+ cyan: "\e[36m",
34
+ white: "\e[37m",
35
+ gray: "\e[90m"
36
+ }.freeze
37
+
38
+ LEVEL_COLORS = {
39
+ debug: :gray,
40
+ info: :cyan,
41
+ warn: :yellow,
42
+ error: :red,
43
+ fatal: :red
44
+ }.freeze
45
+
46
+ LEVEL_LABELS = {
47
+ debug: 'DEBUG',
48
+ info: 'INFO',
49
+ warn: 'WARN',
50
+ error: 'ERROR',
51
+ fatal: 'FATAL'
52
+ }.freeze
53
+
54
+ class << self
55
+ # Log a debug message if debug mode is enabled
56
+ #
57
+ # @param message [String] The message to log
58
+ # @param level [Symbol] Log level (:debug, :info, :warn, :error, :fatal)
59
+ # @param data [Hash] Optional additional data to include
60
+ # @return [void]
61
+ def log(message, level: :info, **data)
62
+ return unless enabled?
63
+
64
+ output = format_message(message, level: level, **data)
65
+ write_output(output, level: level)
66
+ end
67
+
68
+ # Log an outgoing request
69
+ #
70
+ # @param service [String, Symbol] The service name (e.g., :recall, :reflex)
71
+ # @param method [String] HTTP method
72
+ # @param path [String] Request path
73
+ # @param data [Hash] Request payload summary
74
+ # @return [void]
75
+ def log_request(service, method, path, data: nil)
76
+ return unless enabled?
77
+
78
+ data_summary = summarize_data(data) if data
79
+ message = data_summary ? "#{method} #{path} #{data_summary}" : "#{method} #{path}"
80
+
81
+ output = format_arrow_message(:out, service, message)
82
+ write_output(output, level: :info)
83
+ end
84
+
85
+ # Log an incoming response
86
+ #
87
+ # @param service [String, Symbol] The service name
88
+ # @param status [Integer] HTTP status code
89
+ # @param duration_ms [Float] Request duration in milliseconds
90
+ # @param error [String, nil] Error message if request failed
91
+ # @return [void]
92
+ def log_response(service, status, duration_ms, error: nil)
93
+ return unless enabled?
94
+
95
+ status_text = status_message(status)
96
+ duration_text = format_duration(duration_ms)
97
+
98
+ message = if error
99
+ "#{status} #{status_text} (#{duration_text}) - #{error}"
100
+ else
101
+ "#{status} #{status_text} (#{duration_text})"
102
+ end
103
+
104
+ level = status >= 400 ? :error : :info
105
+ output = format_arrow_message(:in, service, message, level: level)
106
+ write_output(output, level: level)
107
+ end
108
+
109
+ # Log an SDK operation with timing
110
+ #
111
+ # @param service [String, Symbol] The service name
112
+ # @param operation [String] Operation description
113
+ # @param data [Hash] Operation data
114
+ # @return [void]
115
+ def log_operation(service, operation, **data)
116
+ return unless enabled?
117
+
118
+ data_summary = data.empty? ? '' : " (#{format_data_inline(data)})"
119
+ message = "#{operation}#{data_summary}"
120
+
121
+ output = format_arrow_message(:out, service, message)
122
+ write_output(output, level: :info)
123
+ end
124
+
125
+ # Measure and log execution time of a block
126
+ #
127
+ # @param service [String, Symbol] The service name
128
+ # @param operation [String] Operation description
129
+ # @yield Block to measure
130
+ # @return [Object] Result of the block
131
+ def measure(service, operation)
132
+ return yield unless enabled?
133
+
134
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
135
+ log_operation(service, operation)
136
+
137
+ result = yield
138
+
139
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
140
+ log("#{operation} completed", level: :debug, duration_ms: duration_ms, service: service.to_s)
141
+
142
+ result
143
+ rescue StandardError => e
144
+ duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
145
+ log("#{operation} failed: #{e.message}", level: :error, duration_ms: duration_ms, service: service.to_s)
146
+ raise
147
+ end
148
+
149
+ # Check if debug mode is enabled
150
+ #
151
+ # @return [Boolean]
152
+ def enabled?
153
+ BrainzLab.configuration.debug?
154
+ end
155
+
156
+ # Check if colorized output should be used
157
+ #
158
+ # @return [Boolean]
159
+ def colorize?
160
+ return false unless enabled?
161
+ return @colorize if defined?(@colorize)
162
+
163
+ @colorize = $stdout.tty?
164
+ end
165
+
166
+ # Reset colorize detection (useful for testing)
167
+ def reset_colorize!
168
+ remove_instance_variable(:@colorize) if defined?(@colorize)
169
+ end
170
+
171
+ private
172
+
173
+ def format_message(message, level:, **data)
174
+ timestamp = format_timestamp
175
+ prefix = colorize("[BrainzLab]", :bold, :blue)
176
+ level_badge = format_level(level)
177
+ data_str = data.empty? ? '' : " #{format_data(data)}"
178
+
179
+ "#{prefix} #{timestamp} #{level_badge} #{message}#{data_str}"
180
+ end
181
+
182
+ def format_arrow_message(direction, service, message, level: :info)
183
+ timestamp = format_timestamp
184
+ prefix = colorize("[BrainzLab]", :bold, :blue)
185
+ arrow = direction == :out ? colorize("->", :cyan) : colorize("<-", :green)
186
+ service_name = colorize(service.to_s.capitalize, :magenta)
187
+
188
+ "#{prefix} #{timestamp} #{arrow} #{service_name} #{message}"
189
+ end
190
+
191
+ def format_timestamp
192
+ time = Time.now.strftime('%H:%M:%S')
193
+ colorize(time, :dim)
194
+ end
195
+
196
+ def format_level(level)
197
+ label = LEVEL_LABELS[level] || level.to_s.upcase
198
+ color = LEVEL_COLORS[level] || :white
199
+ colorize("[#{label}]", color)
200
+ end
201
+
202
+ def format_duration(ms)
203
+ if ms < 1
204
+ colorize("#{(ms * 1000).round(0)}us", :green)
205
+ elsif ms < 100
206
+ colorize("#{ms.round(1)}ms", :green)
207
+ elsif ms < 1000
208
+ colorize("#{ms.round(0)}ms", :yellow)
209
+ else
210
+ colorize("#{(ms / 1000.0).round(2)}s", :red)
211
+ end
212
+ end
213
+
214
+ def format_data(data)
215
+ pairs = data.map { |k, v| "#{k}: #{format_value(v)}" }
216
+ colorize("(#{pairs.join(', ')})", :dim)
217
+ end
218
+
219
+ def format_data_inline(data)
220
+ data.map { |k, v| "#{k}: #{format_value(v)}" }.join(', ')
221
+ end
222
+
223
+ def format_value(value)
224
+ case value
225
+ when String
226
+ value.length > 50 ? "#{value[0..47]}..." : value
227
+ when Hash
228
+ "{#{value.keys.join(', ')}}"
229
+ when Array
230
+ "[#{value.length} items]"
231
+ else
232
+ value.to_s
233
+ end
234
+ end
235
+
236
+ def summarize_data(data)
237
+ return nil unless data.is_a?(Hash)
238
+
239
+ summary_parts = []
240
+ summary_parts << "\"#{truncate(data[:message] || data['message'], 30)}\"" if data[:message] || data['message']
241
+
242
+ other_keys = data.keys.reject { |k| %i[message timestamp level].include?(k.to_sym) }
243
+ if other_keys.any?
244
+ key_summary = other_keys.take(3).map { |k| "#{k}: #{format_value(data[k])}" }.join(', ')
245
+ key_summary += ", ..." if other_keys.length > 3
246
+ summary_parts << "(#{key_summary})"
247
+ end
248
+
249
+ summary_parts.join(' ')
250
+ end
251
+
252
+ def truncate(str, length)
253
+ return '' unless str
254
+
255
+ str = str.to_s
256
+ str.length > length ? "#{str[0..length - 4]}..." : str
257
+ end
258
+
259
+ def status_message(status)
260
+ case status
261
+ when 200 then 'OK'
262
+ when 201 then 'Created'
263
+ when 204 then 'No Content'
264
+ when 400 then 'Bad Request'
265
+ when 401 then 'Unauthorized'
266
+ when 403 then 'Forbidden'
267
+ when 404 then 'Not Found'
268
+ when 422 then 'Unprocessable Entity'
269
+ when 429 then 'Too Many Requests'
270
+ when 500 then 'Internal Server Error'
271
+ when 502 then 'Bad Gateway'
272
+ when 503 then 'Service Unavailable'
273
+ else ''
274
+ end
275
+ end
276
+
277
+ def colorize(text, *colors)
278
+ return text unless colorize?
279
+
280
+ color_codes = colors.map { |c| COLORS[c] }.compact.join
281
+ "#{color_codes}#{text}#{COLORS[:reset]}"
282
+ end
283
+
284
+ def write_output(output, level:)
285
+ config = BrainzLab.configuration
286
+
287
+ if config.logger
288
+ case level
289
+ when :debug then config.logger.debug(strip_colors(output))
290
+ when :info then config.logger.info(strip_colors(output))
291
+ when :warn then config.logger.warn(strip_colors(output))
292
+ when :error, :fatal then config.logger.error(strip_colors(output))
293
+ else config.logger.info(strip_colors(output))
294
+ end
295
+ else
296
+ $stderr.puts output
297
+ end
298
+ end
299
+
300
+ def strip_colors(text)
301
+ text.gsub(/\e\[\d+m/, '')
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'cgi'
7
+
8
+ module BrainzLab
9
+ module Dendrite
10
+ class Client
11
+ def initialize(config)
12
+ @config = config
13
+ @base_url = config.dendrite_url || 'https://dendrite.brainzlab.ai'
14
+ end
15
+
16
+ # Connect a repository
17
+ def connect_repository(url:, name: nil, branch: 'main', **options)
18
+ response = request(
19
+ :post,
20
+ '/api/v1/repositories',
21
+ body: {
22
+ url: url,
23
+ name: name,
24
+ branch: branch,
25
+ **options
26
+ }
27
+ )
28
+
29
+ return nil unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
30
+
31
+ JSON.parse(response.body, symbolize_names: true)
32
+ rescue StandardError => e
33
+ log_error('connect_repository', e)
34
+ nil
35
+ end
36
+
37
+ # Trigger sync for a repository
38
+ def sync_repository(repo_id)
39
+ response = request(:post, "/api/v1/repositories/#{repo_id}/sync")
40
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
41
+ rescue StandardError => e
42
+ log_error('sync_repository', e)
43
+ false
44
+ end
45
+
46
+ # Get repository status
47
+ def get_repository(repo_id)
48
+ response = request(:get, "/api/v1/repositories/#{repo_id}")
49
+
50
+ return nil unless response.is_a?(Net::HTTPSuccess)
51
+
52
+ JSON.parse(response.body, symbolize_names: true)
53
+ rescue StandardError => e
54
+ log_error('get_repository', e)
55
+ nil
56
+ end
57
+
58
+ # List repositories
59
+ def list_repositories
60
+ response = request(:get, '/api/v1/repositories')
61
+
62
+ return [] unless response.is_a?(Net::HTTPSuccess)
63
+
64
+ data = JSON.parse(response.body, symbolize_names: true)
65
+ data[:repositories] || []
66
+ rescue StandardError => e
67
+ log_error('list_repositories', e)
68
+ []
69
+ end
70
+
71
+ # Get wiki pages for a repository
72
+ def get_wiki(repo_id)
73
+ response = request(:get, "/api/v1/wiki/#{repo_id}")
74
+
75
+ return nil unless response.is_a?(Net::HTTPSuccess)
76
+
77
+ JSON.parse(response.body, symbolize_names: true)
78
+ rescue StandardError => e
79
+ log_error('get_wiki', e)
80
+ nil
81
+ end
82
+
83
+ # Get a specific wiki page
84
+ def get_wiki_page(repo_id, page_slug)
85
+ response = request(:get, "/api/v1/wiki/#{repo_id}/#{CGI.escape(page_slug)}")
86
+
87
+ return nil unless response.is_a?(Net::HTTPSuccess)
88
+
89
+ JSON.parse(response.body, symbolize_names: true)
90
+ rescue StandardError => e
91
+ log_error('get_wiki_page', e)
92
+ nil
93
+ end
94
+
95
+ # Semantic search across codebase
96
+ def search(repo_id, query, limit: 10)
97
+ response = request(
98
+ :get,
99
+ '/api/v1/search',
100
+ params: { repo_id: repo_id, q: query, limit: limit }
101
+ )
102
+
103
+ return [] unless response.is_a?(Net::HTTPSuccess)
104
+
105
+ data = JSON.parse(response.body, symbolize_names: true)
106
+ data[:results] || []
107
+ rescue StandardError => e
108
+ log_error('search', e)
109
+ []
110
+ end
111
+
112
+ # Ask a question about the codebase
113
+ def ask(repo_id, question, session_id: nil)
114
+ response = request(
115
+ :post,
116
+ '/api/v1/chat',
117
+ body: {
118
+ repo_id: repo_id,
119
+ question: question,
120
+ session_id: session_id
121
+ }
122
+ )
123
+
124
+ return nil unless response.is_a?(Net::HTTPSuccess)
125
+
126
+ JSON.parse(response.body, symbolize_names: true)
127
+ rescue StandardError => e
128
+ log_error('ask', e)
129
+ nil
130
+ end
131
+
132
+ # Explain a specific file or function
133
+ def explain(repo_id, path, symbol: nil)
134
+ response = request(
135
+ :post,
136
+ '/api/v1/explain',
137
+ body: {
138
+ repo_id: repo_id,
139
+ path: path,
140
+ symbol: symbol
141
+ }
142
+ )
143
+
144
+ return nil unless response.is_a?(Net::HTTPSuccess)
145
+
146
+ JSON.parse(response.body, symbolize_names: true)
147
+ rescue StandardError => e
148
+ log_error('explain', e)
149
+ nil
150
+ end
151
+
152
+ # Generate a diagram
153
+ def generate_diagram(repo_id, type:, scope: nil)
154
+ response = request(
155
+ :post,
156
+ '/api/v1/diagrams',
157
+ body: {
158
+ repo_id: repo_id,
159
+ type: type, # class, er, sequence, architecture
160
+ scope: scope
161
+ }
162
+ )
163
+
164
+ return nil unless response.is_a?(Net::HTTPSuccess)
165
+
166
+ JSON.parse(response.body, symbolize_names: true)
167
+ rescue StandardError => e
168
+ log_error('generate_diagram', e)
169
+ nil
170
+ end
171
+
172
+ def provision(project_id:, app_name:)
173
+ response = request(
174
+ :post,
175
+ '/api/v1/projects/provision',
176
+ body: { project_id: project_id, app_name: app_name },
177
+ use_service_key: true
178
+ )
179
+
180
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
181
+ rescue StandardError => e
182
+ log_error('provision', e)
183
+ false
184
+ end
185
+
186
+ private
187
+
188
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
189
+ uri = URI.parse("#{@base_url}#{path}")
190
+
191
+ uri.query = URI.encode_www_form(params) if params
192
+
193
+ http = Net::HTTP.new(uri.host, uri.port)
194
+ http.use_ssl = uri.scheme == 'https'
195
+ http.open_timeout = 10
196
+ http.read_timeout = 60 # Longer timeout for AI operations
197
+
198
+ request = case method
199
+ when :get
200
+ Net::HTTP::Get.new(uri)
201
+ when :post
202
+ Net::HTTP::Post.new(uri)
203
+ when :put
204
+ Net::HTTP::Put.new(uri)
205
+ when :delete
206
+ Net::HTTP::Delete.new(uri)
207
+ end
208
+
209
+ request['Content-Type'] = 'application/json'
210
+ request['Accept'] = 'application/json'
211
+
212
+ if use_service_key
213
+ request['X-Service-Key'] = @config.dendrite_master_key || @config.secret_key
214
+ else
215
+ auth_key = @config.dendrite_api_key || @config.secret_key
216
+ request['Authorization'] = "Bearer #{auth_key}" if auth_key
217
+ end
218
+
219
+ headers.each { |k, v| request[k] = v }
220
+ request.body = body.to_json if body
221
+
222
+ http.request(request)
223
+ end
224
+
225
+ def log_error(operation, error)
226
+ structured_error = ErrorHandler.wrap(error, service: 'Dendrite', operation: operation)
227
+ BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{structured_error.message}")
228
+
229
+ # Call on_error callback if configured
230
+ if @config.on_error
231
+ @config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
232
+ end
233
+ end
234
+
235
+ def handle_response_error(response, operation)
236
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent) || response.is_a?(Net::HTTPAccepted)
237
+
238
+ structured_error = ErrorHandler.from_response(response, service: 'Dendrite', operation: operation)
239
+ BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{structured_error.message}")
240
+
241
+ # Call on_error callback if configured
242
+ if @config.on_error
243
+ @config.on_error.call(structured_error, { service: 'Dendrite', operation: operation })
244
+ end
245
+
246
+ structured_error
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Dendrite
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.dendrite_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ project_id = detect_project_id
19
+ return unless project_id
20
+
21
+ client = Client.new(@config)
22
+ client.provision(
23
+ project_id: project_id,
24
+ app_name: @config.app_name || @config.service
25
+ )
26
+
27
+ BrainzLab.debug_log("[Dendrite::Provisioner] Project provisioned: #{project_id}")
28
+ rescue StandardError => e
29
+ BrainzLab.debug_log("[Dendrite::Provisioner] Provisioning failed: #{e.message}")
30
+ end
31
+
32
+ private
33
+
34
+ def valid_auth?
35
+ key = @config.dendrite_api_key || @config.dendrite_master_key || @config.secret_key
36
+ !key.nil? && !key.empty?
37
+ end
38
+
39
+ def detect_project_id
40
+ ENV.fetch('BRAINZLAB_PROJECT_ID', nil)
41
+ end
42
+ end
43
+ end
44
+ end