collavre_openclaw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,422 @@
1
+ require "faraday"
2
+ require "json"
3
+
4
+ module CollavreOpenclaw
5
+ class OpenclawAdapter
6
+ # Adapter for OpenClaw AI Gateway
7
+ # Uses OpenAI-compatible /v1/chat/completions endpoint
8
+ #
9
+ # Session mapping:
10
+ # Collavre Topic → OpenClaw Session (1:1)
11
+ # Same Topic, multiple users → shared context
12
+ # Different Topics → isolated sessions
13
+
14
+ def initialize(user:, system_prompt:, context: {})
15
+ @user = user
16
+ @system_prompt = system_prompt
17
+ @context = context
18
+ end
19
+
20
+ def chat(messages, tools: [], &block)
21
+ unless @user&.gateway_url.present?
22
+ Rails.logger.error("[CollavreOpenclaw] No Gateway URL configured for user #{@user&.id}")
23
+ yield "Error: OpenClaw Gateway URL not configured" if block_given?
24
+ return nil
25
+ end
26
+
27
+ response_content = +""
28
+
29
+ begin
30
+ # Build the request payload (OpenAI format)
31
+ payload = build_payload(messages, tools)
32
+
33
+ Rails.logger.info("[CollavreOpenclaw] Sending request to #{api_endpoint} (session: #{session_key})")
34
+
35
+ # Make streaming request to OpenClaw
36
+ stream_response(payload) do |chunk|
37
+ response_content << chunk
38
+ yield chunk if block_given?
39
+ end
40
+
41
+ response_content.presence
42
+ rescue StandardError => e
43
+ Rails.logger.error("[CollavreOpenclaw] Chat error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
44
+ error_msg = "OpenClaw Error: #{e.message}"
45
+ yield error_msg if block_given?
46
+ nil
47
+ end
48
+ end
49
+
50
+ # Get the callback URL for this user
51
+ def callback_url
52
+ return nil unless @user
53
+
54
+ host_options = default_url_options
55
+ return nil if host_options[:host].blank?
56
+
57
+ CollavreOpenclaw::Engine.routes.url_helpers.callback_url(
58
+ user_id: @user.id,
59
+ **host_options
60
+ )
61
+ rescue StandardError => e
62
+ Rails.logger.warn("[CollavreOpenclaw] Failed to generate callback URL: #{e.message}")
63
+ nil
64
+ end
65
+
66
+ # Get the stable session key for this context
67
+ def session_key
68
+ @session_key ||= build_session_key
69
+ end
70
+
71
+ private
72
+
73
+ def api_endpoint
74
+ # OpenClaw uses /v1/chat/completions (OpenAI-compatible)
75
+ uri = URI.parse(@user.gateway_url)
76
+ uri.path = "/v1/chat/completions"
77
+ uri.to_s
78
+ end
79
+
80
+ # Build stable session key based on Topic (not nonce)
81
+ # Same Topic = Same Session = Shared context between users
82
+ # Format: agent:<agent_id>:collavre:<user_id>:creative:<id>:topic:<id>
83
+ def build_session_key
84
+ creative_id = extract_id(@context, :creative) || @context[:creative_id]
85
+ topic_id = @context[:thread_id] || @context[:topic_id]
86
+ agent_id = extract_agent_id_from_email || "main"
87
+
88
+ parts = [ "agent", agent_id, "collavre", @user.id ]
89
+ parts << "creative:#{creative_id}" if creative_id
90
+ parts << "topic:#{topic_id}" if topic_id
91
+
92
+ parts.join(":")
93
+ end
94
+
95
+ def build_payload(messages, tools)
96
+ # Build model string with agent_id derived from user email
97
+ # OpenClaw accepts "openclaw:<agentId>" format (e.g., "openclaw:collavre")
98
+ agent_id = extract_agent_id_from_email
99
+ model_value = agent_id.present? ? "openclaw:#{agent_id}" : "openclaw"
100
+
101
+ payload = {
102
+ model: model_value,
103
+ messages: format_messages(messages),
104
+ stream: true
105
+ }
106
+
107
+ # Add system prompt as first message if present
108
+ if @system_prompt.present?
109
+ payload[:messages].unshift({ role: "system", content: @system_prompt })
110
+ end
111
+
112
+ # Add tools if provided (convert to OpenAI function calling format)
113
+ if tools.present?
114
+ payload[:tools] = format_tools(tools)
115
+ end
116
+
117
+ # Build user context with callback information
118
+ payload[:user] = build_user_context
119
+
120
+ payload
121
+ end
122
+
123
+ # Convert tools to OpenAI function calling format
124
+ # Accepts either:
125
+ # - Array of tool names (strings): ["meta_tool", "search"]
126
+ # - Array of OpenAI-format tool objects (already formatted)
127
+ def format_tools(tools)
128
+ Array(tools).filter_map do |tool|
129
+ if tool.is_a?(String)
130
+ # Tool name - fetch from MCP and convert to OpenAI format
131
+ convert_tool_name_to_openai_format(tool)
132
+ elsif tool.is_a?(Hash)
133
+ # Already a hash - check if it's OpenAI format or needs conversion
134
+ if tool[:type] == "function" || tool["type"] == "function"
135
+ # Already OpenAI format
136
+ tool
137
+ else
138
+ # MCP format - convert to OpenAI format
139
+ convert_mcp_tool_to_openai_format(tool)
140
+ end
141
+ end
142
+ end.compact
143
+ end
144
+
145
+ # Convert a tool name to OpenAI function format by fetching from MCP
146
+ def convert_tool_name_to_openai_format(tool_name)
147
+ return nil unless defined?(::Tools::MetaToolService)
148
+
149
+ result = ::Tools::MetaToolService.new.call(action: "get", tool_name: tool_name, query: nil, arguments: nil)
150
+ return nil if result[:error] || result[:tool].nil?
151
+
152
+ convert_mcp_tool_to_openai_format(result[:tool])
153
+ rescue StandardError => e
154
+ Rails.logger.warn("[CollavreOpenclaw] Failed to fetch tool #{tool_name}: #{e.message}")
155
+ nil
156
+ end
157
+
158
+ # Convert MCP tool format to OpenAI function format
159
+ # MCP format: { name:, description:, params: [...], return_type: }
160
+ # OpenAI format: { type: "function", function: { name:, description:, parameters: { type: "object", properties:, required: } } }
161
+ def convert_mcp_tool_to_openai_format(mcp_tool)
162
+ name = mcp_tool[:name] || mcp_tool["name"]
163
+ description = mcp_tool[:description] || mcp_tool["description"]
164
+ params = mcp_tool[:params] || mcp_tool["params"] || mcp_tool[:parameters] || mcp_tool["parameters"] || []
165
+
166
+ properties = {}
167
+ required = []
168
+
169
+ Array(params).each do |param|
170
+ param_name = (param[:name] || param["name"]).to_s
171
+ param_type = param[:type] || param["type"] || "string"
172
+ param_desc = param[:description] || param["description"]
173
+ param_required = param[:required] || param["required"]
174
+
175
+ # Convert Ruby/MCP types to JSON Schema types
176
+ json_type = case param_type.to_s.downcase
177
+ when "integer", "int" then "integer"
178
+ when "number", "float", "decimal" then "number"
179
+ when "boolean", "bool" then "boolean"
180
+ when "array" then "array"
181
+ when "object", "hash" then "object"
182
+ else "string"
183
+ end
184
+
185
+ properties[param_name] = { type: json_type }
186
+ properties[param_name][:description] = param_desc if param_desc.present?
187
+
188
+ required << param_name if param_required
189
+ end
190
+
191
+ {
192
+ type: "function",
193
+ function: {
194
+ name: name,
195
+ description: description || "",
196
+ parameters: {
197
+ type: "object",
198
+ properties: properties,
199
+ required: required
200
+ }
201
+ }
202
+ }
203
+ end
204
+
205
+ def build_user_context
206
+ context_data = {}
207
+
208
+ # Extract IDs from context
209
+ creative_id = extract_id(@context, :creative) || @context[:creative_id]
210
+ comment_id = extract_id(@context, :comment) || @context[:comment_id]
211
+ topic_id = @context[:thread_id] || @context[:topic_id]
212
+
213
+ # Create pending callback with nonce for secure async responses
214
+ callback = callback_url
215
+ if callback.present? && creative_id.present?
216
+ pending = PendingCallback.create_for_request(
217
+ user: @user,
218
+ creative_id: creative_id,
219
+ comment_id: comment_id,
220
+ thread_id: topic_id,
221
+ context: @context.slice(:extra_data).to_h
222
+ )
223
+
224
+ context_data[:callback_url] = callback
225
+ context_data[:callback_nonce] = pending.nonce
226
+ context_data[:creative_id] = creative_id
227
+ context_data[:comment_id] = comment_id if comment_id
228
+ context_data[:topic_id] = topic_id if topic_id
229
+
230
+ Rails.logger.info("[CollavreOpenclaw] Created pending callback with nonce: #{pending.nonce[0..8]}...")
231
+ end
232
+
233
+ # Return as JSON string (OpenAI user field format)
234
+ if context_data.any?
235
+ "collavre:#{JSON.generate(context_data)}"
236
+ else
237
+ "collavre:#{@user.id}"
238
+ end
239
+ end
240
+
241
+ # Format messages with sender attribution for multi-user context
242
+ def format_messages(messages)
243
+ Array(messages).map do |msg|
244
+ role = msg[:role] || msg["role"]
245
+ parts = msg[:parts] || msg["parts"]
246
+ content = if parts
247
+ Array(parts).map { |p| p[:text] || p["text"] }.compact.join("\n")
248
+ else
249
+ msg[:text] || msg["text"] || msg[:content] || msg["content"]
250
+ end
251
+
252
+ # Add sender attribution for user messages (multi-user support)
253
+ sender_name = msg[:sender_name] || msg["sender_name"]
254
+ if sender_name.present? && normalize_role(role) == "user"
255
+ content = "[#{sender_name}]: #{content}"
256
+ end
257
+
258
+ { role: normalize_role(role), content: content.to_s }
259
+ end
260
+ end
261
+
262
+ def normalize_role(role)
263
+ case role.to_s
264
+ when "model", "assistant" then "assistant"
265
+ when "system" then "system"
266
+ else "user"
267
+ end
268
+ end
269
+
270
+ def build_headers
271
+ headers = {
272
+ "Content-Type" => "application/json",
273
+ "Accept" => "text/event-stream",
274
+ "x-openclaw-session-key" => session_key
275
+ }
276
+
277
+ # Add Authorization header if API key is configured
278
+ if @user&.llm_api_key.present?
279
+ headers["Authorization"] = "Bearer #{@user.llm_api_key}"
280
+ end
281
+
282
+ headers
283
+ end
284
+
285
+ def stream_response(payload, &block)
286
+ retries = 0
287
+ max_retries = CollavreOpenclaw.config.max_retries
288
+
289
+ begin
290
+ connection = build_connection
291
+ buffer = +""
292
+ request_headers = build_headers
293
+
294
+ response = connection.post do |req|
295
+ req.url api_endpoint
296
+ request_headers.each { |k, v| req.headers[k] = v }
297
+
298
+ req.body = payload.to_json
299
+
300
+ req.options.on_data = proc do |chunk, _size, _env|
301
+ buffer << chunk
302
+ process_sse_buffer(buffer, &block)
303
+ end
304
+ end
305
+
306
+ # Process any remaining data in buffer
307
+ process_sse_buffer(buffer, final: true, &block)
308
+
309
+ # Handle non-streaming response
310
+ if response.headers["content-type"]&.include?("application/json")
311
+ handle_json_response(response.body, &block)
312
+ end
313
+
314
+ response
315
+ rescue Faraday::TimeoutError => e
316
+ retries += 1
317
+ if retries <= max_retries
318
+ Rails.logger.warn("[CollavreOpenclaw] Request timed out, retrying (#{retries}/#{max_retries})...")
319
+ sleep(1 * retries) # Exponential backoff
320
+ retry
321
+ end
322
+ raise "OpenClaw request timed out after #{max_retries + 1} attempts (read_timeout: #{CollavreOpenclaw.config.read_timeout}s)"
323
+ rescue Faraday::ConnectionFailed => e
324
+ retries += 1
325
+ if retries <= max_retries
326
+ Rails.logger.warn("[CollavreOpenclaw] Connection failed, retrying (#{retries}/#{max_retries})...")
327
+ sleep(1 * retries)
328
+ retry
329
+ end
330
+ raise "Failed to connect to OpenClaw after #{max_retries + 1} attempts: #{e.message}"
331
+ end
332
+ end
333
+
334
+ def handle_json_response(body, &block)
335
+ json = JSON.parse(body, symbolize_names: true)
336
+ content = json.dig(:choices, 0, :message, :content)
337
+ yield content if content.present? && block_given?
338
+ rescue JSON::ParserError
339
+ # Ignore
340
+ end
341
+
342
+ def process_sse_buffer(buffer, final: false, &block)
343
+ while (idx = buffer.index("\n\n"))
344
+ event_data = buffer.slice!(0, idx + 2)
345
+ parse_sse_event(event_data, &block)
346
+ end
347
+
348
+ if final && buffer.present?
349
+ parse_sse_event(buffer, &block)
350
+ buffer.clear
351
+ end
352
+ end
353
+
354
+ def parse_sse_event(event_str, &block)
355
+ event_str.each_line do |line|
356
+ line = line.strip
357
+ next if line.empty? || line.start_with?(":")
358
+
359
+ if line.start_with?("data:")
360
+ data = line.sub(/^data:\s*/, "")
361
+ next if data == "[DONE]"
362
+
363
+ begin
364
+ json = JSON.parse(data, symbolize_names: true)
365
+ content = extract_content(json)
366
+ yield content if content.present?
367
+ rescue JSON::ParserError
368
+ yield data if data.present?
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ def extract_content(json)
375
+ # OpenAI streaming format
376
+ json.dig(:choices, 0, :delta, :content) ||
377
+ json.dig(:choices, 0, :message, :content) ||
378
+ json[:content] ||
379
+ json[:text]
380
+ end
381
+
382
+ def build_connection
383
+ Faraday.new do |builder|
384
+ builder.options.timeout = CollavreOpenclaw.config.read_timeout # Read timeout (3 min default)
385
+ builder.options.open_timeout = CollavreOpenclaw.config.open_timeout # Connection timeout (10s)
386
+ builder.adapter Faraday.default_adapter
387
+ end
388
+ end
389
+
390
+ def extract_id(context, key)
391
+ value = context[key] || context[key.to_s]
392
+ return nil unless value
393
+
394
+ return value.id if value.respond_to?(:id)
395
+ value[:id] || value["id"]
396
+ end
397
+
398
+ # Extract agent_id from user email
399
+ # e.g., "ai-agent@collavre.com" -> "ai-agent"
400
+ def extract_agent_id_from_email
401
+ return nil unless @user&.email.present?
402
+
403
+ # Extract local part (before @) from email
404
+ @user.email.split("@").first
405
+ end
406
+
407
+ def default_url_options
408
+ options = Rails.application.config.action_mailer.default_url_options || {}
409
+
410
+ host = options[:host]
411
+ host ||= Rails.application.config.action_controller.default_url_options&.dig(:host)
412
+ host ||= ENV["APP_HOST"]
413
+ host ||= ENV["RAILS_HOST"]
414
+
415
+ result = { host: host }
416
+ result[:protocol] = options[:protocol] || "https"
417
+ result[:port] = options[:port] if options[:port].present?
418
+
419
+ result
420
+ end
421
+ end
422
+ end
@@ -0,0 +1,15 @@
1
+ Rails.application.config.to_prepare do
2
+ if defined?(Collavre::AiClient)
3
+ # Prepend the extension if not already done
4
+ unless Collavre::AiClient.singleton_class.method_defined?(:register_adapter)
5
+ Collavre::AiClient.prepend(CollavreOpenclaw::AiClientExtension)
6
+ Rails.logger.info("[CollavreOpenclaw] Extended Collavre::AiClient with adapter support")
7
+ end
8
+
9
+ # Register the OpenClaw adapter
10
+ unless Collavre::AiClient.adapter_registry.key?("openclaw")
11
+ Collavre::AiClient.register_adapter("openclaw", CollavreOpenclaw::OpenclawAdapter)
12
+ Rails.logger.info("[CollavreOpenclaw] Registered OpenClaw adapter")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ en:
2
+ collavre_openclaw:
3
+ errors:
4
+ user_not_found: "User not found"
5
+ unauthorized: "You are not authorized to perform this action"
6
+ connection_failed: "Failed to connect to OpenClaw gateway"
@@ -0,0 +1,6 @@
1
+ ko:
2
+ collavre_openclaw:
3
+ errors:
4
+ user_not_found: "사용자를 찾을 수 없습니다"
5
+ unauthorized: "이 작업을 수행할 권한이 없습니다"
6
+ connection_failed: "OpenClaw 게이트웨이에 연결하지 못했습니다"
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ CollavreOpenclaw::Engine.routes.draw do
2
+ # Callback endpoint for async responses from OpenClaw
3
+ post "/callback/:user_id", to: "callbacks#create", as: :callback
4
+
5
+ # Health check endpoint
6
+ get "/health", to: "health#show"
7
+ end
@@ -0,0 +1,14 @@
1
+ class CreateOpenclawAccounts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :openclaw_accounts do |t|
4
+ t.references :user, null: false, foreign_key: { to_table: :users }, index: { unique: true }
5
+ t.string :gateway_url, null: false
6
+ t.string :webhook_secret
7
+ t.string :api_token
8
+ t.string :channel_id
9
+ t.text :description
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ class CreatePendingCallbacks < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :openclaw_pending_callbacks do |t|
4
+ t.references :openclaw_account, null: false, foreign_key: { to_table: :openclaw_accounts }
5
+ t.string :nonce, null: false, index: { unique: true }
6
+ t.integer :creative_id
7
+ t.integer :comment_id
8
+ t.integer :thread_id
9
+ t.text :context # Use text for SQLite compatibility, serialize in model
10
+ t.datetime :expires_at, null: false
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :openclaw_pending_callbacks, :expires_at
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ class RemoveWebhookSecretFromOpenclawAccounts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ remove_column :openclaw_accounts, :webhook_secret, :string
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ class AddAgentIdToOpenclawAccounts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :openclaw_accounts, :agent_id, :string
4
+ add_column :openclaw_accounts, :agent_provisioned_at, :datetime
5
+ end
6
+ end
@@ -0,0 +1,28 @@
1
+ module CollavreOpenclaw
2
+ class Configuration
3
+ # Connection timeout (seconds) - how long to wait for connection
4
+ attr_accessor :open_timeout
5
+
6
+ # Read timeout (seconds) - how long to wait for streaming response
7
+ # AI responses can take 60-180+ seconds with reasoning/tools
8
+ attr_accessor :read_timeout
9
+
10
+ # Max retries for transient failures
11
+ attr_accessor :max_retries
12
+
13
+ def initialize
14
+ @open_timeout = ENV.fetch("OPENCLAW_OPEN_TIMEOUT", 10).to_i
15
+ @read_timeout = ENV.fetch("OPENCLAW_READ_TIMEOUT", 180).to_i # 3 minutes for AI responses
16
+ @max_retries = ENV.fetch("OPENCLAW_MAX_RETRIES", 2).to_i
17
+ end
18
+
19
+ # Legacy accessor for backward compatibility
20
+ def request_timeout
21
+ @read_timeout
22
+ end
23
+
24
+ def request_timeout=(value)
25
+ @read_timeout = value
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ module CollavreOpenclaw
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace CollavreOpenclaw
4
+
5
+ config.generators do |g|
6
+ g.test_framework :minitest
7
+ end
8
+
9
+ # Path to engine's JavaScript sources for jsbundling-rails integration
10
+ def self.javascript_path
11
+ root.join("app/javascript")
12
+ end
13
+
14
+ # Path to engine's stylesheet sources
15
+ def self.stylesheet_path
16
+ root.join("app/assets/stylesheets")
17
+ end
18
+
19
+ # Load locale files
20
+ config.i18n.load_path += Dir[root.join("config", "locales", "*.yml")]
21
+
22
+ # Auto-mount engine routes
23
+ initializer "collavre_openclaw.routes", before: :add_routing_paths do |app|
24
+ app.routes.append do
25
+ mount CollavreOpenclaw::Engine => "/openclaw", as: :openclaw_engine
26
+ end
27
+ end
28
+
29
+ # Add engine stylesheets to asset paths for Propshaft
30
+ initializer "collavre_openclaw.assets" do |app|
31
+ if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:paths)
32
+ app.config.assets.paths << root.join("app/assets/stylesheets")
33
+ end
34
+ end
35
+
36
+ initializer "collavre_openclaw.migrations" do |app|
37
+ unless app.root.to_s.match?(root.to_s)
38
+ config.paths["db/migrate"].expanded.each do |expanded_path|
39
+ app.config.paths["db/migrate"] << expanded_path
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module CollavreOpenclaw
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,13 @@
1
+ require "collavre_openclaw/version"
2
+ require "collavre_openclaw/configuration"
3
+ require "collavre_openclaw/engine"
4
+
5
+ module CollavreOpenclaw
6
+ def self.config
7
+ @config ||= Configuration.new
8
+ end
9
+
10
+ def self.configure
11
+ yield(config)
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: collavre_openclaw
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Collavre
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '8.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ description: Enables AI agents in Collavre to use OpenClaw as their LLM backend
41
+ email:
42
+ - support@collavre.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - README.md
48
+ - Rakefile
49
+ - app/controllers/collavre_openclaw/application_controller.rb
50
+ - app/controllers/collavre_openclaw/callbacks_controller.rb
51
+ - app/controllers/collavre_openclaw/health_controller.rb
52
+ - app/jobs/collavre_openclaw/application_job.rb
53
+ - app/jobs/collavre_openclaw/callback_processor_job.rb
54
+ - app/models/collavre_openclaw/application_record.rb
55
+ - app/models/collavre_openclaw/pending_callback.rb
56
+ - app/services/collavre_openclaw/ai_client_extension.rb
57
+ - app/services/collavre_openclaw/openclaw_adapter.rb
58
+ - config/initializers/ai_client_extension.rb
59
+ - config/locales/en.yml
60
+ - config/locales/ko.yml
61
+ - config/routes.rb
62
+ - db/migrate/20260131000001_create_openclaw_accounts.rb
63
+ - db/migrate/20260201000001_create_pending_callbacks.rb
64
+ - db/migrate/20260202000001_remove_webhook_secret_from_openclaw_accounts.rb
65
+ - db/migrate/20260202074949_add_agent_id_to_openclaw_accounts.rb
66
+ - lib/collavre_openclaw.rb
67
+ - lib/collavre_openclaw/configuration.rb
68
+ - lib/collavre_openclaw/engine.rb
69
+ - lib/collavre_openclaw/version.rb
70
+ homepage: https://github.com/sh1nj1/plan42
71
+ licenses:
72
+ - AGPL-3.0
73
+ metadata:
74
+ homepage_uri: https://github.com/sh1nj1/plan42
75
+ source_code_uri: https://github.com/sh1nj1/plan42
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.6.7
91
+ specification_version: 4
92
+ summary: OpenClaw AI Gateway integration for Collavre
93
+ test_files: []