htm 0.0.15 → 0.0.17

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/CHANGELOG.md +67 -0
  4. data/README.md +97 -1592
  5. data/bin/htm_mcp +31 -0
  6. data/config/database.yml +7 -4
  7. data/docs/getting-started/installation.md +31 -11
  8. data/docs/guides/mcp-server.md +456 -21
  9. data/docs/multi_framework_support.md +2 -2
  10. data/examples/mcp_client.rb +2 -2
  11. data/examples/rails_app/.gitignore +2 -0
  12. data/examples/rails_app/Gemfile +22 -0
  13. data/examples/rails_app/Gemfile.lock +438 -0
  14. data/examples/rails_app/Procfile.dev +1 -0
  15. data/examples/rails_app/README.md +98 -0
  16. data/examples/rails_app/Rakefile +5 -0
  17. data/examples/rails_app/app/assets/stylesheets/application.css +83 -0
  18. data/examples/rails_app/app/assets/stylesheets/inter-font.css +6 -0
  19. data/examples/rails_app/app/controllers/application_controller.rb +19 -0
  20. data/examples/rails_app/app/controllers/dashboard_controller.rb +27 -0
  21. data/examples/rails_app/app/controllers/files_controller.rb +205 -0
  22. data/examples/rails_app/app/controllers/memories_controller.rb +102 -0
  23. data/examples/rails_app/app/controllers/robots_controller.rb +44 -0
  24. data/examples/rails_app/app/controllers/search_controller.rb +46 -0
  25. data/examples/rails_app/app/controllers/tags_controller.rb +30 -0
  26. data/examples/rails_app/app/javascript/application.js +4 -0
  27. data/examples/rails_app/app/javascript/controllers/application.js +9 -0
  28. data/examples/rails_app/app/javascript/controllers/index.js +6 -0
  29. data/examples/rails_app/app/views/dashboard/index.html.erb +123 -0
  30. data/examples/rails_app/app/views/files/index.html.erb +108 -0
  31. data/examples/rails_app/app/views/files/new.html.erb +321 -0
  32. data/examples/rails_app/app/views/files/show.html.erb +130 -0
  33. data/examples/rails_app/app/views/layouts/application.html.erb +124 -0
  34. data/examples/rails_app/app/views/memories/_memory_card.html.erb +51 -0
  35. data/examples/rails_app/app/views/memories/deleted.html.erb +62 -0
  36. data/examples/rails_app/app/views/memories/edit.html.erb +35 -0
  37. data/examples/rails_app/app/views/memories/index.html.erb +81 -0
  38. data/examples/rails_app/app/views/memories/new.html.erb +71 -0
  39. data/examples/rails_app/app/views/memories/show.html.erb +126 -0
  40. data/examples/rails_app/app/views/robots/index.html.erb +106 -0
  41. data/examples/rails_app/app/views/robots/new.html.erb +36 -0
  42. data/examples/rails_app/app/views/robots/show.html.erb +79 -0
  43. data/examples/rails_app/app/views/search/index.html.erb +184 -0
  44. data/examples/rails_app/app/views/shared/_navbar.html.erb +52 -0
  45. data/examples/rails_app/app/views/shared/_stat_card.html.erb +52 -0
  46. data/examples/rails_app/app/views/tags/index.html.erb +131 -0
  47. data/examples/rails_app/app/views/tags/show.html.erb +67 -0
  48. data/examples/rails_app/bin/dev +8 -0
  49. data/examples/rails_app/bin/rails +4 -0
  50. data/examples/rails_app/bin/rake +4 -0
  51. data/examples/rails_app/config/application.rb +33 -0
  52. data/examples/rails_app/config/boot.rb +5 -0
  53. data/examples/rails_app/config/database.yml +15 -0
  54. data/examples/rails_app/config/environment.rb +5 -0
  55. data/examples/rails_app/config/importmap.rb +7 -0
  56. data/examples/rails_app/config/routes.rb +38 -0
  57. data/examples/rails_app/config/tailwind.config.js +35 -0
  58. data/examples/rails_app/config.ru +5 -0
  59. data/examples/rails_app/log/.keep +0 -0
  60. data/examples/rails_app/tmp/local_secret.txt +1 -0
  61. data/lib/htm/active_record_config.rb +2 -5
  62. data/lib/htm/configuration.rb +35 -2
  63. data/lib/htm/database.rb +3 -6
  64. data/lib/htm/mcp/cli.rb +333 -0
  65. data/lib/htm/mcp/group_tools.rb +476 -0
  66. data/lib/htm/mcp/resources.rb +89 -0
  67. data/lib/htm/mcp/server.rb +98 -0
  68. data/lib/htm/mcp/tools.rb +488 -0
  69. data/lib/htm/models/file_source.rb +5 -3
  70. data/lib/htm/railtie.rb +0 -4
  71. data/lib/htm/tasks.rb +7 -4
  72. data/lib/htm/version.rb +1 -1
  73. data/lib/tasks/htm.rake +6 -9
  74. metadata +59 -4
  75. data/bin/htm_mcp.rb +0 -621
