htm 0.0.10 → 0.0.14

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/.dictate.toml +46 -0
  3. data/.envrc +2 -0
  4. data/CHANGELOG.md +86 -3
  5. data/README.md +86 -7
  6. data/Rakefile +14 -2
  7. data/bin/htm_mcp.rb +621 -0
  8. data/config/database.yml +20 -13
  9. data/db/migrate/00010_add_soft_delete_to_associations.rb +29 -0
  10. data/db/migrate/00011_add_performance_indexes.rb +21 -0
  11. data/db/migrate/00012_add_tags_trigram_index.rb +18 -0
  12. data/db/migrate/00013_enable_lz4_compression.rb +43 -0
  13. data/db/schema.sql +49 -92
  14. data/docs/api/index.md +1 -1
  15. data/docs/api/yard/HTM.md +2 -4
  16. data/docs/architecture/index.md +1 -1
  17. data/docs/development/index.md +1 -1
  18. data/docs/getting-started/index.md +1 -1
  19. data/docs/guides/index.md +1 -1
  20. data/docs/images/telemetry-architecture.svg +153 -0
  21. data/docs/telemetry.md +391 -0
  22. data/examples/README.md +171 -1
  23. data/examples/cli_app/README.md +1 -1
  24. data/examples/cli_app/htm_cli.rb +1 -1
  25. data/examples/mcp_client.rb +529 -0
  26. data/examples/sinatra_app/app.rb +1 -1
  27. data/examples/telemetry/README.md +147 -0
  28. data/examples/telemetry/SETUP_README.md +169 -0
  29. data/examples/telemetry/demo.rb +498 -0
  30. data/examples/telemetry/grafana/dashboards/htm-metrics.json +457 -0
  31. data/lib/htm/configuration.rb +261 -70
  32. data/lib/htm/database.rb +46 -22
  33. data/lib/htm/embedding_service.rb +24 -14
  34. data/lib/htm/errors.rb +15 -1
  35. data/lib/htm/jobs/generate_embedding_job.rb +19 -0
  36. data/lib/htm/jobs/generate_propositions_job.rb +103 -0
  37. data/lib/htm/jobs/generate_tags_job.rb +24 -0
  38. data/lib/htm/loaders/markdown_chunker.rb +79 -0
  39. data/lib/htm/loaders/markdown_loader.rb +41 -15
  40. data/lib/htm/long_term_memory/fulltext_search.rb +138 -0
  41. data/lib/htm/long_term_memory/hybrid_search.rb +324 -0
  42. data/lib/htm/long_term_memory/node_operations.rb +209 -0
  43. data/lib/htm/long_term_memory/relevance_scorer.rb +355 -0
  44. data/lib/htm/long_term_memory/robot_operations.rb +34 -0
  45. data/lib/htm/long_term_memory/tag_operations.rb +428 -0
  46. data/lib/htm/long_term_memory/vector_search.rb +109 -0
  47. data/lib/htm/long_term_memory.rb +51 -1153
  48. data/lib/htm/models/node.rb +35 -2
  49. data/lib/htm/models/node_tag.rb +31 -0
  50. data/lib/htm/models/robot_node.rb +31 -0
  51. data/lib/htm/models/tag.rb +44 -0
  52. data/lib/htm/proposition_service.rb +169 -0
  53. data/lib/htm/query_cache.rb +214 -0
  54. data/lib/htm/sql_builder.rb +178 -0
  55. data/lib/htm/tag_service.rb +16 -6
  56. data/lib/htm/tasks.rb +8 -2
  57. data/lib/htm/telemetry.rb +224 -0
  58. data/lib/htm/version.rb +1 -1
  59. data/lib/htm.rb +64 -3
  60. data/lib/tasks/doc.rake +1 -1
  61. data/lib/tasks/htm.rake +259 -13
  62. data/mkdocs.yml +96 -96
  63. metadata +75 -18
  64. data/.aigcm_msg +0 -1
  65. data/.claude/settings.local.json +0 -92
  66. data/CLAUDE.md +0 -603
  67. data/examples/cli_app/temp.log +0 -93
  68. data/lib/htm/loaders/paragraph_chunker.rb +0 -112
  69. data/notes/ARCHITECTURE_REVIEW.md +0 -1167
  70. data/notes/IMPLEMENTATION_SUMMARY.md +0 -606
  71. data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +0 -451
  72. data/notes/next_steps.md +0 -100
  73. data/notes/plan.md +0 -627
  74. data/notes/tag_ontology_enhancement_ideas.md +0 -222
  75. data/notes/timescaledb_removal_summary.md +0 -200
