htm 0.0.15 → 0.0.18

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/.architecture/decisions/adrs/001-use-postgresql-timescaledb-storage.md +1 -1
  3. data/.architecture/decisions/adrs/011-database-side-embedding-generation-with-pgai.md +4 -4
  4. data/.architecture/decisions/adrs/012-llm-driven-ontology-topic-extraction.md +1 -1
  5. data/.envrc +12 -24
  6. data/.irbrc +7 -7
  7. data/.tbls.yml +2 -2
  8. data/CHANGELOG.md +138 -0
  9. data/README.md +97 -1592
  10. data/Rakefile +8 -3
  11. data/SETUP.md +12 -12
  12. data/bin/htm_mcp +27 -0
  13. data/db/seed_data/README.md +2 -2
  14. data/db/seeds.rb +2 -2
  15. data/docs/api/database.md +37 -37
  16. data/docs/api/htm.md +1 -1
  17. data/docs/api/yard/HTM/ActiveRecordConfig.md +2 -2
  18. data/docs/api/yard/HTM/Configuration.md +26 -15
  19. data/docs/api/yard/HTM/Database.md +7 -8
  20. data/docs/api/yard/HTM/JobAdapter.md +1 -1
  21. data/docs/api/yard/HTM/Railtie.md +2 -2
  22. data/docs/architecture/adrs/001-postgresql-timescaledb.md +1 -1
  23. data/docs/architecture/adrs/011-pgai-integration.md +4 -4
  24. data/docs/database_rake_tasks.md +5 -5
  25. data/docs/development/rake-tasks.md +11 -11
  26. data/docs/development/setup.md +21 -21
  27. data/docs/development/testing.md +1 -1
  28. data/docs/getting-started/installation.md +51 -31
  29. data/docs/getting-started/quick-start.md +12 -12
  30. data/docs/guides/getting-started.md +2 -2
  31. data/docs/guides/long-term-memory.md +1 -1
  32. data/docs/guides/mcp-server.md +464 -29
  33. data/docs/guides/robot-groups.md +8 -8
  34. data/docs/index.md +4 -4
  35. data/docs/multi_framework_support.md +10 -10
  36. data/docs/setup_local_database.md +19 -19
  37. data/docs/using_rake_tasks_in_your_app.md +14 -14
  38. data/examples/README.md +50 -6
  39. data/examples/basic_usage.rb +31 -21
  40. data/examples/cli_app/README.md +8 -8
  41. data/examples/cli_app/htm_cli.rb +5 -5
  42. data/examples/config_file_example/README.md +256 -0
  43. data/examples/config_file_example/config/htm.local.yml +34 -0
  44. data/examples/config_file_example/custom_config.yml +22 -0
  45. data/examples/config_file_example/show_config.rb +125 -0
  46. data/examples/custom_llm_configuration.rb +7 -7
  47. data/examples/example_app/Rakefile +2 -2
  48. data/examples/example_app/app.rb +8 -8
  49. data/examples/file_loader_usage.rb +9 -9
  50. data/examples/mcp_client.rb +7 -7
  51. data/examples/rails_app/.gitignore +2 -0
  52. data/examples/rails_app/Gemfile +22 -0
  53. data/examples/rails_app/Gemfile.lock +430 -0
  54. data/examples/rails_app/Procfile.dev +1 -0
  55. data/examples/rails_app/README.md +98 -0
  56. data/examples/rails_app/Rakefile +5 -0
  57. data/examples/rails_app/app/assets/stylesheets/application.css +83 -0
  58. data/examples/rails_app/app/assets/stylesheets/inter-font.css +6 -0
  59. data/examples/rails_app/app/controllers/application_controller.rb +19 -0
  60. data/examples/rails_app/app/controllers/dashboard_controller.rb +27 -0
  61. data/examples/rails_app/app/controllers/files_controller.rb +205 -0
  62. data/examples/rails_app/app/controllers/memories_controller.rb +102 -0
  63. data/examples/rails_app/app/controllers/robots_controller.rb +44 -0
  64. data/examples/rails_app/app/controllers/search_controller.rb +46 -0
  65. data/examples/rails_app/app/controllers/tags_controller.rb +30 -0
  66. data/examples/rails_app/app/javascript/application.js +4 -0
  67. data/examples/rails_app/app/javascript/controllers/application.js +9 -0
  68. data/examples/rails_app/app/javascript/controllers/index.js +6 -0
  69. data/examples/rails_app/app/views/dashboard/index.html.erb +123 -0
  70. data/examples/rails_app/app/views/files/index.html.erb +108 -0
  71. data/examples/rails_app/app/views/files/new.html.erb +321 -0
  72. data/examples/rails_app/app/views/files/show.html.erb +130 -0
  73. data/examples/rails_app/app/views/layouts/application.html.erb +124 -0
  74. data/examples/rails_app/app/views/memories/_memory_card.html.erb +51 -0
  75. data/examples/rails_app/app/views/memories/deleted.html.erb +62 -0
  76. data/examples/rails_app/app/views/memories/edit.html.erb +35 -0
  77. data/examples/rails_app/app/views/memories/index.html.erb +81 -0
  78. data/examples/rails_app/app/views/memories/new.html.erb +71 -0
  79. data/examples/rails_app/app/views/memories/show.html.erb +126 -0
  80. data/examples/rails_app/app/views/robots/index.html.erb +106 -0
  81. data/examples/rails_app/app/views/robots/new.html.erb +36 -0
  82. data/examples/rails_app/app/views/robots/show.html.erb +79 -0
  83. data/examples/rails_app/app/views/search/index.html.erb +184 -0
  84. data/examples/rails_app/app/views/shared/_navbar.html.erb +52 -0
  85. data/examples/rails_app/app/views/shared/_stat_card.html.erb +52 -0
  86. data/examples/rails_app/app/views/tags/index.html.erb +131 -0
  87. data/examples/rails_app/app/views/tags/show.html.erb +67 -0
  88. data/examples/rails_app/bin/dev +8 -0
  89. data/examples/rails_app/bin/rails +4 -0
  90. data/examples/rails_app/bin/rake +4 -0
  91. data/examples/rails_app/config/application.rb +33 -0
  92. data/examples/rails_app/config/boot.rb +5 -0
  93. data/examples/rails_app/config/database.yml +15 -0
  94. data/examples/rails_app/config/environment.rb +5 -0
  95. data/examples/rails_app/config/importmap.rb +7 -0
  96. data/examples/rails_app/config/routes.rb +38 -0
  97. data/examples/rails_app/config/tailwind.config.js +35 -0
  98. data/examples/rails_app/config.ru +5 -0
  99. data/examples/rails_app/log/.keep +0 -0
  100. data/examples/rails_app/tmp/local_secret.txt +1 -0
  101. data/examples/robot_groups/multi_process.rb +5 -5
  102. data/examples/robot_groups/robot_worker.rb +5 -5
  103. data/examples/robot_groups/same_process.rb +9 -9
  104. data/examples/sinatra_app/app.rb +1 -1
  105. data/examples/timeframe_demo.rb +1 -1
  106. data/lib/htm/active_record_config.rb +12 -28
  107. data/lib/htm/circuit_breaker.rb +0 -2
  108. data/lib/htm/config/defaults.yml +246 -0
  109. data/lib/htm/config.rb +888 -0
  110. data/lib/htm/database.rb +26 -33
  111. data/lib/htm/embedding_service.rb +0 -4
  112. data/lib/htm/integrations/sinatra.rb +3 -7
  113. data/lib/htm/job_adapter.rb +1 -15
  114. data/lib/htm/jobs/generate_embedding_job.rb +1 -7
  115. data/lib/htm/jobs/generate_propositions_job.rb +2 -12
  116. data/lib/htm/jobs/generate_tags_job.rb +1 -8
  117. data/lib/htm/loaders/defaults_loader.rb +143 -0
  118. data/lib/htm/loaders/xdg_config_loader.rb +116 -0
  119. data/lib/htm/mcp/cli.rb +475 -0
  120. data/lib/htm/mcp/group_tools.rb +476 -0
  121. data/lib/htm/mcp/resources.rb +89 -0
  122. data/lib/htm/mcp/server.rb +98 -0
  123. data/lib/htm/mcp/tools.rb +488 -0
  124. data/lib/htm/models/file_source.rb +5 -3
  125. data/lib/htm/proposition_service.rb +2 -12
  126. data/lib/htm/railtie.rb +3 -8
  127. data/lib/htm/tag_service.rb +1 -8
  128. data/lib/htm/tasks.rb +7 -4
  129. data/lib/htm/version.rb +1 -1
  130. data/lib/htm.rb +124 -5
  131. data/lib/tasks/htm.rake +6 -9
  132. metadata +81 -6
  133. data/bin/htm_mcp.rb +0 -621
  134. data/config/database.yml +0 -74
  135. data/lib/htm/configuration.rb +0 -766