@@ -0,0 +1,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fast_mcp'
4
+
5
+ class HTM
6
+ module MCP
7
+ # Session state for the current robot
8
+ # Each MCP client spawns its own server process, so this is naturally isolated
9
+ module Session
10
+ DEFAULT_ROBOT_NAME = "mcp_default"
11
+
12
+ class << self
13
+ attr_accessor :logger
14
+
15
+ def htm_instance
16
+ @htm_instance ||= HTM.new(robot_name: DEFAULT_ROBOT_NAME)
17
+ end
18
+
19
+ def set_robot(name)
20
+ @robot_name = name
21
+ @htm_instance = HTM.new(robot_name: name)
22
+ logger&.info "Robot set: #{name} (id=#{@htm_instance.robot_id})"
23
+ @htm_instance
24
+ end
25
+
26
+ def robot_name
27
+ @robot_name || DEFAULT_ROBOT_NAME
28
+ end
29
+
30
+ def robot_initialized?
31
+ @robot_name != nil
32
+ end
33
+ end
34
+ end
35
+
36
+ # Tool: Set the robot identity for this session
37
+ class SetRobotTool < FastMcp::Tool
38
+ description "Set the robot identity for this session. Call this first to establish your robot name."
39
+
40
+ arguments do
41
+ required(:name).filled(:string).description("The robot name (will be created if it doesn't exist)")
42
+ end
43
+
44
+ def call(name:)
45
+ Session.logger&.info "SetRobotTool called: name=#{name.inspect}"
46
+
47
+ htm = Session.set_robot(name)
48
+ robot = HTM::Models::Robot.find(htm.robot_id)
49
+
50
+ {
51
+ success: true,
52
+ robot_id: htm.robot_id,
53
+ robot_name: htm.robot_name,
54
+ node_count: robot.node_count,
55
+ message: "Robot '#{name}' is now active for this session"
56
+ }.to_json
57
+ end
58
+ end
59
+
60
+ # Tool: Get current robot info
61
+ class GetRobotTool < FastMcp::Tool
62
+ description "Get information about the current robot for this session"
63
+
64
+ arguments do
65
+ end
66
+
67
+ def call
68
+ Session.logger&.info "GetRobotTool called"
69
+
70
+ htm = Session.htm_instance
71
+ robot = HTM::Models::Robot.find(htm.robot_id)
72
+
73
+ {
74
+ success: true,
75
+ robot_id: htm.robot_id,
76
+ robot_name: htm.robot_name,
77
+ initialized: Session.robot_initialized?,
78
+ memory_summary: robot.memory_summary
79
+ }.to_json
80
+ end
81
+ end
82
+
83
+ # Tool: Get working memory contents for session restore
84
+ class GetWorkingMemoryTool < FastMcp::Tool
85
+ description "Get all working memory contents for the current robot. Use this to restore a previous session."
86
+
87
+ arguments do
88
+ end
89
+
90
+ def call
91
+ htm = Session.htm_instance
92
+ robot = HTM::Models::Robot.find(htm.robot_id)
93
+ Session.logger&.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
94
+
95
+ # Get all nodes in working memory with their metadata
96
+ # Filter out any robot_nodes where the node has been deleted (node uses default_scope)
97
+ working_memory_nodes = robot.robot_nodes
98
+ .in_working_memory
99
+ .joins(:node) # Inner join excludes deleted nodes
100
+ .includes(node: :tags)
101
+ .order(last_remembered_at: :desc)
102
+ .filter_map do |rn|
103
+ next unless rn.node # Extra safety check
104
+
105
+ {
106
+ id: rn.node.id,
107
+ content: rn.node.content,
108
+ tags: rn.node.tags.map(&:name),
109
+ remember_count: rn.remember_count,
110
+ last_remembered_at: rn.last_remembered_at&.iso8601,
111
+ created_at: rn.node.created_at.iso8601
112
+ }
113
+ end
114
+
115
+ Session.logger&.info "GetWorkingMemoryTool complete: #{working_memory_nodes.length} nodes in working memory"
116
+
117
+ {
118
+ success: true,
119
+ robot_id: htm.robot_id,
120
+ robot_name: htm.robot_name,
121
+ count: working_memory_nodes.length,
122
+ working_memory: working_memory_nodes
123
+ }.to_json
124
+ rescue StandardError => e
125
+ Session.logger&.error "GetWorkingMemoryTool error: #{e.message}"
126
+ { success: false, error: e.message, count: 0, working_memory: [] }.to_json
127
+ end
128
+ end
129
+
130
+ # Tool: Remember information
131
+ class RememberTool < FastMcp::Tool
132
+ description "Store information in HTM long-term memory with optional tags"
133
+
134
+ arguments do
135
+ required(:content).filled(:string).description("The content to remember")
136
+ optional(:tags).array(:string).description("Optional tags for categorization (e.g., ['database:postgresql', 'config'])")
137
+ optional(:metadata).hash.description("Optional metadata key-value pairs")
138
+ end
139
+
140
+ def call(content:, tags: [], metadata: {})
141
+ Session.logger&.info "RememberTool called: content=#{content[0..50].inspect}..."
142
+
143
+ htm = Session.htm_instance
144
+ node_id = htm.remember(content, tags: tags, metadata: metadata)
145
+ node = HTM::Models::Node.includes(:tags).find(node_id)
146
+
147
+ Session.logger&.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
148
+
149
+ {
150
+ success: true,
151
+ node_id: node_id,
152
+ robot_id: htm.robot_id,
153
+ robot_name: htm.robot_name,
154
+ content: node.content,
155
+ tags: node.tags.map(&:name),
156
+ created_at: node.created_at.iso8601
157
+ }.to_json
158
+ end
159
+ end
160
+
161
+ # Tool: Recall memories
162
+ class RecallTool < FastMcp::Tool
163
+ description "Search and retrieve memories from HTM using semantic, full-text, or hybrid search"
164
+
165
+ arguments do
166
+ required(:query).filled(:string).description("Search query - can be natural language or keywords")
167
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 10)")
168
+ optional(:strategy).filled(:string).description("Search strategy: 'vector', 'fulltext', or 'hybrid' (default: 'hybrid')")
169
+ optional(:timeframe).filled(:string).description("Filter by time: 'today', 'this week', 'this month', or ISO8601 date range")
170
+ end
171
+
172
+ def call(query:, limit: 10, strategy: 'hybrid', timeframe: nil)
173
+ htm = Session.htm_instance
174
+ Session.logger&.info "RecallTool called: query=#{query.inspect}, strategy=#{strategy}, limit=#{limit}, robot=#{htm.robot_name}"
175
+
176
+ recall_opts = {
177
+ limit: limit,
178
+ strategy: strategy.to_sym,
179
+ raw: true
180
+ }
181
+
182
+ # Parse timeframe if provided
183
+ if timeframe
184
+ recall_opts[:timeframe] = parse_timeframe(timeframe)
185
+ end
186
+
187
+ memories = htm.recall(query, **recall_opts)
188
+
189
+ results = memories.map do |memory|
190
+ node = HTM::Models::Node.includes(:tags).find(memory['id'])
191
+ {
192
+ id: memory['id'],
193
+ content: memory['content'],
194
+ tags: node.tags.map(&:name),
195
+ created_at: memory['created_at'],
196
+ score: memory['combined_score'] || memory['similarity']
197
+ }
198
+ end
199
+
200
+ Session.logger&.info "Recall complete: found #{results.length} memories"
201
+
202
+ {
203
+ success: true,
204
+ query: query,
205
+ strategy: strategy,
206
+ robot_name: htm.robot_name,
207
+ count: results.length,
208
+ results: results
209
+ }.to_json
210
+ end
211
+
212
+ private
213
+
214
+ def parse_timeframe(timeframe)
215
+ case timeframe.downcase
216
+ when 'today'
217
+ Time.now.beginning_of_day..Time.now
218
+ when 'this week'
219
+ 1.week.ago..Time.now
220
+ when 'this month'
221
+ 1.month.ago..Time.now
222
+ else
223
+ # Try to parse as ISO8601 range (start..end)
224
+ if timeframe.include?('..')
225
+ parts = timeframe.split('..')
226
+ Time.parse(parts[0])..Time.parse(parts[1])
227
+ else
228
+ # Single date - from that date to now
229
+ Time.parse(timeframe)..Time.now
230
+ end
231
+ end
232
+ rescue ArgumentError
233
+ nil # Invalid timeframe, skip filtering
234
+ end
235
+ end
236
+
237
+ # Tool: Forget a memory
238
+ class ForgetTool < FastMcp::Tool
239
+ description "Soft-delete a memory from HTM (can be restored later)"
240
+
241
+ arguments do
242
+ required(:node_id).filled(:integer).description("The ID of the node to forget")
243
+ end
244
+
245
+ def call(node_id:)
246
+ htm = Session.htm_instance
247
+ Session.logger&.info "ForgetTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
248
+
249
+ htm.forget(node_id)
250
+
251
+ Session.logger&.info "Memory soft-deleted: node_id=#{node_id}"
252
+
253
+ {
254
+ success: true,
255
+ node_id: node_id,
256
+ robot_name: htm.robot_name,
257
+ message: "Memory soft-deleted. Use restore to recover."
258
+ }.to_json
259
+ rescue HTM::NotFoundError, ActiveRecord::RecordNotFound
260
+ Session.logger&.warn "ForgetTool failed: node #{node_id} not found"
261
+ {
262
+ success: false,
263
+ error: "Node #{node_id} not found"
264
+ }.to_json
265
+ end
266
+ end
267
+
268
+ # Tool: Restore a forgotten memory
269
+ class RestoreTool < FastMcp::Tool
270
+ description "Restore a soft-deleted memory"
271
+
272
+ arguments do
273
+ required(:node_id).filled(:integer).description("The ID of the node to restore")
274
+ end
275
+
276
+ def call(node_id:)
277
+ htm = Session.htm_instance
278
+ Session.logger&.info "RestoreTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
279
+
280
+ htm.restore(node_id)
281
+
282
+ Session.logger&.info "Memory restored: node_id=#{node_id}"
283
+
284
+ {
285
+ success: true,
286
+ node_id: node_id,
287
+ robot_name: htm.robot_name,
288
+ message: "Memory restored successfully"
289
+ }.to_json
290
+ rescue HTM::NotFoundError, ActiveRecord::RecordNotFound
291
+ Session.logger&.warn "RestoreTool failed: node #{node_id} not found"
292
+ {
293
+ success: false,
294
+ error: "Node #{node_id} not found"
295
+ }.to_json
296
+ end
297
+ end
298
+
299
+ # Tool: List tags
300
+ class ListTagsTool < FastMcp::Tool
301
+ description "List all tags in HTM, optionally filtered by prefix"
302
+
303
+ arguments do
304
+ optional(:prefix).filled(:string).description("Filter tags by prefix (e.g., 'database' returns 'database:postgresql', etc.)")
305
+ end
306
+
307
+ def call(prefix: nil)
308
+ Session.logger&.info "ListTagsTool called: prefix=#{prefix.inspect}"
309
+
310
+ tags_query = HTM::Models::Tag.order(:name)
311
+ tags_query = tags_query.where("name LIKE ?", "#{prefix}%") if prefix
312
+
313
+ tags = tags_query.map do |tag|
314
+ {
315
+ name: tag.name,
316
+ node_count: tag.nodes.count
317
+ }
318
+ end
319
+
320
+ Session.logger&.info "ListTagsTool complete: found #{tags.length} tags"
321
+
322
+ {
323
+ success: true,
324
+ prefix: prefix,
325
+ count: tags.length,
326
+ tags: tags
327
+ }.to_json
328
+ end
329
+ end
330
+
331
+ # Tool: Search tags with fuzzy matching
332
+ class SearchTagsTool < FastMcp::Tool
333
+ description "Search for tags using fuzzy matching (typo-tolerant). Use this when you're unsure of exact tag names."
334
+
335
+ arguments do
336
+ required(:query).filled(:string).description("Search query - can contain typos (e.g., 'postgrsql' finds 'database:postgresql')")
337
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 20)")
338
+ optional(:min_similarity).filled(:float).description("Minimum similarity threshold 0.0-1.0 (default: 0.3, lower = more fuzzy)")
339
+ end
340
+
341
+ def call(query:, limit: 20, min_similarity: 0.3)
342
+ Session.logger&.info "SearchTagsTool called: query=#{query.inspect}, limit=#{limit}, min_similarity=#{min_similarity}"
343
+
344
+ htm = Session.htm_instance
345
+ ltm = htm.instance_variable_get(:@long_term_memory)
346
+
347
+ results = ltm.search_tags(query, limit: limit, min_similarity: min_similarity)
348
+
349
+ # Enrich with node counts
350
+ tags = results.map do |result|
351
+ tag = HTM::Models::Tag.find_by(name: result[:name])
352
+ {
353
+ name: result[:name],
354
+ similarity: result[:similarity].round(3),
355
+ node_count: tag&.nodes&.count || 0
356
+ }
357
+ end
358
+
359
+ Session.logger&.info "SearchTagsTool complete: found #{tags.length} tags"
360
+
361
+ {
362
+ success: true,
363
+ query: query,
364
+ min_similarity: min_similarity,
365
+ count: tags.length,
366
+ tags: tags
367
+ }.to_json
368
+ end
369
+ end
370
+
371
+ # Tool: Find nodes by topic with fuzzy option
372
+ class FindByTopicTool < FastMcp::Tool
373
+ description "Find memory nodes by topic/tag with optional fuzzy matching for typo tolerance"
374
+
375
+ arguments do
376
+ required(:topic).filled(:string).description("Topic or tag to search for (e.g., 'database:postgresql' or 'postgrsql' with fuzzy)")
377
+ optional(:fuzzy).filled(:bool).description("Enable fuzzy matching for typo tolerance (default: false)")
378
+ optional(:exact).filled(:bool).description("Require exact tag match (default: false, uses prefix matching)")
379
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 20)")
380
+ optional(:min_similarity).filled(:float).description("Minimum similarity for fuzzy mode (default: 0.3)")
381
+ end
382
+
383
+ def call(topic:, fuzzy: false, exact: false, limit: 20, min_similarity: 0.3)
384
+ Session.logger&.info "FindByTopicTool called: topic=#{topic.inspect}, fuzzy=#{fuzzy}, exact=#{exact}"
385
+
386
+ htm = Session.htm_instance
387
+ ltm = htm.instance_variable_get(:@long_term_memory)
388
+
389
+ nodes = ltm.nodes_by_topic(
390
+ topic,
391
+ fuzzy: fuzzy,
392
+ exact: exact,
393
+ min_similarity: min_similarity,
394
+ limit: limit
395
+ )
396
+
397
+ # Enrich with tags
398
+ results = nodes.map do |node_attrs|
399
+ node = HTM::Models::Node.includes(:tags).find_by(id: node_attrs['id'])
400
+ next unless node
401
+
402
+ {
403
+ id: node.id,
404
+ content: node.content[0..200],
405
+ tags: node.tags.map(&:name),
406
+ created_at: node.created_at.iso8601
407
+ }
408
+ end.compact
409
+
410
+ Session.logger&.info "FindByTopicTool complete: found #{results.length} nodes"
411
+
412
+ {
413
+ success: true,
414
+ topic: topic,
415
+ fuzzy: fuzzy,
416
+ exact: exact,
417
+ count: results.length,
418
+ results: results
419
+ }.to_json
420
+ end
421
+ end
422
+
423
+ # Tool: Get memory statistics
424
+ class StatsTool < FastMcp::Tool
425
+ description "Get statistics about HTM memory usage"
426
+
427
+ arguments do
428
+ end
429
+
430
+ def call
431
+ htm = Session.htm_instance
432
+ robot = HTM::Models::Robot.find(htm.robot_id)
433
+ Session.logger&.info "StatsTool called for robot=#{htm.robot_name}"
434
+
435
+ # Note: Node uses default_scope to exclude deleted, so .count returns active nodes
436
+ total_nodes = HTM::Models::Node.count
437
+ deleted_nodes = HTM::Models::Node.deleted.count
438
+ nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
439
+ nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
440
+ total_tags = HTM::Models::Tag.count
441
+ total_robots = HTM::Models::Robot.count
442
+
443
+ Session.logger&.info "StatsTool complete: #{total_nodes} active nodes, #{total_tags} tags"
444
+
445
+ {
446
+ success: true,
447
+ current_robot: {
448
+ name: htm.robot_name,
449
+ id: htm.robot_id,
450
+ memory_summary: robot.memory_summary
451
+ },
452
+ statistics: {
453
+ nodes: {
454
+ active: total_nodes,
455
+ deleted: deleted_nodes,
456
+ with_embeddings: nodes_with_embeddings,
457
+ with_tags: nodes_with_tags
458
+ },
459
+ tags: {
460
+ total: total_tags
461
+ },
462
+ robots: {
463
+ total: total_robots
464
+ }
465
+ }
466
+ }.to_json
467
+ rescue StandardError => e
468
+ Session.logger&.error "StatsTool error: #{e.message}"
469
+ { success: false, error: e.message }.to_json
470
+ end
471
+ end
472
+
473
+ # All individual tools for registration
474
+ TOOLS = [
475
+ SetRobotTool,
476
+ GetRobotTool,
477
+ GetWorkingMemoryTool,
478
+ RememberTool,
479
+ RecallTool,
480
+ ForgetTool,
481
+ RestoreTool,
482
+ ListTagsTool,
483
+ SearchTagsTool,
484
+ FindByTopicTool,
485
+ StatsTool
486
+ ].freeze
487
+ end
488
+ end
@@ -41,12 +41,14 @@ class HTM
41
41
  # - Floating-point rounding errors
