htm 0.0.2 → 0.0.11

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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/.aigcm_msg +1 -0
  3. data/.architecture/reviews/comprehensive-codebase-review.md +577 -0
  4. data/.claude/settings.local.json +95 -0
  5. data/.irbrc +283 -80
  6. data/.tbls.yml +2 -1
  7. data/CHANGELOG.md +327 -26
  8. data/CLAUDE.md +603 -0
  9. data/README.md +83 -12
  10. data/Rakefile +5 -0
  11. data/bin/htm_mcp.rb +527 -0
  12. data/db/migrate/{20250101000001_enable_extensions.rb → 00001_enable_extensions.rb} +0 -1
  13. data/db/migrate/00002_create_robots.rb +11 -0
  14. data/db/migrate/00003_create_file_sources.rb +20 -0
  15. data/db/migrate/00004_create_nodes.rb +65 -0
  16. data/db/migrate/00005_create_tags.rb +13 -0
  17. data/db/migrate/00006_create_node_tags.rb +18 -0
  18. data/db/migrate/00007_create_robot_nodes.rb +26 -0
  19. data/db/migrate/00009_add_working_memory_to_robot_nodes.rb +12 -0
  20. data/db/schema.sql +172 -1
  21. data/docs/api/database.md +1 -2
  22. data/docs/api/htm.md +197 -2
  23. data/docs/api/yard/HTM/ActiveRecordConfig.md +23 -0
  24. data/docs/api/yard/HTM/AuthorizationError.md +11 -0
  25. data/docs/api/yard/HTM/CircuitBreaker.md +92 -0
  26. data/docs/api/yard/HTM/CircuitBreakerOpenError.md +34 -0
  27. data/docs/api/yard/HTM/Configuration.md +175 -0
  28. data/docs/api/yard/HTM/Database.md +99 -0
  29. data/docs/api/yard/HTM/DatabaseError.md +14 -0
  30. data/docs/api/yard/HTM/EmbeddingError.md +18 -0
  31. data/docs/api/yard/HTM/EmbeddingService.md +58 -0
  32. data/docs/api/yard/HTM/Error.md +11 -0
  33. data/docs/api/yard/HTM/JobAdapter.md +39 -0
  34. data/docs/api/yard/HTM/LongTermMemory.md +342 -0
  35. data/docs/api/yard/HTM/NotFoundError.md +17 -0
  36. data/docs/api/yard/HTM/Observability.md +107 -0
  37. data/docs/api/yard/HTM/QueryTimeoutError.md +19 -0
  38. data/docs/api/yard/HTM/Railtie.md +27 -0
  39. data/docs/api/yard/HTM/ResourceExhaustedError.md +13 -0
  40. data/docs/api/yard/HTM/TagError.md +18 -0
  41. data/docs/api/yard/HTM/TagService.md +67 -0
  42. data/docs/api/yard/HTM/Timeframe/Result.md +24 -0
  43. data/docs/api/yard/HTM/Timeframe.md +40 -0
  44. data/docs/api/yard/HTM/TimeframeExtractor/Result.md +24 -0
  45. data/docs/api/yard/HTM/TimeframeExtractor.md +45 -0
  46. data/docs/api/yard/HTM/ValidationError.md +20 -0
  47. data/docs/api/yard/HTM/WorkingMemory.md +131 -0
  48. data/docs/api/yard/HTM.md +80 -0
  49. data/docs/api/yard/index.csv +179 -0
  50. data/docs/api/yard-reference.md +51 -0
  51. data/docs/database/README.md +128 -128
  52. data/docs/database/public.file_sources.md +42 -0
  53. data/docs/database/public.file_sources.svg +211 -0
  54. data/docs/database/public.node_tags.md +4 -4
  55. data/docs/database/public.node_tags.svg +212 -79
  56. data/docs/database/public.nodes.md +22 -12
  57. data/docs/database/public.nodes.svg +246 -127
  58. data/docs/database/public.robot_nodes.md +11 -9
  59. data/docs/database/public.robot_nodes.svg +220 -98
  60. data/docs/database/public.robots.md +2 -2
  61. data/docs/database/public.robots.svg +136 -81
  62. data/docs/database/public.tags.md +3 -3
  63. data/docs/database/public.tags.svg +118 -39
  64. data/docs/database/schema.json +850 -771
  65. data/docs/database/schema.svg +256 -197
  66. data/docs/development/schema.md +67 -2
  67. data/docs/guides/adding-memories.md +93 -7
  68. data/docs/guides/recalling-memories.md +36 -1
  69. data/examples/README.md +405 -0
  70. data/examples/cli_app/htm_cli.rb +65 -5
  71. data/examples/cli_app/temp.log +93 -0
  72. data/examples/file_loader_usage.rb +177 -0
  73. data/examples/mcp_client.rb +529 -0
  74. data/examples/robot_groups/lib/robot_group.rb +419 -0
  75. data/examples/robot_groups/lib/working_memory_channel.rb +140 -0
  76. data/examples/robot_groups/multi_process.rb +286 -0
  77. data/examples/robot_groups/robot_worker.rb +136 -0
  78. data/examples/robot_groups/same_process.rb +229 -0
  79. data/examples/timeframe_demo.rb +276 -0
  80. data/lib/htm/active_record_config.rb +1 -1
  81. data/lib/htm/circuit_breaker.rb +202 -0
  82. data/lib/htm/configuration.rb +59 -13
  83. data/lib/htm/database.rb +67 -36
  84. data/lib/htm/embedding_service.rb +39 -2
  85. data/lib/htm/errors.rb +131 -11
  86. data/lib/htm/jobs/generate_embedding_job.rb +5 -4
  87. data/lib/htm/jobs/generate_tags_job.rb +4 -0
  88. data/lib/htm/loaders/markdown_loader.rb +263 -0
  89. data/lib/htm/loaders/paragraph_chunker.rb +112 -0
  90. data/lib/htm/long_term_memory.rb +460 -343
  91. data/lib/htm/models/file_source.rb +99 -0
  92. data/lib/htm/models/node.rb +80 -5
  93. data/lib/htm/models/robot.rb +24 -1
  94. data/lib/htm/models/robot_node.rb +1 -0
  95. data/lib/htm/models/tag.rb +254 -4
  96. data/lib/htm/observability.rb +395 -0
  97. data/lib/htm/tag_service.rb +60 -3
  98. data/lib/htm/tasks.rb +26 -1
  99. data/lib/htm/timeframe.rb +194 -0
  100. data/lib/htm/timeframe_extractor.rb +307 -0
  101. data/lib/htm/version.rb +1 -1
  102. data/lib/htm/working_memory.rb +165 -70
  103. data/lib/htm.rb +328 -130
  104. data/lib/tasks/doc.rake +300 -0
  105. data/lib/tasks/files.rake +299 -0
  106. data/lib/tasks/htm.rake +158 -3
  107. data/lib/tasks/jobs.rake +3 -9
  108. data/lib/tasks/tags.rake +166 -6
  109. data/mkdocs.yml +36 -1
  110. data/notes/ARCHITECTURE_REVIEW.md +1167 -0
  111. data/notes/IMPLEMENTATION_SUMMARY.md +606 -0
  112. data/notes/MULTI_FRAMEWORK_IMPLEMENTATION.md +451 -0
  113. data/notes/next_steps.md +100 -0
  114. data/notes/plan.md +627 -0
  115. data/notes/tag_ontology_enhancement_ideas.md +222 -0
  116. data/notes/timescaledb_removal_summary.md +200 -0
  117. metadata +158 -17
  118. data/db/migrate/20250101000002_create_robots.rb +0 -14
  119. data/db/migrate/20250101000003_create_nodes.rb +0 -42
  120. data/db/migrate/20250101000005_create_tags.rb +0 -38
  121. data/db/migrate/20250101000007_add_node_vector_indexes.rb +0 -30
  122. data/db/migrate/20250125000001_add_content_hash_to_nodes.rb +0 -14
  123. data/db/migrate/20250125000002_create_robot_nodes.rb +0 -35
  124. data/db/migrate/20250125000003_remove_source_and_robot_id_from_nodes.rb +0 -28
  125. data/db/migrate/20250126000001_create_working_memories.rb +0 -19
  126. data/db/migrate/20250126000002_remove_unused_columns.rb +0 -12
  127. data/docs/database/public.working_memories.md +0 -40
  128. data/docs/database/public.working_memories.svg +0 -112
  129. data/lib/htm/models/working_memory_entry.rb +0 -88
