hokipoki 0.3.4 → 0.5.1

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,515 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/base'
5
+
6
+ module Hokipoki
7
+ module Generators
8
+ # Hokipoki Install Generator - Sets up core gem dependencies and OTP authentication
9
+ # Command: rails g hokipoki:install
10
+ class InstallGenerator < Rails::Generators::Base
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ class_option :skip_otp, type: :boolean, default: false,
14
+ desc: 'Skip OTP setup (not recommended for production)'
15
+
16
+ class_option :auth_provider, type: :string, default: 'github',
17
+ desc: 'Authentication provider (github, totp, both)'
18
+
19
+ class_option :force_install, type: :boolean, default: false,
20
+ desc: 'Force installation even if components exist'
21
+
22
+ def display_installation_banner
23
+ say "\nšŸ” HOKIPOKI INSTALLATION SYSTEM", :cyan
24
+ say "=" * 60, :cyan
25
+ say "Setting up parasitic intelligence framework...", :white
26
+ say "This will configure authentication, dependencies, and core components.", :yellow
27
+ say ""
28
+ end
29
+
30
+ def check_rails_environment
31
+ unless defined?(Rails)
32
+ say "āŒ Rails environment required", :red
33
+ say "Please run this generator from a Rails application directory.", :yellow
34
+ exit(1)
35
+ end
36
+
37
+ say "āœ… Rails detected: #{Rails.version} (#{Rails.env})", :green
38
+ say "šŸ“ Project: #{Rails.application.class.module_parent_name}", :white
39
+ say ""
40
+ end
41
+
42
+ def check_existing_installation
43
+ if File.exist?('config/initializers/hokipoki.rb') && !options[:force_install]
44
+ say "āš ļø Hokipoki already installed", :yellow
45
+ say "Use --force-install to reinstall", :white
46
+ say ""
47
+ return
48
+ end
49
+ end
50
+
51
+ def install_core_dependencies
52
+ say "šŸ“¦ INSTALLING CORE DEPENDENCIES:", :cyan
53
+
54
+ # Add required gems to Gemfile
55
+ gems_to_add = [
56
+ { name: 'rotp', version: '~> 7.0', comment: 'For TOTP generation/verification' },
57
+ { name: 'rqrcode', version: '~> 2.2', comment: 'For QR codes' },
58
+ { name: 'sqlite3', version: '~> 1.4', comment: 'For vector database' },
59
+ { name: 'pastel', version: '~> 0.8', comment: 'For colored console output' }
60
+ ]
61
+
62
+ if options[:auth_provider].include?('github')
63
+ gems_to_add << { name: 'omniauth-github', version: '~> 2.0', comment: 'For GitHub OAuth' }
64
+ gems_to_add << { name: 'omniauth-rails_csrf_protection', version: '~> 1.0', comment: 'CSRF protection for OAuth' }
65
+ end
66
+
67
+ add_gems_to_gemfile(gems_to_add)
68
+
69
+ say " āœ… Dependencies added to Gemfile", :green
70
+ say " šŸ“ Run: bundle install", :yellow
71
+ say ""
72
+ end
73
+
74
+ def setup_encryption_keys
75
+ say "šŸ” SETTING UP ENCRYPTION:", :cyan
76
+
77
+ # Generate Rails encryption key if not exists
78
+ if !Rails.application.credentials.secret_key_base
79
+ say " šŸ”‘ Generating Rails encryption keys...", :white
80
+
81
+ # Create master key if it doesn't exist
82
+ unless File.exist?('config/master.key')
83
+ master_key = SecureRandom.hex(32)
84
+ create_file 'config/master.key', master_key
85
+ say " āœ… Created config/master.key", :green
86
+ end
87
+
88
+ # Set up credentials for OTP
89
+ credentials_content = {
90
+ hokipoki: {
91
+ otp_encryption_key: SecureRandom.hex(32),
92
+ vector_encryption_key: SecureRandom.hex(32)
93
+ }
94
+ }
95
+
96
+ if options[:auth_provider].include?('github')
97
+ credentials_content[:hokipoki][:github] = {
98
+ client_id: 'your_github_client_id_here',
99
+ client_secret: 'your_github_client_secret_here'
100
+ }
101
+ end
102
+
103
+ # We'll prompt user to set up credentials manually
104
+ say " šŸ“ Credentials setup required - see installation steps below", :yellow
105
+ end
106
+
107
+ say " āœ… Encryption configuration ready", :green
108
+ say ""
109
+ end
110
+
111
+ def create_hokipoki_initializer
112
+ say "āš™ļø CREATING INITIALIZER:", :cyan
113
+
114
+ create_file "config/initializers/hokipoki.rb", <<~RUBY
115
+ # Hokipoki Configuration
116
+ # Generated by: rails g hokipoki:install
117
+
118
+ Hokipoki.configure do |config|
119
+ # Core settings
120
+ config.enabled = true
121
+ config.environment = Rails.env
122
+
123
+ # Authentication settings
124
+ config.require_otp = #{!options[:skip_otp]}
125
+ config.auth_provider = '#{options[:auth_provider]}'
126
+
127
+ # Vector database settings
128
+ config.vector_storage = :sqlite
129
+ config.vector_db_path = File.expand_path('~/.hokipoki/vectors.db')
130
+
131
+ # Parasite settings
132
+ config.parasite_mode = :auto_detect
133
+ config.claude_detection = :enhanced
134
+ config.visible_output = true
135
+
136
+ # Token budget management
137
+ config.max_context_tokens = 1500
138
+ config.max_response_tokens = 1000
139
+ config.total_token_budget = 3000
140
+
141
+ # Compression settings
142
+ config.template_compression = true
143
+ config.atomic_facts = true
144
+ config.compression_target = 0.75 # 75% reduction
145
+
146
+ # Security settings
147
+ config.encryption_key = Rails.application.credentials.dig(:hokipoki, :otp_encryption_key)
148
+ config.vector_encryption = Rails.application.credentials.dig(:hokipoki, :vector_encryption_key)
149
+
150
+ # GitHub OAuth (if enabled)
151
+ if config.auth_provider.include?('github')
152
+ config.github_client_id = Rails.application.credentials.dig(:hokipoki, :github, :client_id)
153
+ config.github_client_secret = Rails.application.credentials.dig(:hokipoki, :github, :client_secret)
154
+ end
155
+
156
+ # Development settings
157
+ if Rails.env.development?
158
+ config.debug_mode = true
159
+ config.verbose_logging = true
160
+ end
161
+ end
162
+
163
+ # Auto-load intelligence components
164
+ if Rails.env.development?
165
+ Rails.application.config.after_initialize do
166
+ Thread.new do
167
+ sleep(1) # Let Rails fully initialize
168
+
169
+ begin
170
+ # Display startup banner
171
+ puts "\\n🧠 HOKIPOKI: Parasitic intelligence framework loaded"
172
+ puts "🦠 Status: Ready for enhanced Claude interactions"
173
+
174
+ # Auto-detect Claude CLI
175
+ if Hokipoki.claude_parasite_active?
176
+ puts "āœ… Claude CLI detected: Enhanced intelligence active"
177
+ puts "šŸŽÆ Next: Run 'rails g hive_mind:install' for full setup"
178
+ else
179
+ puts "āš ļø Claude CLI not detected: Run when using Claude"
180
+ end
181
+
182
+ puts ""
183
+ rescue => e
184
+ Rails.logger.error "🦠 Hokipoki startup error: \#{e.message}"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ RUBY
190
+
191
+ say " āœ… Created config/initializers/hokipoki.rb", :green
192
+ say ""
193
+ end
194
+
195
+ def setup_otp_authentication
196
+ return if options[:skip_otp]
197
+
198
+ say "šŸ” SETTING UP OTP AUTHENTICATION:", :cyan
199
+
200
+ # Create OTP model
201
+ create_otp_model
202
+
203
+ # Create OTP controller
204
+ create_otp_controller
205
+
206
+ # Add routes
207
+ setup_otp_routes
208
+
209
+ # Create views
210
+ create_otp_views
211
+
212
+ say " āœ… OTP authentication configured", :green
213
+ say ""
214
+ end
215
+
216
+ def create_vector_storage
217
+ say "🧠 SETTING UP VECTOR STORAGE:", :cyan
218
+
219
+ # Create vector storage directory
220
+ vector_dir = File.expand_path('~/.hokipoki')
221
+ FileUtils.mkdir_p(vector_dir) unless Dir.exist?(vector_dir)
222
+
223
+ # Create initial configuration
224
+ config_file = File.join(vector_dir, 'config.yml')
225
+ config_data = {
226
+ version: Hokipoki::VERSION,
227
+ installation_date: Time.current.iso8601,
228
+ project_name: Rails.application.class.module_parent_name,
229
+ project_path: Rails.root.to_s,
230
+ auth_provider: options[:auth_provider],
231
+ otp_enabled: !options[:skip_otp]
232
+ }
233
+
234
+ File.write(config_file, config_data.to_yaml)
235
+
236
+ say " āœ… Vector storage directory: #{vector_dir}", :green
237
+ say " šŸ“ Configuration: #{config_file}", :white
238
+ say ""
239
+ end
240
+
241
+ def display_next_steps
242
+ say "šŸŽ‰ HOKIPOKI CORE INSTALLATION COMPLETE!", :green
243
+ say "=" * 60, :green
244
+ say ""
245
+ say "šŸ“‹ NEXT STEPS:", :cyan
246
+ say ""
247
+ say "1. šŸ“¦ Install dependencies:", :white
248
+ say " bundle install", :yellow
249
+ say ""
250
+ say "2. šŸ” Configure credentials (REQUIRED):", :white
251
+ say " EDITOR=nano rails credentials:edit", :yellow
252
+ say ""
253
+ say " Add this structure:", :white
254
+ say credentials_template, :yellow
255
+ say ""
256
+ say "3. šŸ—„ļø Run database migrations:", :white
257
+ say " rails db:migrate", :yellow
258
+ say ""
259
+ say "4. 🧠 Install HiveMind intelligence:", :white
260
+ say " rails g hive_mind:install", :yellow
261
+ say ""
262
+ say "5. 🦠 Attach parasite (after HiveMind):", :white
263
+ say " rails g hokipoki:attach_parasite", :yellow
264
+ say ""
265
+ say "šŸ”§ TROUBLESHOOTING:", :cyan
266
+ say " - If bundle fails: Check Ruby version compatibility", :white
267
+ say " - If credentials fail: Ensure master.key exists", :white
268
+ say " - If GitHub OAuth needed: Set up GitHub app first", :white
269
+ say ""
270
+ say "šŸ“š DOCUMENTATION:", :cyan
271
+ say " - GitHub OAuth setup: https://docs.github.com/en/apps/oauth-apps", :white
272
+ say " - Rails credentials: https://guides.rubyonrails.org/security.html#custom-credentials", :white
273
+ say ""
274
+ end
275
+
276
+ private
277
+
278
+ def add_gems_to_gemfile(gems)
279
+ gemfile_path = 'Gemfile'
280
+
281
+ gems.each do |gem_info|
282
+ gem_line = "gem '#{gem_info[:name]}', '#{gem_info[:version]}' # #{gem_info[:comment]}"
283
+
284
+ # Check if gem already exists
285
+ if File.read(gemfile_path).include?("gem '#{gem_info[:name]}'")
286
+ say " ā­ļø #{gem_info[:name]} already in Gemfile", :yellow
287
+ else
288
+ append_to_file gemfile_path, "\n#{gem_line}"
289
+ say " āœ… Added #{gem_info[:name]} #{gem_info[:version]}", :green
290
+ end
291
+ end
292
+ end
293
+
294
+ def create_otp_model
295
+ migration_name = "CreateHokipokiOtpUsers"
296
+ migration_file = "db/migrate/#{timestamp}_create_hokipoki_otp_users.rb"
297
+
298
+ create_file migration_file, <<~RUBY
299
+ class CreateHokipokiOtpUsers < ActiveRecord::Migration[7.0]
300
+ def change
301
+ create_table :hokipoki_otp_users do |t|
302
+ t.string :email, null: false
303
+ t.string :encrypted_otp_secret
304
+ t.boolean :otp_enabled, default: false
305
+ t.string :backup_codes, array: true, default: []
306
+ t.datetime :last_otp_at
307
+ t.integer :failed_attempts, default: 0
308
+ t.datetime :locked_until
309
+
310
+ t.timestamps
311
+ end
312
+
313
+ add_index :hokipoki_otp_users, :email, unique: true
314
+ end
315
+ end
316
+ RUBY
317
+
318
+ # Create model file
319
+ create_file "app/models/hokipoki_otp_user.rb", <<~RUBY
320
+ class HokipokiOtpUser < ApplicationRecord
321
+ validates :email, presence: true, uniqueness: true
322
+
323
+ def generate_otp_secret
324
+ self.encrypted_otp_secret = encrypt_otp_secret(ROTP::Base32.random)
325
+ save!
326
+ end
327
+
328
+ def otp_code
329
+ return nil unless encrypted_otp_secret
330
+
331
+ secret = decrypt_otp_secret(encrypted_otp_secret)
332
+ ROTP::TOTP.new(secret)
333
+ end
334
+
335
+ def verify_otp(code)
336
+ return false unless otp_enabled? && otp_code
337
+
338
+ if otp_code.verify(code, drift: 30)
339
+ update!(last_otp_at: Time.current, failed_attempts: 0)
340
+ true
341
+ else
342
+ increment!(:failed_attempts)
343
+ false
344
+ end
345
+ end
346
+
347
+ def qr_code_uri(issuer = 'Hokipoki')
348
+ return nil unless encrypted_otp_secret
349
+
350
+ secret = decrypt_otp_secret(encrypted_otp_secret)
351
+ ROTP::TOTP.new(secret).provisioning_uri(email, issuer_name: issuer)
352
+ end
353
+
354
+ def generate_backup_codes
355
+ codes = 8.times.map { SecureRandom.hex(4).upcase }
356
+ self.backup_codes = codes
357
+ save!
358
+ codes
359
+ end
360
+
361
+ private
362
+
363
+ def encrypt_otp_secret(secret)
364
+ key = Hokipoki.config.encryption_key
365
+ crypt = ActiveSupport::MessageEncryptor.new(key[0..31])
366
+ crypt.encrypt_and_sign(secret)
367
+ end
368
+
369
+ def decrypt_otp_secret(encrypted_secret)
370
+ key = Hokipoki.config.encryption_key
371
+ crypt = ActiveSupport::MessageEncryptor.new(key[0..31])
372
+ crypt.decrypt_and_verify(encrypted_secret)
373
+ end
374
+ end
375
+ RUBY
376
+ end
377
+
378
+ def create_otp_controller
379
+ create_file "app/controllers/hokipoki/otp_controller.rb", <<~RUBY
380
+ class Hokipoki::OtpController < ApplicationController
381
+ before_action :authenticate_user!
382
+ before_action :find_otp_user
383
+
384
+ def show
385
+ if @otp_user.otp_enabled?
386
+ redirect_to hokipoki_otp_verify_path
387
+ else
388
+ @otp_user.generate_otp_secret unless @otp_user.encrypted_otp_secret
389
+ @qr_code = generate_qr_code(@otp_user.qr_code_uri)
390
+ end
391
+ end
392
+
393
+ def enable
394
+ if @otp_user.verify_otp(params[:otp_code])
395
+ @otp_user.update!(otp_enabled: true)
396
+ @backup_codes = @otp_user.generate_backup_codes
397
+ flash[:notice] = 'Two-factor authentication enabled successfully!'
398
+ render :backup_codes
399
+ else
400
+ flash.now[:alert] = 'Invalid verification code'
401
+ render :show
402
+ end
403
+ end
404
+
405
+ def verify
406
+ # Verification logic for enabled OTP
407
+ end
408
+
409
+ def disable
410
+ if @otp_user.verify_otp(params[:otp_code])
411
+ @otp_user.update!(otp_enabled: false, encrypted_otp_secret: nil)
412
+ flash[:notice] = 'Two-factor authentication disabled'
413
+ redirect_to root_path
414
+ else
415
+ flash.now[:alert] = 'Invalid verification code'
416
+ render :verify
417
+ end
418
+ end
419
+
420
+ private
421
+
422
+ def find_otp_user
423
+ @otp_user = HokipokiOtpUser.find_or_create_by(email: current_user.email)
424
+ end
425
+
426
+ def generate_qr_code(uri)
427
+ qr = RQRCode::QRCode.new(uri)
428
+ qr.as_svg(module_size: 4)
429
+ end
430
+ end
431
+ RUBY
432
+ end
433
+
434
+ def setup_otp_routes
435
+ route_content = <<~RUBY
436
+
437
+ # Hokipoki OTP routes
438
+ namespace :hokipoki do
439
+ resource :otp, only: [:show] do
440
+ post :enable
441
+ get :verify
442
+ delete :disable
443
+ end
444
+ end
445
+ RUBY
446
+
447
+ append_to_file 'config/routes.rb', route_content
448
+ end
449
+
450
+ def create_otp_views
451
+ # Create views directory
452
+ empty_directory "app/views/hokipoki"
453
+ empty_directory "app/views/hokipoki/otp"
454
+
455
+ # Show view (setup)
456
+ create_file "app/views/hokipoki/otp/show.html.erb", <<~ERB
457
+ <div class="hokipoki-otp-setup">
458
+ <h2>šŸ” Set Up Two-Factor Authentication</h2>
459
+
460
+ <div class="setup-instructions">
461
+ <h3>Step 1: Install an authenticator app</h3>
462
+ <p>Download an app like Google Authenticator, Authy, or 1Password on your phone.</p>
463
+
464
+ <h3>Step 2: Scan this QR code</h3>
465
+ <div class="qr-code">
466
+ <%= @qr_code.html_safe %>
467
+ </div>
468
+
469
+ <h3>Step 3: Enter the 6-digit code</h3>
470
+ <%= form_with url: hokipoki_otp_enable_path, method: :post do |form| %>
471
+ <%= form.text_field :otp_code, placeholder: "000000", maxlength: 6, required: true %>
472
+ <%= form.submit "Enable Two-Factor Authentication", class: "btn btn-primary" %>
473
+ <% end %>
474
+ </div>
475
+ </div>
476
+ ERB
477
+
478
+ # Backup codes view
479
+ create_file "app/views/hokipoki/otp/backup_codes.html.erb", <<~ERB
480
+ <div class="hokipoki-backup-codes">
481
+ <h2>šŸ” Your Backup Codes</h2>
482
+
483
+ <div class="alert alert-warning">
484
+ <strong>Important:</strong> Save these backup codes in a safe place.
485
+ You can use them to access your account if you lose your phone.
486
+ </div>
487
+
488
+ <div class="backup-codes">
489
+ <% @backup_codes.each do |code| %>
490
+ <code><%= code %></code>
491
+ <% end %>
492
+ </div>
493
+
494
+ <%= link_to "Continue", root_path, class: "btn btn-primary" %>
495
+ </div>
496
+ ERB
497
+ end
498
+
499
+ def credentials_template
500
+ <<~YAML
501
+ hokipoki:
502
+ otp_encryption_key: #{SecureRandom.hex(32)}
503
+ vector_encryption_key: #{SecureRandom.hex(32)}
504
+ github:
505
+ client_id: your_github_client_id_here
506
+ client_secret: your_github_client_secret_here
507
+ YAML
508
+ end
509
+
510
+ def timestamp
511
+ Time.current.strftime("%Y%m%d%H%M%S")
512
+ end
513
+ end
514
+ end
515
+ end