42
42
  # - Minor timestamp discrepancies across systems
43
43
  #
44
- # @param current_mtime [Time] Current file modification time
45
- # @return [Boolean] true if file modification time differs by more than DELTA_TIME
44
+ # @param current_mtime [Time, nil] Current file modification time (defaults to reading from filesystem)
45
+ # @return [Boolean] true if file modification time differs by more than DELTA_TIME, or file doesn't exist
46
46
  #
47
- def needs_sync?(current_mtime)
47
+ def needs_sync?(current_mtime = nil)
48
48
  return true if mtime.nil?
49
+ return true unless File.exist?(file_path)
49
50
 
51
+ current_mtime ||= File.mtime(file_path)
50
52
  (current_mtime.to_i - mtime.to_i).abs > DELTA_TIME
51
53
  end
52
54
 
data/lib/htm/railtie.rb CHANGED
@@ -68,9 +68,5 @@ class HTM
68
68
  end
69
69
  end
70
70
 
71
- # Add generators path
72
- config.generators do |g|
73
- g.templates.unshift File.expand_path('../generators/templates', __dir__)
74
- end
75
71
  end
76
72
  end
data/lib/htm/tasks.rb CHANGED
@@ -8,7 +8,7 @@
8
8
  #
9
9
  # This will make the following tasks available:
