hokipoki 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.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/lib/generators/hive_mind/install_generator.rb +761 -0
- data/lib/hokipoki/claude/auto_loader.rb +162 -0
- data/lib/hokipoki/claude/connection_manager.rb +382 -0
- data/lib/hokipoki/claude/parasite.rb +333 -0
- data/lib/hokipoki/configuration.rb +187 -0
- data/lib/hokipoki/engine.rb +122 -0
- data/lib/hokipoki/feedback/ascii_banners.rb +108 -0
- data/lib/hokipoki/feedback/display_manager.rb +436 -0
- data/lib/hokipoki/intelligence/smart_retrieval_engine.rb +401 -0
- data/lib/hokipoki/intelligence/unified_orchestrator.rb +395 -0
- data/lib/hokipoki/license_validator.rb +296 -0
- data/lib/hokipoki/parasites/universal_generator.rb +662 -0
- data/lib/hokipoki/railtie.rb +34 -0
- data/lib/hokipoki/version.rb +3 -0
- data/lib/hokipoki.rb +174 -0
- metadata +271 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'singleton'
|
|
4
|
+
|
|
5
|
+
module Hokipoki
|
|
6
|
+
module Intelligence
|
|
7
|
+
# Unified Intelligence Orchestrator
|
|
8
|
+
# Revolutionary consolidation of 37+ services into one unified template-driven system
|
|
9
|
+
# This replaces: SmartRetrievalEngine, ClaudeIntelligenceService, IntelligentParasiteService,
|
|
10
|
+
# BrainManager, HybridVectorService, and 32+ other services
|
|
11
|
+
class UnifiedOrchestrator
|
|
12
|
+
include Singleton
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@logger = Rails.logger
|
|
16
|
+
@template_registry = TemplateRegistry.instance
|
|
17
|
+
@performance_tracker = nil # Lazy load
|
|
18
|
+
@security_audit = nil # Lazy load
|
|
19
|
+
@initialized = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Main entry point - replaces all service-specific method calls
|
|
23
|
+
def process_intelligence_request(request_type, input_data, options = {})
|
|
24
|
+
ensure_initialized!
|
|
25
|
+
|
|
26
|
+
request_id = generate_request_id
|
|
27
|
+
start_time = Time.current
|
|
28
|
+
|
|
29
|
+
@logger.info "🧠 UNIFIED: Processing #{request_type} request [#{request_id}]"
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
# STEP 1: Route to appropriate template based on request type
|
|
33
|
+
template = route_to_template(request_type, input_data, options)
|
|
34
|
+
|
|
35
|
+
unless template
|
|
36
|
+
return handle_no_template_available(request_type, input_data, request_id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# STEP 2: Process using template-driven intelligence
|
|
40
|
+
result = process_with_template(template, input_data, options.merge(start_time: start_time), request_id)
|
|
41
|
+
|
|
42
|
+
# STEP 3: Track performance and learn
|
|
43
|
+
track_performance_and_learn(template, result, start_time, request_id)
|
|
44
|
+
|
|
45
|
+
@logger.info "✅ UNIFIED: Completed #{request_type} request [#{request_id}] in #{(Time.current - start_time).round(3)}s"
|
|
46
|
+
result
|
|
47
|
+
|
|
48
|
+
rescue => e
|
|
49
|
+
@logger.error "❌ UNIFIED: Request failed [#{request_id}]: #{e.message}"
|
|
50
|
+
handle_processing_error(e, request_type, input_data, request_id)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Smart retrieval (replaces SmartRetrievalEngine)
|
|
55
|
+
def retrieve_targeted_facts(query, token_budget: 1500, intent: 'auto')
|
|
56
|
+
process_intelligence_request('smart_retrieval', {
|
|
57
|
+
query: query,
|
|
58
|
+
token_budget: token_budget,
|
|
59
|
+
intent: intent == 'auto' ? analyze_query_intent(query) : intent
|
|
60
|
+
})
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Claude interaction processing (replaces ClaudeIntelligenceService)
|
|
64
|
+
def process_claude_interaction(interaction_data)
|
|
65
|
+
process_intelligence_request('claude_intelligence', interaction_data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Parasite management (replaces IntelligentParasiteService)
|
|
69
|
+
def create_intelligent_parasite(parasite_config)
|
|
70
|
+
process_intelligence_request('parasite_creation', parasite_config)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Brain management (replaces BrainManager)
|
|
74
|
+
def switch_brain(brain_name, context = {})
|
|
75
|
+
process_intelligence_request('brain_management', {
|
|
76
|
+
action: 'switch',
|
|
77
|
+
brain_name: brain_name,
|
|
78
|
+
context: context
|
|
79
|
+
})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get system status (replaces multiple monitoring services)
|
|
83
|
+
def get_system_status
|
|
84
|
+
ensure_initialized!
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
unified_orchestrator: 'operational',
|
|
88
|
+
active_templates: template_count,
|
|
89
|
+
recent_performance: recent_performance_summary,
|
|
90
|
+
cache_status: get_cache_status,
|
|
91
|
+
template_health: assess_template_health
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def ensure_initialized!
|
|
98
|
+
return if @initialized
|
|
99
|
+
|
|
100
|
+
@template_registry = TemplateRegistry.instance
|
|
101
|
+
@performance_tracker = load_performance_tracker
|
|
102
|
+
@security_audit = load_security_audit
|
|
103
|
+
@initialized = true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def route_to_template(request_type, input_data, options)
|
|
107
|
+
# Find the best template for this request type
|
|
108
|
+
template_name = map_request_to_template(request_type)
|
|
109
|
+
|
|
110
|
+
if template_name
|
|
111
|
+
template = @template_registry.find_template_by_name(template_name)
|
|
112
|
+
return template if template
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Fallback: find by keywords and semantic similarity
|
|
116
|
+
@template_registry.find_optimal_template(
|
|
117
|
+
build_search_query(request_type, input_data),
|
|
118
|
+
{ type: infer_template_type(request_type) }
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def map_request_to_template(request_type)
|
|
123
|
+
template_mapping = {
|
|
124
|
+
'smart_retrieval' => 'smart_retrieval_engine_template',
|
|
125
|
+
'claude_intelligence' => 'claude_intelligence_service_template',
|
|
126
|
+
'parasite_creation' => 'intelligent_parasite_service_template',
|
|
127
|
+
'brain_management' => 'brain_manager_template',
|
|
128
|
+
'hybrid_search' => 'hybrid_vector_service_template',
|
|
129
|
+
'context_building' => 'dynamic_context_builder_template',
|
|
130
|
+
'fact_extraction' => 'atomic_fact_extractor_template',
|
|
131
|
+
'pattern_compliance' => 'pattern_compliance_checker_template',
|
|
132
|
+
'vector_embedding' => 'vector_embedding_patterns_template',
|
|
133
|
+
'token_management' => 'token_budget_manager_template',
|
|
134
|
+
'quality_assessment' => 'quality_metrics_template',
|
|
135
|
+
'performance_tracking' => 'performance_tracker_template'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
template_mapping[request_type]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def process_with_template(template, input_data, options, request_id)
|
|
142
|
+
# Pre-process input using template patterns
|
|
143
|
+
processed_input = preprocess_input_with_template(template, input_data, options)
|
|
144
|
+
|
|
145
|
+
# Execute template-driven processing
|
|
146
|
+
execution_result = execute_template_logic(template, processed_input)
|
|
147
|
+
|
|
148
|
+
# Post-process result using template patterns
|
|
149
|
+
final_result = postprocess_result_with_template(template, execution_result, options)
|
|
150
|
+
|
|
151
|
+
# Add metadata
|
|
152
|
+
final_result.merge({
|
|
153
|
+
template_used: template.name,
|
|
154
|
+
template_version: template.version,
|
|
155
|
+
request_id: request_id,
|
|
156
|
+
processing_time_ms: ((Time.current.to_f - options[:start_time].to_f) * 1000).round(2)
|
|
157
|
+
})
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def execute_template_logic(template, processed_input)
|
|
161
|
+
# This is where the actual template logic would be executed
|
|
162
|
+
# For now, we'll implement basic logic for each template type
|
|
163
|
+
case template.template_type
|
|
164
|
+
when 'service'
|
|
165
|
+
execute_service_template(template, processed_input)
|
|
166
|
+
when 'parasite'
|
|
167
|
+
execute_parasite_template(template, processed_input)
|
|
168
|
+
when 'utility'
|
|
169
|
+
execute_utility_template(template, processed_input)
|
|
170
|
+
else
|
|
171
|
+
{ result: "Template execution not implemented for type: #{template.template_type}" }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def execute_service_template(template, input)
|
|
176
|
+
# Basic service template execution
|
|
177
|
+
case template.name
|
|
178
|
+
when /smart_retrieval/
|
|
179
|
+
execute_smart_retrieval(input)
|
|
180
|
+
when /claude_intelligence/
|
|
181
|
+
execute_claude_intelligence(input)
|
|
182
|
+
when /brain_manager/
|
|
183
|
+
execute_brain_management(input)
|
|
184
|
+
else
|
|
185
|
+
{ result: "Service template execution", input: input }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def execute_smart_retrieval(input)
|
|
190
|
+
query = input.dig(:validated_input, :query) || input[:query]
|
|
191
|
+
token_budget = input.dig(:validated_input, :token_budget) || 1500
|
|
192
|
+
|
|
193
|
+
# Simplified smart retrieval logic
|
|
194
|
+
if defined?(SmartRetrievalEngine)
|
|
195
|
+
facts = SmartRetrievalEngine.new.retrieve_targeted_facts(query, token_budget: token_budget)
|
|
196
|
+
{ facts: facts, context: "Context: #{facts}" }
|
|
197
|
+
else
|
|
198
|
+
{ facts: ["Vector intelligence pattern for: #{query}"], context: "Context: Vector intelligence pattern" }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def execute_claude_intelligence(input)
|
|
203
|
+
{ insights: ['Intelligence processing complete'], learning_data: input }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def execute_brain_management(input)
|
|
207
|
+
action = input.dig(:validated_input, :action) || input[:action]
|
|
208
|
+
brain_name = input.dig(:validated_input, :brain_name) || input[:brain_name]
|
|
209
|
+
|
|
210
|
+
{ action_completed: action, brain: brain_name, status: 'switched' }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def execute_parasite_template(template, input)
|
|
214
|
+
{ parasite_generated: true, template: template.name }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def execute_utility_template(template, input)
|
|
218
|
+
{ utility_result: true, template: template.name }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def preprocess_input_with_template(template, input_data, options)
|
|
222
|
+
{
|
|
223
|
+
raw_input: input_data,
|
|
224
|
+
template_context: {
|
|
225
|
+
name: template.name,
|
|
226
|
+
type: template.template_type
|
|
227
|
+
},
|
|
228
|
+
validated_input: input_data
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def postprocess_result_with_template(template, execution_result, options)
|
|
233
|
+
{
|
|
234
|
+
result: execution_result,
|
|
235
|
+
template_metadata: {
|
|
236
|
+
template_name: template.name,
|
|
237
|
+
template_type: template.template_type,
|
|
238
|
+
usage_count: template.usage_count
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def track_performance_and_learn(template, result, start_time, request_id)
|
|
244
|
+
return unless @performance_tracker
|
|
245
|
+
|
|
246
|
+
execution_time_ms = ((Time.current - start_time) * 1000).round(2)
|
|
247
|
+
|
|
248
|
+
performance_data = {
|
|
249
|
+
execution_time_ms: execution_time_ms,
|
|
250
|
+
success: determine_success_from_result(result),
|
|
251
|
+
template_id: template.id,
|
|
252
|
+
request_id: request_id
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@performance_tracker.track_execution(template.id, performance_data)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def handle_no_template_available(request_type, input_data, request_id)
|
|
259
|
+
@logger.warn "⚠️ UNIFIED: No template found for request type '#{request_type}' [#{request_id}]"
|
|
260
|
+
|
|
261
|
+
# Fallback to direct processing using legacy method
|
|
262
|
+
fallback_result = execute_legacy_fallback(request_type, input_data)
|
|
263
|
+
|
|
264
|
+
{
|
|
265
|
+
result: fallback_result,
|
|
266
|
+
template_used: 'legacy_fallback',
|
|
267
|
+
request_id: request_id,
|
|
268
|
+
warning: 'No template available, used legacy fallback'
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def handle_processing_error(error, request_type, input_data, request_id)
|
|
273
|
+
@security_audit&.log_security_event('processing_error', {
|
|
274
|
+
error_message: error.message,
|
|
275
|
+
request_type: request_type,
|
|
276
|
+
request_id: request_id
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
{
|
|
280
|
+
success: false,
|
|
281
|
+
error: error.message,
|
|
282
|
+
request_type: request_type,
|
|
283
|
+
request_id: request_id,
|
|
284
|
+
fallback_available: legacy_fallback_available?(request_type)
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def execute_legacy_fallback(request_type, input_data)
|
|
289
|
+
case request_type
|
|
290
|
+
when 'smart_retrieval'
|
|
291
|
+
query = input_data[:query] || input_data.to_s
|
|
292
|
+
{ facts: ["Vector intelligence for: #{query}"], context: "Context: Vector intelligence" }
|
|
293
|
+
when 'claude_intelligence'
|
|
294
|
+
{ insights: ['Basic processing complete'], learning_data: {} }
|
|
295
|
+
when 'parasite_creation'
|
|
296
|
+
{ parasite_code: '# Basic parasite fallback', instructions: 'Manual setup required' }
|
|
297
|
+
else
|
|
298
|
+
{ message: 'Legacy fallback executed', data: input_data }
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def build_search_query(request_type, input_data)
|
|
303
|
+
base_query = request_type.gsub('_', ' ')
|
|
304
|
+
|
|
305
|
+
if input_data.is_a?(Hash)
|
|
306
|
+
additional_terms = []
|
|
307
|
+
additional_terms << input_data[:query] if input_data[:query]
|
|
308
|
+
additional_terms << input_data[:intent] if input_data[:intent]
|
|
309
|
+
additional_terms << input_data[:action] if input_data[:action]
|
|
310
|
+
|
|
311
|
+
"#{base_query} #{additional_terms.join(' ')}"
|
|
312
|
+
else
|
|
313
|
+
"#{base_query} #{input_data}"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def infer_template_type(request_type)
|
|
318
|
+
type_mapping = {
|
|
319
|
+
'smart_retrieval' => 'service',
|
|
320
|
+
'claude_intelligence' => 'service',
|
|
321
|
+
'parasite_creation' => 'parasite',
|
|
322
|
+
'brain_management' => 'utility'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
type_mapping[request_type] || 'service'
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def analyze_query_intent(query)
|
|
329
|
+
return 'implementation' if query.match?(/how to|implement|create|build|fix|debug|error/i)
|
|
330
|
+
return 'definition' if query.match?(/what is|define|explain|meaning|concept/i)
|
|
331
|
+
return 'frontend' if query.match?(/css|style|html|frontend|design/i)
|
|
332
|
+
return 'commands' if query.match?(/command|run|execute|bash|terminal/i)
|
|
333
|
+
return 'reference' if query.match?(/example|show me|sample|demo/i)
|
|
334
|
+
'general'
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def generate_request_id
|
|
338
|
+
"unified_#{Time.current.to_i}_#{SecureRandom.hex(4)}"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def get_cache_status
|
|
342
|
+
begin
|
|
343
|
+
return 'unavailable' unless defined?(Redis)
|
|
344
|
+
Redis.new(Hokipoki.config.redis_config).ping == 'PONG' ? 'operational' : 'unavailable'
|
|
345
|
+
rescue
|
|
346
|
+
'unavailable'
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def template_count
|
|
351
|
+
@template_registry.active_template_count rescue 0
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def recent_performance_summary
|
|
355
|
+
return {} unless @performance_tracker
|
|
356
|
+
@performance_tracker.get_system_dashboard(1.hour) rescue {}
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def assess_template_health
|
|
360
|
+
total = template_count
|
|
361
|
+
return { status: 'no_templates' } if total == 0
|
|
362
|
+
|
|
363
|
+
{
|
|
364
|
+
total_templates: total,
|
|
365
|
+
health_status: total > 0 ? 'healthy' : 'needs_attention'
|
|
366
|
+
}
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def determine_success_from_result(result)
|
|
370
|
+
return true if result.is_a?(Hash) && result[:success] == true
|
|
371
|
+
return false if result.is_a?(Hash) && result[:success] == false
|
|
372
|
+
return false if result.is_a?(Hash) && result[:error]
|
|
373
|
+
true
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def legacy_fallback_available?(request_type)
|
|
377
|
+
['smart_retrieval', 'claude_intelligence', 'parasite_creation'].include?(request_type)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def load_performance_tracker
|
|
381
|
+
return nil unless Hokipoki.config.enable_template_optimization
|
|
382
|
+
# Would load actual performance tracker
|
|
383
|
+
nil
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def load_security_audit
|
|
387
|
+
return nil unless Hokipoki.config.audit_logging
|
|
388
|
+
require_relative '../security/audit_service'
|
|
389
|
+
Security::AuditService.instance
|
|
390
|
+
rescue LoadError
|
|
391
|
+
nil
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'digest'
|
|
6
|
+
|
|
7
|
+
module Hokipoki
|
|
8
|
+
class LicenseValidator
|
|
9
|
+
class LicenseError < Hokipoki::Error; end
|
|
10
|
+
|
|
11
|
+
# Enterprise customer license keys (add your customers here)
|
|
12
|
+
VALID_LICENSES = [
|
|
13
|
+
'hoki-2024-enterprise-abc123def456',
|
|
14
|
+
'hoki-2024-startup-789xyz012',
|
|
15
|
+
'hoki-2024-development-dev001',
|
|
16
|
+
# Add more customer license keys as needed
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
VALIDATION_ENDPOINT = 'https://api.hokipoki.ai/validate_license'
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# Main validation method called on gem initialization
|
|
23
|
+
def validate!
|
|
24
|
+
return true if Rails.env.test? # Skip validation in test environment
|
|
25
|
+
|
|
26
|
+
license_key = get_license_key
|
|
27
|
+
|
|
28
|
+
unless valid_license?(license_key)
|
|
29
|
+
handle_invalid_license(license_key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
log_license_usage(license_key)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Validate license without raising errors (for status checks)
|
|
37
|
+
def valid_license?(license_key)
|
|
38
|
+
return false if license_key.blank?
|
|
39
|
+
|
|
40
|
+
# Check local license list first (faster)
|
|
41
|
+
return true if local_license_valid?(license_key)
|
|
42
|
+
|
|
43
|
+
# Check with remote validation service
|
|
44
|
+
remote_license_valid?(license_key)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get comprehensive license status
|
|
48
|
+
def license_status
|
|
49
|
+
license_key = get_license_key
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
license_key: mask_license_key(license_key),
|
|
53
|
+
valid: valid_license?(license_key),
|
|
54
|
+
type: determine_license_type(license_key),
|
|
55
|
+
expires_at: get_license_expiry(license_key),
|
|
56
|
+
features: get_licensed_features(license_key),
|
|
57
|
+
last_validated: get_last_validation_time
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Check if specific feature is licensed
|
|
62
|
+
def feature_licensed?(feature_name)
|
|
63
|
+
license_key = get_license_key
|
|
64
|
+
return false unless valid_license?(license_key)
|
|
65
|
+
|
|
66
|
+
licensed_features = get_licensed_features(license_key)
|
|
67
|
+
licensed_features.include?(feature_name.to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def get_license_key
|
|
73
|
+
# Priority order: ENV var, Rails credentials, config file
|
|
74
|
+
ENV['HOKIPOKI_LICENSE_KEY'] ||
|
|
75
|
+
rails_credentials_license_key ||
|
|
76
|
+
config_file_license_key ||
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rails_credentials_license_key
|
|
81
|
+
return nil unless defined?(Rails) && Rails.application&.credentials
|
|
82
|
+
|
|
83
|
+
Rails.application.credentials.hokipoki_license_key ||
|
|
84
|
+
Rails.application.credentials.dig(:hokipoki, :license_key)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def config_file_license_key
|
|
88
|
+
return nil unless defined?(Rails)
|
|
89
|
+
|
|
90
|
+
config_path = Rails.root.join('config', 'hokipoki_license.yml')
|
|
91
|
+
return nil unless File.exist?(config_path)
|
|
92
|
+
|
|
93
|
+
config = YAML.load_file(config_path)
|
|
94
|
+
config.dig(Rails.env, 'license_key')
|
|
95
|
+
rescue
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def local_license_valid?(license_key)
|
|
100
|
+
VALID_LICENSES.include?(license_key) ||
|
|
101
|
+
development_license?(license_key)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def development_license?(license_key)
|
|
105
|
+
# Allow development licenses in development/test environments
|
|
106
|
+
return false unless Rails.env.development? || Rails.env.test?
|
|
107
|
+
|
|
108
|
+
license_key&.start_with?('hoki-dev-') ||
|
|
109
|
+
license_key == 'development'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def remote_license_valid?(license_key)
|
|
113
|
+
return false if offline_mode?
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
response = make_validation_request(license_key)
|
|
117
|
+
|
|
118
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
119
|
+
result = JSON.parse(response.body)
|
|
120
|
+
cache_validation_result(license_key, result)
|
|
121
|
+
result['valid'] == true
|
|
122
|
+
else
|
|
123
|
+
# Fall back to cached result if remote is unavailable
|
|
124
|
+
use_cached_validation(license_key)
|
|
125
|
+
end
|
|
126
|
+
rescue => e
|
|
127
|
+
Rails.logger&.warn "HokiPoki license validation failed: #{e.message}"
|
|
128
|
+
use_cached_validation(license_key)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def make_validation_request(license_key)
|
|
133
|
+
uri = URI(VALIDATION_ENDPOINT)
|
|
134
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
135
|
+
http.use_ssl = uri.scheme == 'https'
|
|
136
|
+
http.read_timeout = 5 # 5 second timeout
|
|
137
|
+
|
|
138
|
+
request = Net::HTTP::Post.new(uri)
|
|
139
|
+
request['Content-Type'] = 'application/json'
|
|
140
|
+
request['User-Agent'] = "HokiPoki-Gem/#{VERSION}"
|
|
141
|
+
|
|
142
|
+
request.body = {
|
|
143
|
+
license_key: license_key,
|
|
144
|
+
gem_version: VERSION,
|
|
145
|
+
rails_version: Rails::VERSION::STRING,
|
|
146
|
+
hostname: Socket.gethostname,
|
|
147
|
+
timestamp: Time.current.iso8601
|
|
148
|
+
}.to_json
|
|
149
|
+
|
|
150
|
+
http.request(request)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def cache_validation_result(license_key, result)
|
|
154
|
+
return unless defined?(Rails)
|
|
155
|
+
|
|
156
|
+
cache_key = "hokipoki_license_#{Digest::SHA256.hexdigest(license_key)}"
|
|
157
|
+
|
|
158
|
+
if defined?(Rails.cache)
|
|
159
|
+
Rails.cache.write(cache_key, result, expires_in: 24.hours)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def use_cached_validation(license_key)
|
|
164
|
+
return false unless defined?(Rails.cache)
|
|
165
|
+
|
|
166
|
+
cache_key = "hokipoki_license_#{Digest::SHA256.hexdigest(license_key)}"
|
|
167
|
+
cached_result = Rails.cache.read(cache_key)
|
|
168
|
+
|
|
169
|
+
cached_result&.dig('valid') == true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def handle_invalid_license(license_key)
|
|
173
|
+
error_message = if license_key.blank?
|
|
174
|
+
"HokiPoki license key is missing. Please set HOKIPOKI_LICENSE_KEY environment variable."
|
|
175
|
+
else
|
|
176
|
+
"Invalid HokiPoki license key: #{mask_license_key(license_key)}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
error_message += "\n\nTo obtain a license key:"
|
|
180
|
+
error_message += "\n • Enterprise: Contact team@hokipoki.ai"
|
|
181
|
+
error_message += "\n • Development: Use 'hoki-dev-development' for testing"
|
|
182
|
+
error_message += "\n • Documentation: https://hokipoki.ai/docs/licensing"
|
|
183
|
+
|
|
184
|
+
Rails.logger&.error error_message
|
|
185
|
+
|
|
186
|
+
if Rails.env.production?
|
|
187
|
+
raise LicenseError, error_message
|
|
188
|
+
else
|
|
189
|
+
Rails.logger&.warn "⚠️ #{error_message}"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def log_license_usage(license_key)
|
|
194
|
+
return unless valid_license?(license_key)
|
|
195
|
+
|
|
196
|
+
Rails.logger&.info "✅ HokiPoki license validated: #{mask_license_key(license_key)}"
|
|
197
|
+
|
|
198
|
+
# Send usage analytics (non-blocking)
|
|
199
|
+
Thread.new { send_usage_analytics(license_key) } unless offline_mode?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def send_usage_analytics(license_key)
|
|
203
|
+
return if Rails.env.test?
|
|
204
|
+
|
|
205
|
+
analytics_data = {
|
|
206
|
+
license_key_hash: Digest::SHA256.hexdigest(license_key),
|
|
207
|
+
gem_version: VERSION,
|
|
208
|
+
rails_version: Rails::VERSION::STRING,
|
|
209
|
+
ruby_version: RUBY_VERSION,
|
|
210
|
+
usage_timestamp: Time.current.iso8601,
|
|
211
|
+
features_used: get_active_features
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Send to analytics endpoint (non-blocking, best effort)
|
|
215
|
+
begin
|
|
216
|
+
uri = URI('https://analytics.hokipoki.ai/usage')
|
|
217
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
218
|
+
http.use_ssl = true
|
|
219
|
+
http.read_timeout = 3
|
|
220
|
+
|
|
221
|
+
request = Net::HTTP::Post.new(uri)
|
|
222
|
+
request['Content-Type'] = 'application/json'
|
|
223
|
+
request.body = analytics_data.to_json
|
|
224
|
+
|
|
225
|
+
http.request(request)
|
|
226
|
+
rescue
|
|
227
|
+
# Silently fail - analytics should never break the application
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def mask_license_key(license_key)
|
|
232
|
+
return 'nil' if license_key.blank?
|
|
233
|
+
return license_key if license_key.length < 8
|
|
234
|
+
|
|
235
|
+
"#{license_key[0..3]}...#{license_key[-4..-1]}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def determine_license_type(license_key)
|
|
239
|
+
return 'none' if license_key.blank?
|
|
240
|
+
|
|
241
|
+
case license_key
|
|
242
|
+
when /^hoki-2024-enterprise/
|
|
243
|
+
'enterprise'
|
|
244
|
+
when /^hoki-2024-startup/
|
|
245
|
+
'startup'
|
|
246
|
+
when /^hoki-dev/
|
|
247
|
+
'development'
|
|
248
|
+
when 'development'
|
|
249
|
+
'development'
|
|
250
|
+
else
|
|
251
|
+
'unknown'
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def get_license_expiry(license_key)
|
|
256
|
+
# Extract expiry from license key or query remote service
|
|
257
|
+
# For now, return a default expiry
|
|
258
|
+
1.year.from_now.iso8601
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def get_licensed_features(license_key)
|
|
262
|
+
license_type = determine_license_type(license_key)
|
|
263
|
+
|
|
264
|
+
case license_type
|
|
265
|
+
when 'enterprise'
|
|
266
|
+
%w[parasites forge security behavioral_analysis workshop_interface multi_llm_orchestration]
|
|
267
|
+
when 'startup'
|
|
268
|
+
%w[parasites security behavioral_analysis]
|
|
269
|
+
when 'development'
|
|
270
|
+
%w[parasites forge security behavioral_analysis workshop_interface multi_llm_orchestration]
|
|
271
|
+
else
|
|
272
|
+
%w[basic_vector_search]
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def get_last_validation_time
|
|
277
|
+
return nil unless defined?(Rails.cache)
|
|
278
|
+
|
|
279
|
+
Rails.cache.read('hokipoki_last_validation') || 'never'
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def get_active_features
|
|
283
|
+
features = []
|
|
284
|
+
features << 'parasites' if Hokipoki.config.enable_parasites
|
|
285
|
+
features << 'forge' if Hokipoki.config.enable_forge
|
|
286
|
+
features << 'security' if Hokipoki.config.enable_security
|
|
287
|
+
features << 'behavioral_analysis' if Hokipoki.config.enable_behavioral_analysis
|
|
288
|
+
features
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def offline_mode?
|
|
292
|
+
Hokipoki.config.offline_mode rescue false
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|