data/bin/htm_mcp.rb ADDED
@@ -0,0 +1,621 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # htm/bin/htm_mcp.rb
4
+
5
+ # MCP Server Example for HTM
6
+ #
7
+ # This example demonstrates using FastMCP to expose HTM's memory
8
+ # capabilities as an MCP (Model Context Protocol) server that can
9
+ # be used by AI assistants like Claude Desktop.
10
+ #
11
+ # Prerequisites:
12
+ # 1. Install fast-mcp gem: gem install fast-mcp
13
+ # 2. Set HTM_DBURL environment variable
14
+ # 3. Initialize database schema: rake db_setup
15
+ #
16
+ # Usage:
17
+ # ruby examples/mcp_server.rb
18
+ #
19
+ # The server uses STDIO transport by default, making it compatible
20
+ # with Claude Desktop and other MCP clients.
21
+ #
22
+ # Claude Desktop configuration (~/.config/claude/claude_desktop_config.json):
23
+ # {
24
+ # "mcpServers": {
25
+ # "htm-memory": {
26
+ # "command": "ruby",
27
+ # "args": ["/path/to/htm/examples/mcp_server.rb"],
28
+ # "env": {
29
+ # "HTM_DBURL": "postgresql://postgres@localhost:5432/htm_development"
30
+ # }
31
+ # }
32
+ # }
33
+ # }
34
+
35
+ require_relative '../lib/htm'
36
+
37
+ begin
38
+ require 'fast_mcp'
39
+ rescue LoadError
40
+ warn "Error: fast-mcp gem not found."
41
+ warn "Install it with: gem install fast-mcp"
42
+ exit 1
43
+ end
44
+
45
+ # Check environment
46
+ unless ENV['HTM_DBURL']
47
+ warn "Error: HTM_DBURL not set."
48
+ warn " export HTM_DBURL=\"postgresql://postgres@localhost:5432/htm_development\""
49
+ exit 1
50
+ end
51
+
52
+ # IMPORTANT: MCP uses STDIO for JSON-RPC communication.
53
+ # ALL logging must go to STDERR to avoid corrupting the protocol.
54
+ # Create a logger that writes to STDERR for MCP server diagnostics.
55
+ MCP_STDERR_LOG = Logger.new($stderr)
56
+ MCP_STDERR_LOG.level = Logger::INFO
57
+ MCP_STDERR_LOG.formatter = proc do |severity, datetime, _progname, msg|
58
+ "[MCP #{severity}] #{datetime.strftime('%H:%M:%S')} #{msg}\n"
59
+ end
60
+
61
+ # Silent logger for RubyLLM/HTM internals (prevents STDOUT corruption)
62
+ mcp_logger = Logger.new(IO::NULL)
63
+
64
+ # Configure RubyLLM to not log to STDOUT (corrupts MCP protocol)
65
+ require 'ruby_llm'
66
+ RubyLLM.configure do |config|
67
+ config.logger = mcp_logger
68
+ end
69
+
70
+ # Configure HTM
71
+ HTM.configure do |config|
72
+ config.job_backend = :inline # Synchronous for MCP responses
73
+ config.logger = mcp_logger # Silent logging for MCP
74
+ end
75
+
76
+ # Session state for the current robot
77
+ # Each MCP client spawns its own server process, so this is naturally isolated
78
+ module MCPSession
79
+ DEFAULT_ROBOT_NAME = "mcp_default"
80
+
81
+ class << self
82
+ def htm_instance
83
+ @htm_instance ||= HTM.new(robot_name: DEFAULT_ROBOT_NAME)
84
+ end
85
+
86
+ def set_robot(name)
87
+ @robot_name = name
88
+ @htm_instance = HTM.new(robot_name: name)
89
+ MCP_STDERR_LOG.info "Robot set: #{name} (id=#{@htm_instance.robot_id})"
90
+ @htm_instance
91
+ end
92
+
93
+ def robot_name
94
+ @robot_name || DEFAULT_ROBOT_NAME
95
+ end
96
+
97
+ def robot_initialized?
98
+ @robot_name != nil
99
+ end
100
+ end
101
+ end
102
+
103
+ # Tool: Set the robot identity for this session
104
+ class SetRobotTool < FastMcp::Tool
105
+ description "Set the robot identity for this session. Call this first to establish your robot name."
106
+
107
+ arguments do
108
+ required(:name).filled(:string).description("The robot name (will be created if it doesn't exist)")
109
+ end
110
+
111
+ def call(name:)
112
+ MCP_STDERR_LOG.info "SetRobotTool called: name=#{name.inspect}"
113
+
114
+ htm = MCPSession.set_robot(name)
115
+ robot = HTM::Models::Robot.find(htm.robot_id)
116
+
117
+ {
118
+ success: true,
119
+ robot_id: htm.robot_id,
120
+ robot_name: htm.robot_name,
121
+ node_count: robot.node_count,
122
+ message: "Robot '#{name}' is now active for this session"
123
+ }.to_json
124
+ end
125
+ end
126
+
127
+ # Tool: Get current robot info
128
+ class GetRobotTool < FastMcp::Tool
129
+ description "Get information about the current robot for this session"
130
+
131
+ arguments do
132
+ end
133
+
134
+ def call
135
+ MCP_STDERR_LOG.info "GetRobotTool called"
136
+
137
+ htm = MCPSession.htm_instance
138
+ robot = HTM::Models::Robot.find(htm.robot_id)
139
+
140
+ {
141
+ success: true,
142
+ robot_id: htm.robot_id,
143
+ robot_name: htm.robot_name,
144
+ initialized: MCPSession.robot_initialized?,
145
+ memory_summary: robot.memory_summary
146
+ }.to_json
147
+ end
148
+ end
149
+
150
+ # Tool: Get working memory contents for session restore
151
+ class GetWorkingMemoryTool < FastMcp::Tool
152
+ description "Get all working memory contents for the current robot. Use this to restore a previous session."
153
+
154
+ arguments do
155
+ end
156
+
157
+ def call
158
+ htm = MCPSession.htm_instance
159
+ robot = HTM::Models::Robot.find(htm.robot_id)
160
+ MCP_STDERR_LOG.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
161
+
162
+ # Get all nodes in working memory with their metadata
163
+ # Filter out any robot_nodes where the node has been deleted (node uses default_scope)
164
+ working_memory_nodes = robot.robot_nodes
165
+ .in_working_memory
166
+ .joins(:node) # Inner join excludes deleted nodes
167
+ .includes(node: :tags)
168
+ .order(last_remembered_at: :desc)
169
+ .filter_map do |rn|
170
+ next unless rn.node # Extra safety check
171
+
172
+ {
173
+ id: rn.node.id,
174
+ content: rn.node.content,
175
+ tags: rn.node.tags.map(&:name),
176
+ remember_count: rn.remember_count,
177
+ last_remembered_at: rn.last_remembered_at&.iso8601,
178
+ created_at: rn.node.created_at.iso8601
179
+ }
180
+ end
181
+
182
+ MCP_STDERR_LOG.info "GetWorkingMemoryTool complete: #{working_memory_nodes.length} nodes in working memory"
183
+
184
+ {
185
+ success: true,
186
+ robot_id: htm.robot_id,
187
+ robot_name: htm.robot_name,
188
+ count: working_memory_nodes.length,
189
+ working_memory: working_memory_nodes
190
+ }.to_json
191
+ rescue StandardError => e
192
+ MCP_STDERR_LOG.error "GetWorkingMemoryTool error: #{e.message}"
193
+ { success: false, error: e.message, count: 0, working_memory: [] }.to_json
194
+ end
195
+ end
196
+
197
+ # Tool: Remember information
198
+ class RememberTool < FastMcp::Tool
199
+ description "Store information in HTM long-term memory with optional tags"
200
+
201
+ arguments do
202
+ required(:content).filled(:string).description("The content to remember")
203
+ optional(:tags).array(:string).description("Optional tags for categorization (e.g., ['database:postgresql', 'config'])")
204
+ optional(:metadata).hash.description("Optional metadata key-value pairs")
205
+ end
206
+
207
+ def call(content:, tags: [], metadata: {})
208
+ MCP_STDERR_LOG.info "RememberTool called: content=#{content[0..50].inspect}..."
209
+
210
+ htm = MCPSession.htm_instance
211
+ node_id = htm.remember(content, tags: tags, metadata: metadata)
212
+ node = HTM::Models::Node.includes(:tags).find(node_id)
213
+
214
+ MCP_STDERR_LOG.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
215
+
216
+ {
217
+ success: true,
218
+ node_id: node_id,
219
+ robot_id: htm.robot_id,
220
+ robot_name: htm.robot_name,
221
+ content: node.content,
222
+ tags: node.tags.map(&:name),
223
+ created_at: node.created_at.iso8601
224
+ }.to_json
225
+ end
226
+ end
227
+
228
+ # Tool: Recall memories
229
+ class RecallTool < FastMcp::Tool
230
+ description "Search and retrieve memories from HTM using semantic, full-text, or hybrid search"
231
+
232
+ arguments do
233
+ required(:query).filled(:string).description("Search query - can be natural language or keywords")
234
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 10)")
235
+ optional(:strategy).filled(:string).description("Search strategy: 'vector', 'fulltext', or 'hybrid' (default: 'hybrid')")
236
+ optional(:timeframe).filled(:string).description("Filter by time: 'today', 'this week', 'this month', or ISO8601 date range")
237
+ end
238
+
239
+ def call(query:, limit: 10, strategy: 'hybrid', timeframe: nil)
240
+ htm = MCPSession.htm_instance
241
+ MCP_STDERR_LOG.info "RecallTool called: query=#{query.inspect}, strategy=#{strategy}, limit=#{limit}, robot=#{htm.robot_name}"
242
+
243
+ recall_opts = {
244
+ limit: limit,
245
+ strategy: strategy.to_sym,
246
+ raw: true
247
+ }
248
+
249
+ # Parse timeframe if provided
250
+ if timeframe
251
+ recall_opts[:timeframe] = parse_timeframe(timeframe)
252
+ end
253
+
254
+ memories = htm.recall(query, **recall_opts)
255
+
256
+ results = memories.map do |memory|
257
+ node = HTM::Models::Node.includes(:tags).find(memory['id'])
258
+ {
259
+ id: memory['id'],
260
+ content: memory['content'],
261
+ tags: node.tags.map(&:name),
262
+ created_at: memory['created_at'],
263
+ score: memory['combined_score'] || memory['similarity']
264
+ }
265
+ end
266
+
267
+ MCP_STDERR_LOG.info "Recall complete: found #{results.length} memories"
268
+
269
+ {
270
+ success: true,
271
+ query: query,
272
+ strategy: strategy,
273
+ robot_name: htm.robot_name,
274
+ count: results.length,
275
+ results: results
276
+ }.to_json
277
+ end
278
+
279
+ private
280
+
281
+ def parse_timeframe(timeframe)
282
+ case timeframe.downcase
283
+ when 'today'
284
+ Time.now.beginning_of_day..Time.now
285
+ when 'this week'
286
+ 1.week.ago..Time.now
287
+ when 'this month'
288
+ 1.month.ago..Time.now
289
+ else
290
+ # Try to parse as ISO8601 range (start..end)
291
+ if timeframe.include?('..')
292
+ parts = timeframe.split('..')
293
+ Time.parse(parts[0])..Time.parse(parts[1])
294
+ else
295
+ # Single date - from that date to now
296
+ Time.parse(timeframe)..Time.now
297
+ end
298
+ end
299
+ rescue ArgumentError
300
+ nil # Invalid timeframe, skip filtering
301
+ end
302
+ end
303
+
304
+ # Tool: Forget a memory
305
+ class ForgetTool < FastMcp::Tool
306
+ description "Soft-delete a memory from HTM (can be restored later)"
307
+
308
+ arguments do
309
+ required(:node_id).filled(:integer).description("The ID of the node to forget")
310
+ end
311
+
312
+ def call(node_id:)
313
+ htm = MCPSession.htm_instance
314
+ MCP_STDERR_LOG.info "ForgetTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
315
+
316
+ htm.forget(node_id)
317
+
318
+ MCP_STDERR_LOG.info "Memory soft-deleted: node_id=#{node_id}"
319
+
320
+ {
321
+ success: true,
322
+ node_id: node_id,
323
+ robot_name: htm.robot_name,
324
+ message: "Memory soft-deleted. Use restore to recover."
325
+ }.to_json
326
+ rescue ActiveRecord::RecordNotFound
327
+ MCP_STDERR_LOG.warn "ForgetTool failed: node #{node_id} not found"
328
+ {
329
+ success: false,
330
+ error: "Node #{node_id} not found"
331
+ }.to_json
332
+ end
333
+ end
334
+
335
+ # Tool: Restore a forgotten memory
336
+ class RestoreTool < FastMcp::Tool
337
+ description "Restore a soft-deleted memory"
338
+
339
+ arguments do
340
+ required(:node_id).filled(:integer).description("The ID of the node to restore")
341
+ end
342
+
343
+ def call(node_id:)
344
+ htm = MCPSession.htm_instance
345
+ MCP_STDERR_LOG.info "RestoreTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
346
+
347
+ htm.restore(node_id)
348
+
349
+ MCP_STDERR_LOG.info "Memory restored: node_id=#{node_id}"
350
+
351
+ {
352
+ success: true,
353
+ node_id: node_id,
354
+ robot_name: htm.robot_name,
355
+ message: "Memory restored successfully"
356
+ }.to_json
357
+ rescue ActiveRecord::RecordNotFound
358
+ MCP_STDERR_LOG.warn "RestoreTool failed: node #{node_id} not found"
359
+ {
360
+ success: false,
361
+ error: "Node #{node_id} not found"
362
+ }.to_json
363
+ end
364
+ end
365
+
366
+ # Tool: List tags
367
+ class ListTagsTool < FastMcp::Tool
368
+ description "List all tags in HTM, optionally filtered by prefix"
369
+
370
+ arguments do
371
+ optional(:prefix).filled(:string).description("Filter tags by prefix (e.g., 'database' returns 'database:postgresql', etc.)")
372
+ end
373
+
374
+ def call(prefix: nil)
375
+ MCP_STDERR_LOG.info "ListTagsTool called: prefix=#{prefix.inspect}"
376
+
377
+ tags_query = HTM::Models::Tag.order(:name)
378
+ tags_query = tags_query.where("name LIKE ?", "#{prefix}%") if prefix
379
+
380
+ tags = tags_query.map do |tag|
381
+ {
382
+ name: tag.name,
383
+ node_count: tag.nodes.count
384
+ }
385
+ end
386
+
387
+ MCP_STDERR_LOG.info "ListTagsTool complete: found #{tags.length} tags"
388
+
389
+ {
390
+ success: true,
391
+ prefix: prefix,
392
+ count: tags.length,
393
+ tags: tags
394
+ }.to_json
395
+ end
396
+ end
397
+
398
+ # Tool: Search tags with fuzzy matching
399
+ class SearchTagsTool < FastMcp::Tool
400
+ description "Search for tags using fuzzy matching (typo-tolerant). Use this when you're unsure of exact tag names."
401
+
402
+ arguments do
403
+ required(:query).filled(:string).description("Search query - can contain typos (e.g., 'postgrsql' finds 'database:postgresql')")
404
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 20)")
405
+ optional(:min_similarity).filled(:float).description("Minimum similarity threshold 0.0-1.0 (default: 0.3, lower = more fuzzy)")
406
+ end
407
+
408
+ def call(query:, limit: 20, min_similarity: 0.3)
409
+ MCP_STDERR_LOG.info "SearchTagsTool called: query=#{query.inspect}, limit=#{limit}, min_similarity=#{min_similarity}"
410
+
411
+ htm = MCPSession.htm_instance
412
+ ltm = htm.instance_variable_get(:@long_term_memory)
413
+
414
+ results = ltm.search_tags(query, limit: limit, min_similarity: min_similarity)
415
+
416
+ # Enrich with node counts
417
+ tags = results.map do |result|
418
+ tag = HTM::Models::Tag.find_by(name: result[:name])
419
+ {
420
+ name: result[:name],
421
+ similarity: result[:similarity].round(3),
422
+ node_count: tag&.nodes&.count || 0
423
+ }
424
+ end
425
+
426
+ MCP_STDERR_LOG.info "SearchTagsTool complete: found #{tags.length} tags"
427
+
428
+ {
429
+ success: true,
430
+ query: query,
431
+ min_similarity: min_similarity,
432
+ count: tags.length,
433
+ tags: tags
434
+ }.to_json
435
+ end
436
+ end
437
+
438
+ # Tool: Find nodes by topic with fuzzy option
439
+ class FindByTopicTool < FastMcp::Tool
440
+ description "Find memory nodes by topic/tag with optional fuzzy matching for typo tolerance"
441
+
442
+ arguments do
443
+ required(:topic).filled(:string).description("Topic or tag to search for (e.g., 'database:postgresql' or 'postgrsql' with fuzzy)")
444
+ optional(:fuzzy).filled(:bool).description("Enable fuzzy matching for typo tolerance (default: false)")
445
+ optional(:exact).filled(:bool).description("Require exact tag match (default: false, uses prefix matching)")
446
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 20)")
447
+ optional(:min_similarity).filled(:float).description("Minimum similarity for fuzzy mode (default: 0.3)")
448
+ end
449
+
450
+ def call(topic:, fuzzy: false, exact: false, limit: 20, min_similarity: 0.3)
451
+ MCP_STDERR_LOG.info "FindByTopicTool called: topic=#{topic.inspect}, fuzzy=#{fuzzy}, exact=#{exact}"
452
+
453
+ htm = MCPSession.htm_instance
454
+ ltm = htm.instance_variable_get(:@long_term_memory)
455
+
456
+ nodes = ltm.nodes_by_topic(
457
+ topic,
458
+ fuzzy: fuzzy,
459
+ exact: exact,
460
+ min_similarity: min_similarity,
461
+ limit: limit
462
+ )
463
+
464
+ # Enrich with tags
465
+ results = nodes.map do |node_attrs|
466
+ node = HTM::Models::Node.includes(:tags).find_by(id: node_attrs['id'])
467
+ next unless node
468
+
469
+ {
470
+ id: node.id,
471
+ content: node.content[0..200],
472
+ tags: node.tags.map(&:name),
473
+ created_at: node.created_at.iso8601
474
+ }
475
+ end.compact
476
+
477
+ MCP_STDERR_LOG.info "FindByTopicTool complete: found #{results.length} nodes"
478
+
479
+ {
480
+ success: true,
481
+ topic: topic,
482
+ fuzzy: fuzzy,
483
+ exact: exact,
484
+ count: results.length,
485
+ results: results
486
+ }.to_json
487
+ end
488
+ end
489
+
490
+ # Tool: Get memory statistics
491
+ class StatsTool < FastMcp::Tool
492
+ description "Get statistics about HTM memory usage"
493
+
494
+ arguments do
495
+ end
496
+
497
+ def call
498
+ htm = MCPSession.htm_instance
499
+ robot = HTM::Models::Robot.find(htm.robot_id)
500
+ MCP_STDERR_LOG.info "StatsTool called for robot=#{htm.robot_name}"
501
+
502
+ # Note: Node uses default_scope to exclude deleted, so .count returns active nodes
503
+ total_nodes = HTM::Models::Node.count
504
+ deleted_nodes = HTM::Models::Node.deleted.count
505
+ nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
506
+ nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
507
+ total_tags = HTM::Models::Tag.count
508
+ total_robots = HTM::Models::Robot.count
509
+
510
+ MCP_STDERR_LOG.info "StatsTool complete: #{total_nodes} active nodes, #{total_tags} tags"
511
+
512
+ {
513
+ success: true,
514
+ current_robot: {
515
+ name: htm.robot_name,
516
+ id: htm.robot_id,
517
+ memory_summary: robot.memory_summary
518
+ },
519
+ statistics: {
520
+ nodes: {
521
+ active: total_nodes,
522
+ deleted: deleted_nodes,
523
+ with_embeddings: nodes_with_embeddings,
524
+ with_tags: nodes_with_tags
525
+ },
526
+ tags: {
527
+ total: total_tags
528
+ },
529
+ robots: {
530
+ total: total_robots
531
+ }
532
+ }
533
+ }.to_json
534
+ rescue StandardError => e
535
+ MCP_STDERR_LOG.error "StatsTool error: #{e.message}"
536
+ { success: false, error: e.message }.to_json
537
+ end
538
+ end
539
+
540
+ # Resource: Memory Statistics
541
+ class MemoryStatsResource < FastMcp::Resource
542
+ uri "htm://statistics"
543
+ resource_name "HTM Memory Statistics"
544
+ mime_type "application/json"
545
+
546
+ def content
547
+ htm = MCPSession.htm_instance
548
+ {
549
+ total_nodes: HTM::Models::Node.active.count,
550
+ total_tags: HTM::Models::Tag.count,
551
+ total_robots: HTM::Models::Robot.count,
552
+ current_robot: htm.robot_name,
553
+ robot_id: htm.robot_id,
554
+ robot_initialized: MCPSession.robot_initialized?,
555
+ embedding_provider: HTM.configuration.embedding_provider,
556
+ embedding_model: HTM.configuration.embedding_model
557
+ }.to_json
558
+ end
559
+ end
560
+
561
+ # Resource: Tag Hierarchy
562
+ class TagHierarchyResource < FastMcp::Resource
563
+ uri "htm://tags/hierarchy"
564
+ resource_name "HTM Tag Hierarchy"
565
+ mime_type "text/plain"
566
+
567
+ def content
568
+ HTM::Models::Tag.all.tree_string
569
+ end
570
+ end
571
+
572
+ # Resource: Recent Memories
573
+ class RecentMemoriesResource < FastMcp::Resource
574
+ uri "htm://memories/recent"
575
+ resource_name "Recent HTM Memories"
576
+ mime_type "application/json"
577
+
578
+ def content
579
+ recent = HTM::Models::Node.active
580
+ .includes(:tags)
581
+ .order(created_at: :desc)
582
+ .limit(20)
583
+ .map do |node|
584
+ {
585
+ id: node.id,
586
+ content: node.content[0..200],
587
+ tags: node.tags.map(&:name),
588
+ created_at: node.created_at.iso8601
589
+ }
590
+ end
591
+
592
+ { recent_memories: recent }.to_json
593
+ end
594
+ end
595
+
596
+ # Create and configure the MCP server
597
+ server = FastMcp::Server.new(
598
+ name: 'htm-memory-server',
599
+ version: HTM::VERSION
600
+ )
601
+
602
+ # Register tools
603
+ server.register_tool(SetRobotTool) # Call first to set robot identity
604
+ server.register_tool(GetRobotTool) # Get current robot info
605
+ server.register_tool(GetWorkingMemoryTool) # Get working memory for session restore
606
+ server.register_tool(RememberTool)
607
+ server.register_tool(RecallTool)
608
+ server.register_tool(ForgetTool)
609
+ server.register_tool(RestoreTool)
610
+ server.register_tool(ListTagsTool)
611
+ server.register_tool(SearchTagsTool) # Fuzzy tag search with typo tolerance
612
+ server.register_tool(FindByTopicTool) # Find nodes by topic with fuzzy option
613
+ server.register_tool(StatsTool)
614
+
615
+ # Register resources
616
+ server.register_resource(MemoryStatsResource)
617
+ server.register_resource(TagHierarchyResource)
618
+ server.register_resource(RecentMemoriesResource)
619
+
620
+ # Start the server (STDIO transport by default)
621
+ server.start
data/config/database.yml CHANGED
@@ -2,36 +2,46 @@
2
2
  # Uses ERB to read from environment variables