10
10
  #
11
- # Database tasks (all respect RAILS_ENV, default: development):
11
+ # Database tasks (all respect HTM_ENV/RAILS_ENV, default: development):
12
12
  # rake htm:db:create # Create database if it doesn't exist
13
13
  # rake htm:db:setup # Set up HTM database schema and run migrations
14
14
  # rake htm:db:migrate # Run pending database migrations
@@ -20,10 +20,13 @@
20
20
  # rake htm:db:drop # Drop all HTM tables (destructive!)
21
21
  # rake htm:db:reset # Drop and recreate database (destructive!)
22
22
  #
23
+ # Environment detection priority: HTM_ENV > RAILS_ENV > RACK_ENV > 'development'
24
+ #
23
25
  # Examples:
24
- # RAILS_ENV=test rake htm:db:create # Create htm_test database
25
- # RAILS_ENV=test rake htm:db:setup # Setup test database with migrations
26
- # RAILS_ENV=test rake htm:db:drop # Drop test database
26
+ # HTM_ENV=test rake htm:db:create # Create htm_test database
27
+ # HTM_ENV=test rake htm:db:setup # Setup test database with migrations
28
+ # HTM_ENV=test rake htm:db:drop # Drop test database
29
+ # RAILS_ENV=test rake htm:db:create # Also works (for Rails apps)
27
30
  #