data/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  - **Client-Side Embeddings**
29
29
  - Automatic embedding generation before database insertion
30
- - Supports Ollama (local, default) and OpenAI
30
+ - Uses the [ruby_llm](https://ruby_llm.com) gem for LLM access
31
31
  - Configurable embedding providers and models
32
32
 
33
33
  - **Two-Tier Memory Architecture**
@@ -62,6 +62,12 @@
62
62
  - Tag-based categorization
63
63
  - Hierarchical tag structures
64
64
 
65
+ - **File Loading**
66
+ - Load markdown files into long-term memory
67
+ - Automatic paragraph-based chunking
68
+ - Source file tracking with re-sync support
69
+ - YAML frontmatter extraction as metadata
70
+
65
71
  ## Installation
66
72
 
67
73
  Add this line to your application's Gemfile:
@@ -264,6 +270,42 @@ memories = htm.recall(
264
270
  htm.forget(node_id, confirm: :confirmed)
265
271
  ```
266
272
 
273
+ ### Loading Files
274
+
275
+ HTM can load text-based files (currently markdown) into long-term memory with automatic chunking and source tracking.
276
+
277
+ ```ruby
278
+ htm = HTM.new(robot_name: "Document Loader")
279
+
280
+ # Load a single markdown file
281
+ result = htm.load_file("docs/guide.md")
282
+ # => { file_source_id: 1, chunks_created: 5, chunks_updated: 0, chunks_deleted: 0 }
283
+
284
+ # Load all markdown files in a directory
285
+ results = htm.load_directory("docs/", pattern: "**/*.md")
286
+
287
+ # Get nodes from a specific file
288
+ nodes = htm.nodes_from_file("docs/guide.md")
289
+
290
+ # Unload a file (soft deletes chunks)
291
+ htm.unload_file("docs/guide.md")
292
+ ```
293
+
294
+ **Features:**
295
+ - **Paragraph chunking**: Text split by blank lines, code blocks preserved
296
+ - **Source tracking**: Files tracked with mtime for automatic re-sync
297
+ - **YAML frontmatter**: Extracted and stored as metadata
298
+ - **Duplicate detection**: Content hash prevents duplicate nodes
299
+
300
+ **Rake tasks:**
301
+ ```bash
302
+ rake 'htm:files:load[docs/guide.md]' # Load a single file
303
+ rake 'htm:files:load_dir[docs/]' # Load all markdown files from directory
304
+ rake htm:files:list # List all loaded file sources
305
+ rake htm:files:sync # Sync all files (reload changed)
306
+ rake htm:files:stats # Show file loading statistics
307
+ ```
308
+
267
309
  ### Automatic Tag Extraction
268
310
 
269
311
  HTM automatically extracts hierarchical tags from content using LLM analysis. Tags are inferred from the content itself - you never specify them manually.
@@ -518,13 +560,14 @@ HTM provides a minimal, focused API with only 3 core instance methods for memory
518
560
 
519
561
  ### Core Memory Operations
520
562
 
521
- #### `remember(content, source: "")`
563
+ #### `remember(content, source: "", metadata: {})`
522
564
 
523
565
  Store information in memory. Embeddings and tags are automatically generated asynchronously.
524
566
 
525
567
  **Parameters:**
526
568
  - `content` (String, required) - The information to remember. Converted to string if nil. Returns ID of last node if empty.
527
569
  - `source` (String, optional) - Where the content came from (e.g., "user", "assistant", "system"). Defaults to empty string.
570
+ - `metadata` (Hash, optional) - Arbitrary key-value metadata stored as JSONB. Keys must be strings or symbols. Defaults to `{}`.
528
571
 
529
572
  **Returns:** Integer - The node ID of the stored memory
530
573
 
@@ -536,6 +579,12 @@ node_id = htm.remember("PostgreSQL is excellent for vector search with pgvector"
536
579
  # Store without source (uses default empty string)
537
580
  node_id = htm.remember("HTM uses two-tier memory architecture")
538
581
 
582
+ # Store with metadata
583
+ node_id = htm.remember(
584
+ "User prefers dark mode",
585
+ metadata: { category: "preference", priority: "high", version: 2 }
586
+ )
587
+
539
588
  # Nil/empty handling
540
589
  node_id = htm.remember(nil) # Returns ID of last node without creating duplicate
541
590
  node_id = htm.remember("") # Returns ID of last node without creating duplicate
@@ -543,7 +592,7 @@ node_id = htm.remember("") # Returns ID of last node without creating duplicat
543
592
 
544
593
  ---
545
594
 
546
- #### `recall(topic, timeframe: nil, limit: 20, strategy: :vector, with_relevance: false, query_tags: [])`
595
+ #### `recall(topic, timeframe: nil, limit: 20, strategy: :vector, with_relevance: false, query_tags: [], metadata: {})`
547
596
 
548
597
  Retrieve memories using temporal filtering and semantic/keyword search.
549
598
 
@@ -559,8 +608,9 @@ Retrieve memories using temporal filtering and semantic/keyword search.
559
608
  - `:hybrid` - Weighted combination (70% vector, 30% full-text)
560
609
  - `with_relevance` (Boolean, optional) - Include dynamic relevance scores. Default: false
561
610
  - `query_tags` (Array<String>, optional) - Filter results by tags. Default: []
611
+ - `metadata` (Hash, optional) - Filter results by metadata using JSONB containment (`@>`). Default: `{}`
562
612
 
563
- **Returns:** Array<Hash> - Matching memories with fields: `id`, `content`, `source`, `created_at`, `access_count`, (optionally `relevance`)
613
+ **Returns:** Array<Hash> - Matching memories with fields: `id`, `content`, `source`, `created_at`, `access_count`, `metadata`, (optionally `relevance`)
564
614
 
565
615
  **Example:**
566
616
  ```ruby
@@ -586,6 +636,21 @@ memories = htm.recall(
586
636
  query_tags: ["architecture"]
587
637
  )
588
638
  # => [{ "id" => 123, "content" => "...", "relevance" => 0.92, ... }, ...]
639
+
640
+ # Filter by metadata
641
+ memories = htm.recall(
642
+ "user preferences",
643
+ metadata: { category: "preference" }
644
+ )
645
+ # => Returns only nodes with metadata containing { category: "preference" }
646
+
647
+ # Combine metadata with other filters
648
+ memories = htm.recall(
649
+ "settings",
650
+ timeframe: "last month",
651
+ strategy: :hybrid,
652
+ metadata: { priority: "high", version: 2 }
653
+ )
589
654
  ```
590
655
 
591
656
  ---
@@ -1312,10 +1377,16 @@ See [htm_teamwork.md](htm_teamwork.md) for detailed design documentation and pla
1312
1377
  ### Database Schema
1313
1378
 
1314
1379
  - `robots`: Robot registry for all LLM agents using HTM
1315
- - `nodes`: Main memory storage with vector embeddings (pgvector), full-text search (tsvector), metadata
1380
+ - `nodes`: Main memory storage with vector embeddings (pgvector), full-text search (tsvector), JSONB metadata
1316
1381
  - `tags`: Hierarchical tag ontology (format: `root:level1:level2:level3`)
1317
1382
  - `node_tags`: Join table implementing many-to-many relationship between nodes and tags
1318
1383
 
1384
+ **Nodes Table Key Columns:**
1385
+ - `content`: The memory content
1386
+ - `embedding`: Vector embedding for semantic search (up to 2000 dimensions)
1387
+ - `metadata`: JSONB column for arbitrary key-value data (filterable via `@>` containment operator)
1388
+ - `content_hash`: SHA-256 hash for deduplication
1389
+
1319
1390
  ### Service Architecture
1320
1391
 
1321
1392
  HTM uses a layered architecture for LLM integration:
@@ -1329,13 +1400,13 @@ This separation allows you to provide any LLM implementation while HTM handles r
1329
1400
  ## Roadmap
1330
1401
 
1331
1402
  - [x] Phase 1: Foundation (basic two-tier memory)
1332
- - [ ] Phase 2: RAG retrieval (semantic search)
1333
- - [ ] Phase 3: Relationships & tags
1334
- - [ ] Phase 4: Working memory management
1335
- - [ ] Phase 5: Hive mind features
1336
- - [ ] Phase 6: Operations & observability
1337
- - [ ] Phase 7: Advanced features
1338
- - [ ] Phase 8: Production-ready gem
1403
+ - [x] Phase 2: RAG retrieval (semantic search)
1404
+ - [x] Phase 3: Relationships & tags
1405
+ - [x] Phase 4: Working memory management
1406
+ - [x] Phase 5: Hive mind features
1407
+ - [x] Phase 6: Operations & observability
1408
+ - [x] Phase 7: Advanced features
1409
+ - [x] Phase 8: Production-ready gem
1339
1410
 
1340
1411
 
1341
1412
  ## Contributing
data/Rakefile CHANGED
@@ -28,6 +28,11 @@ task :example do
28
28
  ruby "examples/basic_usage.rb"
29
29
  end
30
30
 
31
+ desc "Run timeframe demo"
32
+ task :timeframe_demo do
33
+ ruby "examples/timeframe_demo.rb"
34
+ end
35
+
31
36
  desc "Show gem stats"
32
37
  task :stats do
33
38
  puts "\nHTM Gem Statistics:"
data/bin/htm_mcp.rb ADDED
@@ -0,0 +1,527 @@
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: Get memory statistics
399
+ class StatsTool < FastMcp::Tool
400
+ description "Get statistics about HTM memory usage"
401
+
402
+ arguments do
403
+ end
404
+
405
+ def call
406
+ htm = MCPSession.htm_instance
407
+ robot = HTM::Models::Robot.find(htm.robot_id)
408
+ MCP_STDERR_LOG.info "StatsTool called for robot=#{htm.robot_name}"
409
+
410
+ # Note: Node uses default_scope to exclude deleted, so .count returns active nodes
411
+ total_nodes = HTM::Models::Node.count
412
+ deleted_nodes = HTM::Models::Node.deleted.count
413
+ nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
414
+ nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
415
+ total_tags = HTM::Models::Tag.count
416
+ total_robots = HTM::Models::Robot.count
417
+
418
+ MCP_STDERR_LOG.info "StatsTool complete: #{total_nodes} active nodes, #{total_tags} tags"
419
+
420
+ {
421
+ success: true,
422
+ current_robot: {
423
+ name: htm.robot_name,
424
+ id: htm.robot_id,
425
+ memory_summary: robot.memory_summary
426
+ },
427
+ statistics: {
428
+ nodes: {
429
+ active: total_nodes,
430
+ deleted: deleted_nodes,
431
+ with_embeddings: nodes_with_embeddings,
432
+ with_tags: nodes_with_tags
433
+ },
434
+ tags: {
435
+ total: total_tags
436
+ },
437
+ robots: {
438
+ total: total_robots
439
+ }
440
+ }
441
+ }.to_json
442
+ rescue StandardError => e
443
+ MCP_STDERR_LOG.error "StatsTool error: #{e.message}"
444
+ { success: false, error: e.message }.to_json
445
+ end
446
+ end
447
+
448
+ # Resource: Memory Statistics
449
+ class MemoryStatsResource < FastMcp::Resource
450
+ uri "htm://statistics"
451
+ resource_name "HTM Memory Statistics"
452
+ mime_type "application/json"
453
+
454
+ def content
455
+ htm = MCPSession.htm_instance
456
+ {
457
+ total_nodes: HTM::Models::Node.active.count,
458
+ total_tags: HTM::Models::Tag.count,
459
+ total_robots: HTM::Models::Robot.count,
460
+ current_robot: htm.robot_name,
461
+ robot_id: htm.robot_id,
462
+ robot_initialized: MCPSession.robot_initialized?,
463
+ embedding_provider: HTM.configuration.embedding_provider,
464
+ embedding_model: HTM.configuration.embedding_model
465
+ }.to_json
466
+ end
467
+ end
468
+
469
+ # Resource: Tag Hierarchy
470
+ class TagHierarchyResource < FastMcp::Resource
471
+ uri "htm://tags/hierarchy"
472
+ resource_name "HTM Tag Hierarchy"
473
+ mime_type "text/plain"
474
+
475
+ def content
476
+ HTM::Models::Tag.all.tree_string
477
+ end
478
+ end
479
+
480
+ # Resource: Recent Memories
481
+ class RecentMemoriesResource < FastMcp::Resource
482
+ uri "htm://memories/recent"
483
+ resource_name "Recent HTM Memories"
484
+ mime_type "application/json"
485
+
486
+ def content
487
+ recent = HTM::Models::Node.active
488
+ .includes(:tags)
489
+ .order(created_at: :desc)
490
+ .limit(20)
491
+ .map do |node|
492
+ {
493
+ id: node.id,
494
+ content: node.content[0..200],
495
+ tags: node.tags.map(&:name),
496
+ created_at: node.created_at.iso8601
497
+ }
498
+ end
499
+
500
+ { recent_memories: recent }.to_json
501
+ end
502
+ end
503
+
504
+ # Create and configure the MCP server
505
+ server = FastMcp::Server.new(
506
+ name: 'htm-memory-server',
507
+ version: HTM::VERSION
508
+ )
509
+
510
+ # Register tools
511
+ server.register_tool(SetRobotTool) # Call first to set robot identity
512
+ server.register_tool(GetRobotTool) # Get current robot info
513
+ server.register_tool(GetWorkingMemoryTool) # Get working memory for session restore
514
+ server.register_tool(RememberTool)
515
+ server.register_tool(RecallTool)
516
+ server.register_tool(ForgetTool)
517
+ server.register_tool(RestoreTool)
518
+ server.register_tool(ListTagsTool)
519
+ server.register_tool(StatsTool)
520
+
521
+ # Register resources
522
+ server.register_resource(MemoryStatsResource)
523
+ server.register_resource(TagHierarchyResource)
524
+ server.register_resource(RecentMemoriesResource)
525
+
526
+ # Start the server (STDIO transport by default)
527
+ server.start