@@ -0,0 +1,475 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HTM
4
+ module MCP
5
+ # CLI commands for htm_mcp executable
6
+ module CLI
7
+ module_function
8
+
9
+ def print_help
10
+ puts <<~HELP
11
+ HTM MCP Server v#{HTM::VERSION} - Memory management for AI assistants
12
+
13
+ USAGE:
14
+ htm_mcp [COMMAND]
15
+
16
+ COMMANDS:
17
+ server Start the MCP server (default if no command given)
18
+ stdio Alias for server (for MCP client compatibility)
19
+ setup Initialize the database schema
20
+ init Alias for setup
21
+ verify Verify database connection and extensions
22
+ stats Show memory statistics
23
+ config Output default configuration to STDOUT
24
+ version Show HTM version
25
+ help Show this help message
26
+
27
+ ENVIRONMENT VARIABLES:
28
+
29
+ Note: Nested config uses double underscores (e.g., HTM_EMBEDDING__PROVIDER)
30
+
31
+ Environment:
32
+ HTM_ENV Environment name: development, test, production
33
+ (priority: HTM_ENV > RAILS_ENV > RACK_ENV > 'development')
34
+
35
+ Database:
36
+ HTM_DATABASE__URL PostgreSQL connection URL (preferred)
37
+ Example: postgresql://user:pass@localhost:5432/htm_development
38
+ HTM_DATABASE__HOST Database host (default: localhost)
39
+ HTM_DATABASE__PORT Database port (default: 5432)
40
+ HTM_DATABASE__NAME Database name
41
+ HTM_DATABASE__USER Database username
42
+ HTM_DATABASE__PASSWORD Database password
43
+ HTM_DATABASE__SSLMODE SSL mode (default: prefer)
44
+ HTM_DATABASE__POOL_SIZE Connection pool size (default: 10)
45
+
46
+ Embedding:
47
+ HTM_EMBEDDING__PROVIDER Provider (default: ollama)
48
+ HTM_EMBEDDING__MODEL Model (default: nomic-embed-text:latest)
49
+ HTM_EMBEDDING__DIMENSIONS Dimensions (default: 768)
50
+ HTM_EMBEDDING__TIMEOUT Timeout seconds (default: 120)
51
+ HTM_EMBEDDING__MAX_DIMENSION Max dimensions (default: 2000)
52
+
53
+ Tag Extraction:
54
+ HTM_TAG__PROVIDER Provider (default: ollama)
55
+ HTM_TAG__MODEL Model (default: gemma3:latest)
56
+ HTM_TAG__TIMEOUT Timeout seconds (default: 180)
57
+ HTM_TAG__MAX_DEPTH Max hierarchy depth (default: 4)
58
+
59
+ Proposition Extraction:
60
+ HTM_PROPOSITION__PROVIDER Provider (default: ollama)
61
+ HTM_PROPOSITION__MODEL Model (default: gemma3:latest)
62
+ HTM_PROPOSITION__TIMEOUT Timeout seconds (default: 180)
63
+ HTM_PROPOSITION__ENABLED Enable extraction (default: false)
64
+
65
+ Chunking:
66
+ HTM_CHUNKING__SIZE Max chars per chunk (default: 1024)
67
+ HTM_CHUNKING__OVERLAP Chunk overlap chars (default: 64)
68
+
69
+ Job Backend:
70
+ HTM_JOB__BACKEND Backend: inline, thread, active_job, sidekiq
71
+
72
+ Provider API Keys:
73
+ HTM_PROVIDERS__OLLAMA__URL Ollama URL (default: http://localhost:11434)
74
+ HTM_PROVIDERS__OPENAI__API_KEY OpenAI API key
75
+ HTM_PROVIDERS__ANTHROPIC__API_KEY Anthropic API key
76
+ HTM_PROVIDERS__GEMINI__API_KEY Google Gemini API key
77
+ HTM_PROVIDERS__AZURE__API_KEY Azure OpenAI API key
78
+ HTM_PROVIDERS__AZURE__ENDPOINT Azure OpenAI endpoint
79
+
80
+ Other:
81
+ HTM_LOG_LEVEL Log level (default: info)
82
+ HTM_CONNECTION_TIMEOUT Connection timeout seconds (default: 30)
83
+ HTM_TELEMETRY_ENABLED Enable OpenTelemetry (default: false)
84
+
85
+ OPTIONS:
86
+ -c, --config [PATH] Without PATH: output default config to STDOUT
87
+ With PATH: load config from YAML file
88
+
89
+ EXAMPLES:
90
+ # Generate a config file template
91
+ htm_mcp --config > my_config.yml
92
+
93
+ # Start server with custom config
94
+ htm_mcp --config my_config.yml
95
+
96
+ # First-time setup
97
+ export HTM_DATABASE__URL="postgresql://postgres@localhost:5432/htm"
98
+ htm_mcp setup
99
+
100
+ # Verify connection
101
+ htm_mcp verify
102
+
103
+ # Use test database
104
+ HTM_ENV=test htm_mcp setup
105
+ HTM_ENV=test htm_mcp stats
106
+
107
+ # Start MCP server (for Claude Desktop)
108
+ htm_mcp
109
+
110
+ CLAUDE DESKTOP CONFIGURATION:
111
+ Add to ~/.config/claude/claude_desktop_config.json:
112
+
113
+ {
114
+ "mcpServers": {
115
+ "htm-memory": {
116
+ "command": "/path/to/htm_mcp",
117
+ "env": {
118
+ "HTM_DATABASE__URL": "postgresql://postgres@localhost:5432/htm_development"
119
+ }
120
+ }
121
+ }
122
+ }
123
+ HELP
124
+ end
125
+
126
+ def check_database_config!
127
+ unless ENV['HTM_DATABASE__URL'] || ENV['HTM_DATABASE__NAME']
128
+ warn "Error: Database not configured."
129
+ warn "Set HTM_DATABASE__URL or HTM_DATABASE__NAME environment variable."
130
+ warn "Run 'htm_mcp help' for details."
131
+ exit 1
132
+ end
133
+ end
134
+
135
+ def print_error_suggestion(error_message)
136
+ msg = error_message.to_s.downcase
137
+
138
+ warn ""
139
+ if msg.include?("does not exist")
140
+ warn "Suggestion: The database does not exist. Create it with:"
141
+ warn " createdb #{extract_dbname(ENV['HTM_DATABASE__URL'] || ENV['HTM_DATABASE__NAME'])}"
142
+ warn "Then initialize the schema with:"
143
+ warn " htm_mcp setup"
144
+ elsif msg.include?("password authentication failed") || msg.include?("no password supplied")
145
+ warn "Suggestion: Check your database credentials."
146
+ warn "Verify HTM_DATABASE__URL has correct username and password:"
147
+ warn " postgresql://USER:PASSWORD@localhost:5432/DATABASE"
148
+ elsif msg.include?("connection refused") || msg.include?("could not connect")
149
+ warn "Suggestion: PostgreSQL server is not running or not accepting connections."
150
+ warn "Start PostgreSQL with:"
151
+ warn " brew services start postgresql@17 # macOS with Homebrew"
152
+ warn " sudo systemctl start postgresql # Linux"
153
+ elsif msg.include?("role") && msg.include?("does not exist")
154
+ warn "Suggestion: The database user does not exist. Create it with:"
155
+ warn " createuser -s YOUR_USERNAME"
156
+ elsif msg.include?("permission denied")
157
+ warn "Suggestion: The user lacks permission to access this database."
158
+ warn "Grant access or use a different user with appropriate privileges."
159
+ elsif msg.include?("timeout") || msg.include?("timed out")
160
+ warn "Suggestion: Connection timed out. Check:"
161
+ warn " - PostgreSQL is running"
162
+ warn " - Firewall allows connections on port 5432"
163
+ warn " - Host address is correct"
164
+ elsif msg.include?("extension") && msg.include?("vector")
165
+ warn "Suggestion: pgvector extension is not installed. Install it with:"
166
+ warn " brew install pgvector # macOS"
167
+ warn "Then enable it in your database:"
168
+ warn " psql -d DATABASE -c 'CREATE EXTENSION vector;'"
169
+ else
170
+ warn "Suggestion: Run 'htm_mcp help' for configuration details."
171
+ end
172
+ end
173
+
174
+ def extract_dbname(url_or_name)
175
+ return url_or_name unless url_or_name&.include?("://")
176
+
177
+ # Extract database name from URL like postgresql://user@host:port/dbname
178
+ if url_or_name =~ %r{/([^/?]+)(?:\?|$)}
179
+ $1
180
+ else
181
+ "htm_development"
182
+ end
183
+ end
184
+
185
+ def run_setup
186
+ puts "HTM Database Setup"
187
+ puts "=================="
188
+ puts
189
+
190
+ check_database_config!
191
+
192
+ begin
193
+ HTM::Database.setup
194
+ puts
195
+ puts "Database initialized successfully!"
196
+ puts "You can now start the MCP server with: htm_mcp"
197
+ rescue => e
198
+ warn "Setup failed: #{e.message}"
199
+ print_error_suggestion(e.message)
200
+ warn e.backtrace.first(5).join("\n") if ENV['DEBUG']
201
+ exit 1
202
+ end
203
+ end
204
+
205
+ def run_verify
206
+ puts "HTM Database Verification"
207
+ puts "========================="
208
+ puts
209
+
210
+ check_database_config!
211
+
212
+ begin
213
+ HTM::Database.info
214
+ puts
215
+
216
+ # Check migration status
217
+ pending = check_migration_status
218
+ puts
219
+
220
+ if pending > 0
221
+ warn "Warning: #{pending} pending migration(s) detected."
222
+ warn " Run 'htm_mcp setup' to apply pending migrations."
223
+ puts
224
+ end
225
+
226
+ puts "Database connection verified!"
227
+ rescue => e
228
+ warn "Verification failed: #{e.message}"
229
+ print_error_suggestion(e.message)
230
+ warn e.backtrace.first(5).join("\n") if ENV['DEBUG']
231
+ exit 1
232
+ end
233
+ end
234
+
235
+ def check_migration_status
236
+ migrations_path = File.expand_path('../../../db/migrate', __dir__)
237
+
238
+ # Get available migrations from files
239
+ available_migrations = Dir.glob(File.join(migrations_path, '*.rb')).map do |file|
240
+ {
241
+ version: File.basename(file).split('_').first,
242
+ name: File.basename(file, '.rb')
243
+ }
244
+ end.sort_by { |m| m[:version] }
245
+
246
+ # Ensure ActiveRecord connection for migration check
247
+ HTM::ActiveRecordConfig.establish_connection!
248
+
249
+ # Get applied migrations from database
250
+ applied_versions = begin
251
+ ActiveRecord::Base.connection.select_values('SELECT version FROM schema_migrations ORDER BY version')
252
+ rescue ActiveRecord::StatementInvalid
253
+ []
254
+ end
255
+
256
+ puts "Migration Status"
257
+ puts "-" * 80
258
+
259
+ if available_migrations.empty?
260
+ puts " No migration files found"
261
+ return 0
262
+ end
263
+
264
+ available_migrations.each do |migration|
265
+ applied = applied_versions.include?(migration[:version])
266
+ status_mark = applied ? "+" : "-"
267
+ puts " #{status_mark} #{migration[:name]}"
268
+ end
269
+
270
+ applied_count = applied_versions.length
271
+ pending_count = available_migrations.length - applied_count
272
+
273
+ puts "-" * 80
274
+ puts " #{applied_count} applied, #{pending_count} pending"
275
+
276
+ pending_count
277
+ end
278
+
279
+ def output_default_config
280
+ defaults_path = File.expand_path('../config/defaults.yml', __dir__)
281
+ if File.exist?(defaults_path)
282
+ puts File.read(defaults_path)
283
+ else
284
+ warn "Error: defaults.yml not found at #{defaults_path}"
285
+ exit 1
286
+ end
287
+ end
288
+
289
+ def load_config_file(path)
290
+ unless File.exist?(path)
291
+ warn "Error: Config file not found: #{path}"
292
+ exit 1
293
+ end
294
+
295
+ begin
296
+ require 'yaml'
297
+ config_data = YAML.safe_load(
298
+ File.read(path),
299
+ permitted_classes: [Symbol],
300
+ symbolize_names: true,
301
+ aliases: true
302
+ ) || {}
303
+
304
+ # Determine which section to use based on environment
305
+ env = HTM::Config.env.to_sym
306
+ base = config_data[:defaults] || {}
307
+ env_overrides = config_data[env] || {}
308
+
309
+ # Merge base with environment-specific overrides
310
+ merged = deep_merge(base, env_overrides)
311
+
312
+ apply_config(merged)
313
+
314
+ warn "Loaded configuration from: #{path}"
315
+ warn "Environment: #{env}"
316
+ rescue => e
317
+ warn "Error loading config file: #{e.message}"
318
+ warn e.backtrace.first(5).join("\n") if ENV['DEBUG']
319
+ exit 1
320
+ end
321
+ end
322
+
323
+ def deep_merge(base, override)
324
+ base.merge(override) do |_key, old_val, new_val|
325
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
326
+ deep_merge(old_val, new_val)
327
+ else
328
+ new_val.nil? ? old_val : new_val
329
+ end
330
+ end
331
+ end
332
+
333
+ def apply_config(config)
334
+ HTM.configure do |c|
335
+ # Apply nested sections
336
+ apply_section(c, :database, config[:database])
337
+ apply_section(c, :service, config[:service])
338
+ apply_section(c, :embedding, config[:embedding])
339
+ apply_section(c, :tag, config[:tag])
340
+ apply_section(c, :proposition, config[:proposition])
341
+ apply_section(c, :chunking, config[:chunking])
342
+ apply_section(c, :circuit_breaker, config[:circuit_breaker])
343
+ apply_section(c, :relevance, config[:relevance])
344
+ apply_section(c, :job, config[:job])
345
+ apply_section(c, :providers, config[:providers])
346
+
347
+ # Apply top-level scalars
348
+ c.week_start = config[:week_start] if config[:week_start]
349
+ c.connection_timeout = config[:connection_timeout] if config[:connection_timeout]
350
+ c.telemetry_enabled = config[:telemetry_enabled] unless config[:telemetry_enabled].nil?
351
+ c.log_level = config[:log_level] if config[:log_level]
352
+ end
353
+ end
354
+
355
+ def apply_section(config, section_name, values)
356
+ return unless values.is_a?(Hash)
357
+
358
+ section = config.send(section_name)
359
+ values.each do |key, value|
360
+ next if value.nil?
361
+
362
+ if value.is_a?(Hash)
363
+ # Handle nested sections (like providers.openai)
364
+ subsection = section.send(key)
365
+ value.each do |subkey, subvalue|
366
+ subsection.send("#{subkey}=", subvalue) unless subvalue.nil?
367
+ end
368
+ else
369
+ section.send("#{key}=", value)
370
+ end
371
+ end
372
+ end
373
+
374
+ def run_stats
375
+ puts "HTM Memory Statistics"
376
+ puts "====================="
377
+ puts
378
+
379
+ check_database_config!
380
+
381
+ begin
382
+ HTM::ActiveRecordConfig.establish_connection!
383
+
384
+ total_nodes = HTM::Models::Node.count
385
+ deleted_nodes = HTM::Models::Node.deleted.count
386
+ with_embeddings = HTM::Models::Node.with_embeddings.count
387
+ total_tags = HTM::Models::Tag.count
388
+ total_robots = HTM::Models::Robot.count
389
+ total_files = HTM::Models::FileSource.count
390
+
391
+ # Get database size
392
+ db_size = ActiveRecord::Base.connection.execute(
393
+ "SELECT pg_size_pretty(pg_database_size(current_database())) AS size"
394
+ ).first['size']
395
+
396
+ puts "Nodes: #{total_nodes} active, #{deleted_nodes} deleted, #{with_embeddings} with embeddings"
397
+ puts "Tags: #{total_tags}"
398
+ puts "Robots: #{total_robots}"
399
+ puts "Files: #{total_files}"
400
+ puts
401
+ puts "Database size: #{db_size}"
402
+ rescue => e
403
+ warn "Stats failed: #{e.message}"
404
+ print_error_suggestion(e.message)
405
+ warn e.backtrace.first(5).join("\n") if ENV['DEBUG']
406
+ exit 1
407
+ end
408
+ end
409
+
410
+ def run(args)
411
+ args = args.dup
412
+
413
+ # Handle -c / --config option first (can be combined with other commands)
414
+ config_loaded = handle_config_option(args)
415
+
416
+ # Process remaining command
417
+ case args[0]&.downcase
418
+ when 'help', '-h', '--help'
419
+ print_help
420
+ when 'version', '-v', '--version'
421
+ puts "HTM #{HTM::VERSION}"
422
+ when 'setup', 'init'
423
+ run_setup
424
+ when 'verify'
425
+ run_verify
426
+ when 'stats'
427
+ run_stats
428
+ when 'config'
429
+ output_default_config
430
+ when 'server', 'stdio', nil
431
+ # Return false to indicate server should start
432
+ # 'stdio' is accepted for compatibility with MCP clients that pass it as an argument
433
+ return false
434
+ when /^-/
435
+ $stderr.puts "Unknown option: #{args[0]}"
436
+ $stderr.puts "Run 'htm_mcp help' for usage."
437
+ exit 1
438
+ else
439
+ $stderr.puts "Unknown command: #{args[0]}"
440
+ $stderr.puts "Run 'htm_mcp help' for usage."
441
+ exit 1
442
+ end
443
+ true
444
+ end
445
+
446
+ # Handle -c / --config option, modifying args in place
447
+ # Returns true if config was loaded, nil otherwise
448
+ def handle_config_option(args)
449
+ config_idx = args.index('-c') || args.index('--config')
450
+ return nil unless config_idx
451
+
452
+ # Remove the -c/--config flag
453
+ args.delete_at(config_idx)
454
+
455
+ # Check if next arg is a path (not another flag or command)
456
+ next_arg = args[config_idx]
457
+
458
+ if next_arg.nil? || next_arg.start_with?('-') || command?(next_arg)
459
+ # No path provided - output default config and exit
460
+ output_default_config
461
+ exit 0
462
+ else
463
+ # Path provided - load config file
464
+ config_path = args.delete_at(config_idx)
465
+ load_config_file(config_path)
466
+ true
467
+ end
468
+ end
469
+
470
+ def command?(arg)
471
+ %w[help version setup init verify stats config server stdio].include?(arg.downcase)
472
+ end
473
+ end
474
+ end
475
+ end