28
31
  # Async job tasks:
29
32
  # rake htm:jobs:stats # Show async job statistics
data/lib/htm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HTM
4
- VERSION = '0.0.15'
4
+ VERSION = '0.0.17'
5
5
  end
data/lib/tasks/htm.rake CHANGED
@@ -68,14 +68,13 @@ namespace :htm do
68
68
  end
69
69
  end
70
70
 
71
- desc "Verify database connection (respects RAILS_ENV)"
71
+ desc "Verify database connection (respects HTM_ENV/RAILS_ENV)"
72
72
  task :verify do
73
73
  require 'htm'
74
74
 
75
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
76
75
  config = HTM::ActiveRecordConfig.load_database_config
77
76
 
78
- puts "Verifying HTM database connection (#{env})..."
77
+ puts "Verifying HTM database connection (#{HTM.env})..."
79
78
  puts " Host: #{config[:host]}"
80
79
  puts " Port: #{config[:port]}"
81
80
  puts " Database: #{config[:database]}"
@@ -107,14 +106,13 @@ namespace :htm do
107
106
  end
108
107
  end
109
108
 
110
- desc "Open PostgreSQL console (respects RAILS_ENV)"
109
+ desc "Open PostgreSQL console (respects HTM_ENV/RAILS_ENV)"
111
110
  task :console do
112
111
  require 'htm'
113
112
 
114
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
115
113
  config = HTM::ActiveRecordConfig.load_database_config
116
114
 
117
- puts "Connecting to #{config[:database]} (#{env})..."
115
+ puts "Connecting to #{config[:database]} (#{HTM.env})..."
118
116
  exec "psql", "-h", config[:host],
119
117
  "-p", config[:port].to_s,
120
118
  "-U", config[:username],
@@ -416,15 +414,14 @@ namespace :htm do
416
414
  end
417
415
  end
418
416
 
419
- desc "Create database if it doesn't exist (respects RAILS_ENV)"
417
+ desc "Create database if it doesn't exist (respects HTM_ENV/RAILS_ENV)"
420
418
  task :create do
421
419
  require 'htm'
422
420
 
423
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
424
421
  config = HTM::ActiveRecordConfig.load_database_config
425
422
  db_name = config[:database]
426
423
 
427
- puts "Creating database: #{db_name} (#{env})"
424
+ puts "Creating database: #{db_name} (#{HTM.env})"
428
425
 
429
426
  admin_config = config.dup
430
427
  admin_config[:database] = 'postgres'