3
3
  #
4
4
  # Priority:
5
- # 1. HTM_DBURL - Full connection URL (preferred)
5
+ # 1. HTM_DBURL - Full connection URL (preferred for development/production)
6
6
  # 2. Individual HTM_DB* variables - Host, name, user, password, port
7
7
  # 3. Defaults for development/test
8
8
  #
9
9
  # Example HTM_DBURL format:
10
10
  # postgresql://user:password@host:port/database?sslmode=require
11
+ #
12
+ # Test database:
13
+ # Tests always use htm_test database (Rails convention).
14
+ # Set RAILS_ENV=test or RACK_ENV=test to use the test configuration.
11
15
 
12
16
  <%
13
17
  require 'uri'
14
-
18
+
19
+ # Determine current environment
20
+ current_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
21
+
15
22
  # Parse connection from HTM_DBURL or use individual variables
16
23
  if ENV['HTM_DBURL']
17
24
  uri = URI.parse(ENV['HTM_DBURL'])
18
25
  params = URI.decode_www_form(uri.query || '').to_h
19
-
26
+ base_database = uri.path[1..-1]
27
+
28
+ # Extract base name (remove _development, _test, _production suffixes if present)
29
+ base_name = base_database.sub(/_(development|test|production)$/, '')
30
+
20
31
  db_config = {
21
32
  'host' => uri.host,
22
33
  'port' => uri.port || 5432,
23
- 'database' => uri.path[1..-1],
34
+ 'base_name' => base_name,
24
35
  'username' => uri.user,
25
36
  'password' => uri.password,
26
37
  'sslmode' => params['sslmode'] || 'prefer'
27
38
  }
28
39
  else
29
- env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
30
40
  db_config = {
31
41
  'host' => ENV.fetch('HTM_DBHOST', 'localhost'),
32
42
  'port' => ENV.fetch('HTM_DBPORT', 5432).to_i,
33
- 'database' => ENV.fetch('HTM_DBNAME', "htm_#{env}"),
34
- 'username' => ENV.fetch('HTM_DBUSER', 'postgres'),
43
+ 'base_name' => ENV.fetch('HTM_DBNAME', 'htm').sub(/_(development|test|production)$/, ''),
44
+ 'username' => ENV.fetch('HTM_DBUSER', ENV['USER']),
35
45
  'password' => ENV.fetch('HTM_DBPASS', ''),
36
46
  'sslmode' => ENV.fetch('HTM_SSLMODE', 'prefer')
37
47
  }
@@ -53,15 +63,12 @@ default: &default
53
63
 
54
64
  development:
55
65
  <<: *default
56
- database: <%= db_config['database'] %>
66
+ database: <%= db_config['base_name'] %>_development
57
67
 
58
68
  test:
59
69
  <<: *default
60
- database: <%= db_config['database'] %>_test
70
+ database: <%= db_config['base_name'] %>_test
61
71
 
62
72
  production:
63
73
  <<: *default
64
- database: <%= db_config['database'] %>
65
- <% unless ENV['HTM_DBURL'] %>
66
- # WARNING: Production should use HTM_DBURL with SSL
67
- <% end %>
74
+ database: <%= db_config['base_name'] %>_production