htm 0.0.18 → 0.0.30
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +119 -1
- data/README.md +12 -0
- data/Rakefile +104 -18
- data/db/migrate/00001_enable_extensions.rb +9 -5
- data/db/migrate/00002_create_robots.rb +18 -6
- data/db/migrate/00003_create_file_sources.rb +30 -17
- data/db/migrate/00004_create_nodes.rb +60 -48
- data/db/migrate/00005_create_tags.rb +24 -12
- data/db/migrate/00006_create_node_tags.rb +28 -13
- data/db/migrate/00007_create_robot_nodes.rb +40 -26
- data/db/schema.sql +17 -1
- data/db/seeds.rb +34 -34
- data/docs/api/embedding-service.md +140 -110
- data/docs/api/yard/HTM/ActiveRecordConfig.md +6 -0
- data/docs/api/yard/HTM/Config.md +173 -0
- data/docs/api/yard/HTM/ConfigSection.md +28 -0
- data/docs/api/yard/HTM/Database.md +1 -1
- data/docs/api/yard/HTM/Railtie.md +2 -2
- data/docs/api/yard/HTM.md +0 -57
- data/docs/api/yard/index.csv +76 -61
- data/docs/api/yard-reference.md +2 -1
- data/docs/architecture/adrs/003-ollama-embeddings.md +45 -36
- data/docs/architecture/adrs/004-hive-mind.md +1 -1
- data/docs/architecture/adrs/008-robot-identification.md +1 -1
- data/docs/architecture/index.md +11 -9
- data/docs/architecture/overview.md +11 -7
- data/docs/assets/images/balanced-strategy-decay.svg +41 -0
- data/docs/assets/images/class-hierarchy.svg +1 -1
- data/docs/assets/images/eviction-priority.svg +43 -0
- data/docs/assets/images/exception-hierarchy.svg +2 -2
- data/docs/assets/images/hive-mind-shared-memory.svg +52 -0
- data/docs/assets/images/htm-architecture-overview.svg +3 -3
- data/docs/assets/images/htm-core-components.svg +4 -4
- data/docs/assets/images/htm-layered-architecture.svg +1 -1
- data/docs/assets/images/htm-memory-addition-flow.svg +2 -2
- data/docs/assets/images/htm-memory-recall-flow.svg +2 -2
- data/docs/assets/images/memory-topology.svg +53 -0
- data/docs/assets/images/two-tier-memory-architecture.svg +55 -0
- data/docs/database/naming-convention.md +244 -0
- data/docs/database_rake_tasks.md +31 -0
- data/docs/development/rake-tasks.md +80 -35
- data/docs/development/setup.md +76 -44
- data/docs/examples/basic-usage.md +133 -0
- data/docs/examples/config-files.md +170 -0
- data/docs/examples/file-loading.md +208 -0
- data/docs/examples/index.md +116 -0
- data/docs/examples/llm-configuration.md +168 -0
- data/docs/examples/mcp-client.md +172 -0
- data/docs/examples/rails-integration.md +173 -0
- data/docs/examples/robot-groups.md +210 -0
- data/docs/examples/sinatra-integration.md +218 -0
- data/docs/examples/standalone-app.md +216 -0
- data/docs/examples/telemetry.md +224 -0
- data/docs/examples/timeframes.md +143 -0
- data/docs/getting-started/installation.md +97 -40
- data/docs/getting-started/quick-start.md +28 -11
- data/docs/guides/configuration.md +515 -0
- data/docs/guides/file-loading.md +322 -0
- data/docs/guides/getting-started.md +40 -9
- data/docs/guides/index.md +3 -3
- data/docs/guides/mcp-server.md +100 -13
- data/docs/guides/propositions.md +264 -0
- data/docs/guides/recalling-memories.md +4 -4
- data/docs/guides/search-strategies.md +3 -3
- data/docs/guides/tags.md +318 -0
- data/docs/guides/telemetry.md +229 -0
- data/docs/index.md +8 -16
- data/docs/{architecture → robots}/hive-mind.md +8 -111
- data/docs/robots/index.md +73 -0
- data/docs/{guides → robots}/multi-robot.md +3 -3
- data/docs/{guides → robots}/robot-groups.md +8 -7
- data/docs/{architecture → robots}/two-tier-memory.md +13 -149
- data/docs/robots/why-robots.md +85 -0
- data/examples/.envrc +6 -0
- data/examples/.gitignore +2 -0
- data/examples/00_create_examples_db.rb +94 -0
- data/examples/{basic_usage.rb → 01_basic_usage.rb} +12 -16
- data/examples/{custom_llm_configuration.rb → 03_custom_llm_configuration.rb} +13 -3
- data/examples/{file_loader_usage.rb → 04_file_loader_usage.rb} +11 -14
- data/examples/{timeframe_demo.rb → 05_timeframe_demo.rb} +10 -3
- data/examples/{example_app → 06_example_app}/app.rb +15 -15
- data/examples/{cli_app → 07_cli_app}/htm_cli.rb +15 -22
- data/examples/08_sinatra_app/Gemfile.lock +241 -0
- data/examples/{sinatra_app → 08_sinatra_app}/app.rb +19 -18
- data/examples/{mcp_client.rb → 09_mcp_client.rb} +5 -8
- data/examples/{telemetry → 10_telemetry}/SETUP_README.md +1 -1
- data/examples/{telemetry → 10_telemetry}/demo.rb +14 -10
- data/examples/11_robot_groups/README.md +335 -0
- data/examples/{robot_groups → 11_robot_groups/lib}/robot_worker.rb +17 -3
- data/examples/{robot_groups → 11_robot_groups}/multi_process.rb +9 -9
- data/examples/{robot_groups → 11_robot_groups}/same_process.rb +9 -12
- data/examples/{rails_app → 12_rails_app}/Gemfile +3 -0
- data/examples/{rails_app → 12_rails_app}/Gemfile.lock +87 -58
- data/examples/{rails_app → 12_rails_app}/app/controllers/dashboard_controller.rb +10 -6
- data/examples/{rails_app → 12_rails_app}/app/controllers/files_controller.rb +5 -5
- data/examples/{rails_app → 12_rails_app}/app/controllers/memories_controller.rb +11 -7
- data/examples/{rails_app → 12_rails_app}/app/controllers/robots_controller.rb +8 -8
- data/examples/12_rails_app/app/controllers/tags_controller.rb +36 -0
- data/examples/{rails_app → 12_rails_app}/app/views/dashboard/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/files/new.html.erb +5 -2
- data/examples/{rails_app → 12_rails_app}/app/views/memories/_memory_card.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/deleted.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/edit.html.erb +3 -3
- data/examples/{rails_app → 12_rails_app}/app/views/memories/show.html.erb +4 -4
- data/examples/{rails_app → 12_rails_app}/app/views/robots/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/robots/show.html.erb +4 -4
- data/examples/{rails_app → 12_rails_app}/app/views/search/index.html.erb +1 -1
- data/examples/{rails_app → 12_rails_app}/app/views/tags/index.html.erb +2 -2
- data/examples/{rails_app → 12_rails_app}/app/views/tags/show.html.erb +1 -1
- data/examples/12_rails_app/config/initializers/htm.rb +7 -0
- data/examples/12_rails_app/config/initializers/rack.rb +5 -0
- data/examples/README.md +230 -211
- data/examples/examples_helper.rb +138 -0
- data/lib/htm/config/builder.rb +167 -0
- data/lib/htm/config/database.rb +317 -0
- data/lib/htm/config/defaults.yml +41 -13
- data/lib/htm/config/section.rb +74 -0
- data/lib/htm/config/validator.rb +83 -0
- data/lib/htm/config.rb +65 -361
- data/lib/htm/database.rb +85 -127
- data/lib/htm/errors.rb +14 -0
- data/lib/htm/integrations/sinatra.rb +13 -44
- data/lib/htm/job_adapter.rb +75 -1
- data/lib/htm/jobs/generate_embedding_job.rb +3 -4
- data/lib/htm/jobs/generate_propositions_job.rb +4 -5
- data/lib/htm/jobs/generate_tags_job.rb +16 -15
- data/lib/htm/loaders/defaults_loader.rb +23 -0
- data/lib/htm/loaders/markdown_loader.rb +17 -15
- data/lib/htm/loaders/xdg_config_loader.rb +9 -9
- data/lib/htm/long_term_memory/fulltext_search.rb +14 -14
- data/lib/htm/long_term_memory/hybrid_search.rb +396 -229
- data/lib/htm/long_term_memory/node_operations.rb +24 -23
- data/lib/htm/long_term_memory/relevance_scorer.rb +23 -20
- data/lib/htm/long_term_memory/robot_operations.rb +4 -4
- data/lib/htm/long_term_memory/tag_operations.rb +91 -77
- data/lib/htm/long_term_memory/vector_search.rb +4 -5
- data/lib/htm/long_term_memory.rb +13 -13
- data/lib/htm/mcp/cli.rb +115 -8
- data/lib/htm/mcp/resources.rb +4 -3
- data/lib/htm/mcp/server.rb +5 -4
- data/lib/htm/mcp/tools.rb +37 -28
- data/lib/htm/migration.rb +72 -0
- data/lib/htm/models/file_source.rb +52 -31
- data/lib/htm/models/node.rb +224 -108
- data/lib/htm/models/node_tag.rb +49 -28
- data/lib/htm/models/robot.rb +38 -27
- data/lib/htm/models/robot_node.rb +63 -35
- data/lib/htm/models/tag.rb +126 -123
- data/lib/htm/observability.rb +45 -41
- data/lib/htm/proposition_service.rb +76 -7
- data/lib/htm/railtie.rb +2 -2
- data/lib/htm/robot_group.rb +30 -18
- data/lib/htm/sequel_config.rb +215 -0
- data/lib/htm/sql_builder.rb +14 -16
- data/lib/htm/tag_service.rb +78 -0
- data/lib/htm/tasks.rb +3 -0
- data/lib/htm/version.rb +1 -1
- data/lib/htm/workflows/remember_workflow.rb +213 -0
- data/lib/htm.rb +27 -22
- data/lib/tasks/db.rake +0 -2
- data/lib/tasks/doc.rake +2 -2
- data/lib/tasks/files.rake +11 -18
- data/lib/tasks/htm.rake +190 -62
- data/lib/tasks/jobs.rake +179 -54
- data/lib/tasks/tags.rake +8 -13
- data/mkdocs.yml +33 -8
- data/scripts/backfill_parent_tags.rb +376 -0
- data/scripts/normalize_plural_tags.rb +335 -0
- metadata +168 -86
- data/docs/api/yard/HTM/Configuration.md +0 -240
- data/docs/telemetry.md +0 -391
- data/examples/rails_app/app/controllers/tags_controller.rb +0 -30
- data/examples/sinatra_app/Gemfile.lock +0 -166
- data/lib/htm/active_record_config.rb +0 -104
- /data/examples/{config_file_example → 02_config_file_example}/README.md +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/config/htm.local.yml +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/custom_config.yml +0 -0
- /data/examples/{config_file_example → 02_config_file_example}/show_config.rb +0 -0
- /data/examples/{example_app → 06_example_app}/Rakefile +0 -0
- /data/examples/{cli_app → 07_cli_app}/README.md +0 -0
- /data/examples/{sinatra_app → 08_sinatra_app}/Gemfile +0 -0
- /data/examples/{telemetry → 10_telemetry}/README.md +0 -0
- /data/examples/{telemetry → 10_telemetry}/grafana/dashboards/htm-metrics.json +0 -0
- /data/examples/{rails_app → 12_rails_app}/.gitignore +0 -0
- /data/examples/{rails_app → 12_rails_app}/Procfile.dev +0 -0
- /data/examples/{rails_app → 12_rails_app}/README.md +0 -0
- /data/examples/{rails_app → 12_rails_app}/Rakefile +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/application.css +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/assets/stylesheets/inter-font.css +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/controllers/application_controller.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/controllers/search_controller.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/application.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/application.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/javascript/controllers/index.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/files/index.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/files/show.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/layouts/application.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/memories/index.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/memories/new.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/robots/new.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/shared/_navbar.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/app/views/shared/_stat_card.html.erb +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/dev +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/rails +0 -0
- /data/examples/{rails_app → 12_rails_app}/bin/rake +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/application.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/boot.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/database.yml +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/environment.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/importmap.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/routes.rb +0 -0
- /data/examples/{rails_app → 12_rails_app}/config/tailwind.config.js +0 -0
- /data/examples/{rails_app → 12_rails_app}/config.ru +0 -0
- /data/examples/{rails_app → 12_rails_app}/log/.keep +0 -0
- /data/examples/{rails_app → 12_rails_app}/tmp/local_secret.txt +0 -0
data/lib/htm/mcp/cli.rb
CHANGED
|
@@ -21,6 +21,7 @@ class HTM
|
|
|
21
21
|
verify Verify database connection and extensions
|
|
22
22
|
stats Show memory statistics
|
|
23
23
|
config Output default configuration to STDOUT
|
|
24
|
+
rake Run HTM rake tasks (use -T [pattern] to list)
|
|
24
25
|
version Show HTM version
|
|
25
26
|
help Show this help message
|
|
26
27
|
|
|
@@ -107,6 +108,19 @@ class HTM
|
|
|
107
108
|
# Start MCP server (for Claude Desktop)
|
|
108
109
|
htm_mcp
|
|
109
110
|
|
|
111
|
+
# List available rake tasks
|
|
112
|
+
htm_mcp rake -T
|
|
113
|
+
htm_mcp rake --tasks
|
|
114
|
+
|
|
115
|
+
# List tasks matching a pattern
|
|
116
|
+
htm_mcp rake -T htm:jobs
|
|
117
|
+
htm_mcp rake -T db
|
|
118
|
+
|
|
119
|
+
# Run rake tasks
|
|
120
|
+
htm_mcp rake htm:db:stats
|
|
121
|
+
htm_mcp rake htm:tags:tree
|
|
122
|
+
htm_mcp rake 'htm:tags:tree[database]'
|
|
123
|
+
|
|
110
124
|
CLAUDE DESKTOP CONFIGURATION:
|
|
111
125
|
Add to ~/.config/claude/claude_desktop_config.json:
|
|
112
126
|
|
|
@@ -243,13 +257,13 @@ class HTM
|
|
|
243
257
|
}
|
|
244
258
|
end.sort_by { |m| m[:version] }
|
|
245
259
|
|
|
246
|
-
# Ensure
|
|
247
|
-
HTM::
|
|
260
|
+
# Ensure Sequel connection for migration check
|
|
261
|
+
HTM::SequelConfig.establish_connection!
|
|
248
262
|
|
|
249
263
|
# Get applied migrations from database
|
|
250
264
|
applied_versions = begin
|
|
251
|
-
|
|
252
|
-
rescue
|
|
265
|
+
HTM.db[:schema_migrations].select_order_map(:version)
|
|
266
|
+
rescue Sequel::DatabaseError
|
|
253
267
|
[]
|
|
254
268
|
end
|
|
255
269
|
|
|
@@ -379,7 +393,7 @@ class HTM
|
|
|
379
393
|
check_database_config!
|
|
380
394
|
|
|
381
395
|
begin
|
|
382
|
-
HTM::
|
|
396
|
+
HTM::SequelConfig.establish_connection!
|
|
383
397
|
|
|
384
398
|
total_nodes = HTM::Models::Node.count
|
|
385
399
|
deleted_nodes = HTM::Models::Node.deleted.count
|
|
@@ -389,9 +403,9 @@ class HTM
|
|
|
389
403
|
total_files = HTM::Models::FileSource.count
|
|
390
404
|
|
|
391
405
|
# Get database size
|
|
392
|
-
db_size =
|
|
406
|
+
db_size = HTM.db.fetch(
|
|
393
407
|
"SELECT pg_size_pretty(pg_database_size(current_database())) AS size"
|
|
394
|
-
).first[
|
|
408
|
+
).first[:size]
|
|
395
409
|
|
|
396
410
|
puts "Nodes: #{total_nodes} active, #{deleted_nodes} deleted, #{with_embeddings} with embeddings"
|
|
397
411
|
puts "Tags: #{total_tags}"
|
|
@@ -427,6 +441,8 @@ class HTM
|
|
|
427
441
|
run_stats
|
|
428
442
|
when 'config'
|
|
429
443
|
output_default_config
|
|
444
|
+
when 'rake'
|
|
445
|
+
run_rake(args[1..] || [])
|
|
430
446
|
when 'server', 'stdio', nil
|
|
431
447
|
# Return false to indicate server should start
|
|
432
448
|
# 'stdio' is accepted for compatibility with MCP clients that pass it as an argument
|
|
@@ -468,7 +484,98 @@ class HTM
|
|
|
468
484
|
end
|
|
469
485
|
|
|
470
486
|
def command?(arg)
|
|
471
|
-
%w[help version setup init verify stats config server stdio].include?(arg.downcase)
|
|
487
|
+
%w[help version setup init verify stats config server stdio rake].include?(arg.downcase)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def run_rake(args)
|
|
491
|
+
require 'rake'
|
|
492
|
+
|
|
493
|
+
# Handle --tasks / -T to list available tasks (with optional pattern)
|
|
494
|
+
if args.empty? || args.first == '--tasks' || args.first == '-T'
|
|
495
|
+
# Check for optional pattern after -T/--tasks
|
|
496
|
+
pattern = nil
|
|
497
|
+
if args.first == '--tasks' || args.first == '-T'
|
|
498
|
+
pattern = args[1] # May be nil if no pattern provided
|
|
499
|
+
end
|
|
500
|
+
list_rake_tasks(pattern: pattern)
|
|
501
|
+
return
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
task_name = args.shift
|
|
505
|
+
|
|
506
|
+
# Load HTM rake tasks
|
|
507
|
+
load_htm_rake_tasks
|
|
508
|
+
|
|
509
|
+
# Check if task exists
|
|
510
|
+
unless Rake::Task.task_defined?(task_name)
|
|
511
|
+
warn "Unknown rake task: #{task_name}"
|
|
512
|
+
warn "Run 'htm_mcp rake --tasks' to see available tasks."
|
|
513
|
+
exit 1
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Set remaining args as task arguments if any
|
|
517
|
+
# Rake tasks use ARGV for arguments in brackets like task[arg1,arg2]
|
|
518
|
+
begin
|
|
519
|
+
Rake::Task[task_name].invoke
|
|
520
|
+
rescue => e
|
|
521
|
+
warn "Rake task failed: #{e.message}"
|
|
522
|
+
warn e.backtrace.first(5).join("\n") if ENV['DEBUG']
|
|
523
|
+
exit 1
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def load_htm_rake_tasks
|
|
528
|
+
# Clear any existing tasks to avoid conflicts
|
|
529
|
+
Rake::TaskManager.record_task_metadata = true
|
|
530
|
+
Rake.application = Rake::Application.new
|
|
531
|
+
Rake.application.init('htm_mcp')
|
|
532
|
+
|
|
533
|
+
# Load all HTM task files
|
|
534
|
+
tasks_dir = File.expand_path('../../tasks', __dir__)
|
|
535
|
+
Dir.glob(File.join(tasks_dir, '*.rake')).sort.each do |rake_file|
|
|
536
|
+
load rake_file
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def list_rake_tasks(pattern: nil)
|
|
541
|
+
load_htm_rake_tasks
|
|
542
|
+
|
|
543
|
+
# Collect tasks with descriptions, sorted by name
|
|
544
|
+
tasks = Rake.application.tasks
|
|
545
|
+
.select { |t| t.comment && t.name.start_with?('htm:') }
|
|
546
|
+
.sort_by(&:name)
|
|
547
|
+
|
|
548
|
+
# Filter by pattern if provided (matches task name)
|
|
549
|
+
if pattern
|
|
550
|
+
tasks = tasks.select { |t| t.name.include?(pattern) }
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
if tasks.empty?
|
|
554
|
+
if pattern
|
|
555
|
+
puts "No HTM rake tasks matching '#{pattern}'"
|
|
556
|
+
else
|
|
557
|
+
puts "No HTM rake tasks found"
|
|
558
|
+
end
|
|
559
|
+
return
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
if pattern
|
|
563
|
+
puts "HTM rake tasks matching '#{pattern}':"
|
|
564
|
+
else
|
|
565
|
+
puts "Available HTM rake tasks:"
|
|
566
|
+
end
|
|
567
|
+
puts
|
|
568
|
+
|
|
569
|
+
# Find max task name length for alignment
|
|
570
|
+
max_len = tasks.map { |t| t.name.length }.max || 0
|
|
571
|
+
|
|
572
|
+
tasks.each do |task|
|
|
573
|
+
printf " %-#{max_len}s # %s\n", task.name, task.comment
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
puts
|
|
577
|
+
puts "Run with: htm_mcp rake <task_name>"
|
|
578
|
+
puts "Example: htm_mcp rake htm:db:stats"
|
|
472
579
|
end
|
|
473
580
|
end
|
|
474
581
|
end
|
data/lib/htm/mcp/resources.rb
CHANGED
|
@@ -51,7 +51,7 @@ class HTM
|
|
|
51
51
|
mime_type "text/plain"
|
|
52
52
|
|
|
53
53
|
def content
|
|
54
|
-
HTM::Models::Tag.
|
|
54
|
+
HTM::Models::Tag.tree_string
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -62,9 +62,10 @@ class HTM
|
|
|
62
62
|
mime_type "application/json"
|
|
63
63
|
|
|
64
64
|
def content
|
|
65
|
-
recent = HTM::Models::Node.
|
|
66
|
-
.order(created_at
|
|
65
|
+
recent = HTM::Models::Node.eager(:tags)
|
|
66
|
+
.order(Sequel.desc(:created_at))
|
|
67
67
|
.limit(20)
|
|
68
|
+
.all
|
|
68
69
|
.map do |node|
|
|
69
70
|
{
|
|
70
71
|
id: node.id,
|
data/lib/htm/mcp/server.rb
CHANGED
|
@@ -37,9 +37,9 @@ class HTM
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def verify_database_connection!
|
|
40
|
-
HTM::
|
|
40
|
+
HTM::SequelConfig.establish_connection!
|
|
41
41
|
# Quick connectivity test
|
|
42
|
-
|
|
42
|
+
HTM.db.execute("SELECT 1")
|
|
43
43
|
rescue => e
|
|
44
44
|
warn "Error: Cannot connect to database."
|
|
45
45
|
warn e.message
|
|
@@ -70,8 +70,9 @@ class HTM
|
|
|
70
70
|
|
|
71
71
|
def configure_htm!
|
|
72
72
|
HTM.configure do |config|
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
# Job backend now comes from config (defaults to :fiber)
|
|
74
|
+
# Use HTM_JOB__BACKEND=inline or config file to override
|
|
75
|
+
config.logger = @silent_logger # Silent logging for MCP
|
|
75
76
|
end
|
|
76
77
|
end
|
|
77
78
|
|
data/lib/htm/mcp/tools.rb
CHANGED
|
@@ -45,7 +45,7 @@ class HTM
|
|
|
45
45
|
Session.logger&.info "SetRobotTool called: name=#{name.inspect}"
|
|
46
46
|
|
|
47
47
|
htm = Session.set_robot(name)
|
|
48
|
-
robot = HTM::Models::Robot
|
|
48
|
+
robot = HTM::Models::Robot[htm.robot_id]
|
|
49
49
|
|
|
50
50
|
{
|
|
51
51
|
success: true,
|
|
@@ -68,7 +68,7 @@ class HTM
|
|
|
68
68
|
Session.logger&.info "GetRobotTool called"
|
|
69
69
|
|
|
70
70
|
htm = Session.htm_instance
|
|
71
|
-
robot = HTM::Models::Robot
|
|
71
|
+
robot = HTM::Models::Robot[htm.robot_id]
|
|
72
72
|
|
|
73
73
|
{
|
|
74
74
|
success: true,
|
|
@@ -89,26 +89,27 @@ class HTM
|
|
|
89
89
|
|
|
90
90
|
def call
|
|
91
91
|
htm = Session.htm_instance
|
|
92
|
-
robot = HTM::Models::Robot
|
|
92
|
+
robot = HTM::Models::Robot[htm.robot_id]
|
|
93
93
|
Session.logger&.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
|
|
94
94
|
|
|
95
95
|
# Get all nodes in working memory with their metadata
|
|
96
|
-
# Filter out any robot_nodes where the node has been deleted
|
|
97
|
-
working_memory_nodes = robot.
|
|
96
|
+
# Filter out any robot_nodes where the node has been deleted
|
|
97
|
+
working_memory_nodes = robot.robot_nodes_dataset
|
|
98
98
|
.in_working_memory
|
|
99
|
-
.
|
|
100
|
-
.
|
|
101
|
-
.
|
|
99
|
+
.eager(node: :tags)
|
|
100
|
+
.order(Sequel.desc(:last_remembered_at))
|
|
101
|
+
.all
|
|
102
102
|
.filter_map do |rn|
|
|
103
|
-
|
|
103
|
+
node = rn.node
|
|
104
|
+
next unless node # Exclude if node is nil (was deleted)
|
|
104
105
|
|
|
105
106
|
{
|
|
106
|
-
id:
|
|
107
|
-
content:
|
|
108
|
-
tags:
|
|
107
|
+
id: node.id,
|
|
108
|
+
content: node.content,
|
|
109
|
+
tags: node.tags.map(&:name),
|
|
109
110
|
remember_count: rn.remember_count,
|
|
110
111
|
last_remembered_at: rn.last_remembered_at&.iso8601,
|
|
111
|
-
created_at:
|
|
112
|
+
created_at: node.created_at.iso8601
|
|
112
113
|
}
|
|
113
114
|
end
|
|
114
115
|
|
|
@@ -142,7 +143,7 @@ class HTM
|
|
|
142
143
|
|
|
143
144
|
htm = Session.htm_instance
|
|
144
145
|
node_id = htm.remember(content, tags: tags, metadata: metadata)
|
|
145
|
-
node = HTM::Models::Node.
|
|
146
|
+
node = HTM::Models::Node.eager(:tags).first!(id: node_id)
|
|
146
147
|
|
|
147
148
|
Session.logger&.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
|
|
148
149
|
|
|
@@ -187,7 +188,8 @@ class HTM
|
|
|
187
188
|
memories = htm.recall(query, **recall_opts)
|
|
188
189
|
|
|
189
190
|
results = memories.map do |memory|
|
|
190
|
-
node = HTM::Models::Node.
|
|
191
|
+
node = HTM::Models::Node.eager(:tags).first(id: memory['id'])
|
|
192
|
+
next unless node
|
|
191
193
|
{
|
|
192
194
|
id: memory['id'],
|
|
193
195
|
content: memory['content'],
|
|
@@ -195,7 +197,7 @@ class HTM
|
|
|
195
197
|
created_at: memory['created_at'],
|
|
196
198
|
score: memory['combined_score'] || memory['similarity']
|
|
197
199
|
}
|
|
198
|
-
end
|
|
200
|
+
end.compact
|
|
199
201
|
|
|
200
202
|
Session.logger&.info "Recall complete: found #{results.length} memories"
|
|
201
203
|
|
|
@@ -212,13 +214,17 @@ class HTM
|
|
|
212
214
|
private
|
|
213
215
|
|
|
214
216
|
def parse_timeframe(timeframe)
|
|
217
|
+
now = Time.now
|
|
215
218
|
case timeframe.downcase
|
|
216
219
|
when 'today'
|
|
217
|
-
|
|
220
|
+
# Beginning of today
|
|
221
|
+
Time.new(now.year, now.month, now.day)..now
|
|
218
222
|
when 'this week'
|
|
219
|
-
|
|
223
|
+
# 7 days ago
|
|
224
|
+
(now - 7 * 24 * 60 * 60)..now
|
|
220
225
|
when 'this month'
|
|
221
|
-
|
|
226
|
+
# 30 days ago
|
|
227
|
+
(now - 30 * 24 * 60 * 60)..now
|
|
222
228
|
else
|
|
223
229
|
# Try to parse as ISO8601 range (start..end)
|
|
224
230
|
if timeframe.include?('..')
|
|
@@ -226,7 +232,7 @@ class HTM
|
|
|
226
232
|
Time.parse(parts[0])..Time.parse(parts[1])
|
|
227
233
|
else
|
|
228
234
|
# Single date - from that date to now
|
|
229
|
-
Time.parse(timeframe)..
|
|
235
|
+
Time.parse(timeframe)..now
|
|
230
236
|
end
|
|
231
237
|
end
|
|
232
238
|
rescue ArgumentError
|
|
@@ -256,7 +262,7 @@ class HTM
|
|
|
256
262
|
robot_name: htm.robot_name,
|
|
257
263
|
message: "Memory soft-deleted. Use restore to recover."
|
|
258
264
|
}.to_json
|
|
259
|
-
rescue HTM::NotFoundError,
|
|
265
|
+
rescue HTM::NotFoundError, Sequel::NoMatchingRow
|
|
260
266
|
Session.logger&.warn "ForgetTool failed: node #{node_id} not found"
|
|
261
267
|
{
|
|
262
268
|
success: false,
|
|
@@ -287,7 +293,7 @@ class HTM
|
|
|
287
293
|
robot_name: htm.robot_name,
|
|
288
294
|
message: "Memory restored successfully"
|
|
289
295
|
}.to_json
|
|
290
|
-
rescue HTM::NotFoundError,
|
|
296
|
+
rescue HTM::NotFoundError, Sequel::NoMatchingRow
|
|
291
297
|
Session.logger&.warn "RestoreTool failed: node #{node_id} not found"
|
|
292
298
|
{
|
|
293
299
|
success: false,
|
|
@@ -308,7 +314,7 @@ class HTM
|
|
|
308
314
|
Session.logger&.info "ListTagsTool called: prefix=#{prefix.inspect}"
|
|
309
315
|
|
|
310
316
|
tags_query = HTM::Models::Tag.order(:name)
|
|
311
|
-
tags_query = tags_query.where(
|
|
317
|
+
tags_query = tags_query.where(Sequel.like(:name, "#{prefix}%")) if prefix
|
|
312
318
|
|
|
313
319
|
tags = tags_query.map do |tag|
|
|
314
320
|
{
|
|
@@ -348,7 +354,7 @@ class HTM
|
|
|
348
354
|
|
|
349
355
|
# Enrich with node counts
|
|
350
356
|
tags = results.map do |result|
|
|
351
|
-
tag = HTM::Models::Tag.
|
|
357
|
+
tag = HTM::Models::Tag.first(name: result[:name])
|
|
352
358
|
{
|
|
353
359
|
name: result[:name],
|
|
354
360
|
similarity: result[:similarity].round(3),
|
|
@@ -396,7 +402,7 @@ class HTM
|
|
|
396
402
|
|
|
397
403
|
# Enrich with tags
|
|
398
404
|
results = nodes.map do |node_attrs|
|
|
399
|
-
node = HTM::Models::Node.
|
|
405
|
+
node = HTM::Models::Node.eager(:tags).first(id: node_attrs['id'])
|
|
400
406
|
next unless node
|
|
401
407
|
|
|
402
408
|
{
|
|
@@ -429,14 +435,17 @@ class HTM
|
|
|
429
435
|
|
|
430
436
|
def call
|
|
431
437
|
htm = Session.htm_instance
|
|
432
|
-
robot = HTM::Models::Robot
|
|
438
|
+
robot = HTM::Models::Robot[htm.robot_id]
|
|
433
439
|
Session.logger&.info "StatsTool called for robot=#{htm.robot_name}"
|
|
434
440
|
|
|
435
|
-
# Note: Node uses
|
|
441
|
+
# Note: Node uses set_dataset to exclude deleted, so .count returns active nodes
|
|
436
442
|
total_nodes = HTM::Models::Node.count
|
|
437
443
|
deleted_nodes = HTM::Models::Node.deleted.count
|
|
438
444
|
nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
|
|
439
|
-
nodes_with_tags = HTM::Models::Node
|
|
445
|
+
nodes_with_tags = HTM::Models::Node
|
|
446
|
+
.join(:node_tags, node_id: :id)
|
|
447
|
+
.distinct
|
|
448
|
+
.count
|
|
440
449
|
total_tags = HTM::Models::Tag.count
|
|
441
450
|
total_robots = HTM::Models::Robot.count
|
|
442
451
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class HTM
|
|
4
|
+
# Base class for Sequel migrations
|
|
5
|
+
#
|
|
6
|
+
# Provides a simple interface for writing migrations compatible with
|
|
7
|
+
# HTM's migration runner.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# class CreateUsers < HTM::Migration
|
|
11
|
+
# def up
|
|
12
|
+
# create_table(:users) do
|
|
13
|
+
# primary_key :id
|
|
14
|
+
# String :name, null: false
|
|
15
|
+
# DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def down
|
|
20
|
+
# drop_table(:users)
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class Migration
|
|
25
|
+
attr_reader :db
|
|
26
|
+
|
|
27
|
+
def initialize(db)
|
|
28
|
+
@db = db
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Override in subclass
|
|
32
|
+
def up
|
|
33
|
+
raise NotImplementedError, "#{self.class}#up must be implemented"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Override in subclass (optional for irreversible migrations)
|
|
37
|
+
def down
|
|
38
|
+
raise NotImplementedError, "#{self.class}#down must be implemented"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Delegate common methods to db
|
|
44
|
+
def create_table(name, **options, &block)
|
|
45
|
+
db.create_table(name, **options, &block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def drop_table(name, **options)
|
|
49
|
+
db.drop_table(name, **options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def alter_table(name, &block)
|
|
53
|
+
db.alter_table(name, &block)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_index(table, columns, **options)
|
|
57
|
+
db.add_index(table, columns, **options)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def drop_index(table, columns, **options)
|
|
61
|
+
db.drop_index(table, columns, **options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run(sql)
|
|
65
|
+
db.run(sql)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def execute(sql)
|
|
69
|
+
db.run(sql)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -7,42 +7,44 @@ class HTM
|
|
|
7
7
|
# Represents a file that has been loaded into HTM with its metadata.
|
|
8
8
|
# Each file can have multiple associated nodes (chunks).
|
|
9
9
|
#
|
|
10
|
-
|
|
11
|
-
# source = FileSource.by_path('/path/to/doc.md').first
|
|
12
|
-
# source.chunks # => [Node, Node, ...]
|
|
13
|
-
#
|
|
14
|
-
# @example Check if re-sync needed
|
|
15
|
-
# current_mtime = File.mtime('/path/to/doc.md')
|
|
16
|
-
# source.needs_sync?(current_mtime) # => true/false
|
|
17
|
-
#
|
|
18
|
-
class FileSource < ActiveRecord::Base
|
|
19
|
-
self.table_name = 'file_sources'
|
|
20
|
-
|
|
10
|
+
class FileSource < Sequel::Model(:file_sources)
|
|
21
11
|
# Tolerance for mtime comparison to avoid false positives from
|
|
22
12
|
# precision differences between filesystem and database timestamps
|
|
23
13
|
DELTA_TIME = 5 # seconds
|
|
24
14
|
|
|
25
15
|
# Associations
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
one_to_many :nodes, class: 'HTM::Models::Node', key: :source_id
|
|
17
|
+
|
|
18
|
+
# Plugins
|
|
19
|
+
plugin :validation_helpers
|
|
20
|
+
plugin :timestamps, update_on_create: true
|
|
28
21
|
|
|
29
22
|
# Validations
|
|
30
|
-
|
|
23
|
+
def validate
|
|
24
|
+
super
|
|
25
|
+
validates_presence :file_path
|
|
26
|
+
validates_unique :file_path
|
|
27
|
+
end
|
|
31
28
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
# Dataset methods (scopes)
|
|
30
|
+
dataset_module do
|
|
31
|
+
def by_path(path)
|
|
32
|
+
where(file_path: File.expand_path(path))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stale
|
|
36
|
+
where(Sequel.lit('mtime < last_synced_at'))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def recently_synced
|
|
40
|
+
order(Sequel.desc(:last_synced_at))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
36
43
|
|
|
37
44
|
# Check if file needs re-sync based on mtime
|
|
38
45
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# - Floating-point rounding errors
|
|
42
|
-
# - Minor timestamp discrepancies across systems
|
|
43
|
-
#
|
|
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
|
+
# @param current_mtime [Time, nil] Current file modification time
|
|
47
|
+
# @return [Boolean] true if file needs re-sync
|
|
46
48
|
#
|
|
47
49
|
def needs_sync?(current_mtime = nil)
|
|
48
50
|
return true if mtime.nil?
|
|
@@ -54,10 +56,18 @@ class HTM
|
|
|
54
56
|
|
|
55
57
|
# Get ordered chunks from this file
|
|
56
58
|
#
|
|
57
|
-
# @return [
|
|
59
|
+
# @return [Array<Node>] Nodes ordered by chunk_position
|
|
58
60
|
#
|
|
59
61
|
def chunks
|
|
60
|
-
|
|
62
|
+
nodes_dataset.order(:chunk_position).all
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Alias for nodes_dataset - used for consistency with "chunks" terminology
|
|
66
|
+
#
|
|
67
|
+
# @return [Sequel::Dataset] Dataset of nodes from this file
|
|
68
|
+
#
|
|
69
|
+
def chunks_dataset
|
|
70
|
+
nodes_dataset
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
# Extract tags from frontmatter
|
|
@@ -65,7 +75,7 @@ class HTM
|
|
|
65
75
|
# @return [Array<String>] Tag names from frontmatter 'tags' field
|
|
66
76
|
#
|
|
67
77
|
def frontmatter_tags
|
|
68
|
-
return [] unless
|
|
78
|
+
return [] unless frontmatter_hash?
|
|
69
79
|
|
|
70
80
|
tags = frontmatter['tags'] || frontmatter[:tags] || []
|
|
71
81
|
Array(tags).map(&:to_s)
|
|
@@ -76,7 +86,7 @@ class HTM
|
|
|
76
86
|
# @return [String, nil] Title from frontmatter
|
|
77
87
|
#
|
|
78
88
|
def title
|
|
79
|
-
return nil unless
|
|
89
|
+
return nil unless frontmatter_hash?
|
|
80
90
|
frontmatter['title'] || frontmatter[:title]
|
|
81
91
|
end
|
|
82
92
|
|
|
@@ -85,7 +95,7 @@ class HTM
|
|
|
85
95
|
# @return [String, nil] Author from frontmatter
|
|
86
96
|
#
|
|
87
97
|
def author
|
|
88
|
-
return nil unless
|
|
98
|
+
return nil unless frontmatter_hash?
|
|
89
99
|
frontmatter['author'] || frontmatter[:author]
|
|
90
100
|
end
|
|
91
101
|
|
|
@@ -94,7 +104,18 @@ class HTM
|
|
|
94
104
|
# @return [Integer] Number of chunks soft-deleted
|
|
95
105
|
#
|
|
96
106
|
def soft_delete_chunks!
|
|
97
|
-
|
|
107
|
+
nodes_dataset.update(deleted_at: Time.now)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Check if frontmatter is a hash-like object
|
|
113
|
+
# Sequel::Postgres::JSONBHash doesn't inherit from Hash but acts like one
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean]
|
|
116
|
+
#
|
|
117
|
+
def frontmatter_hash?
|
|
118
|
+
frontmatter.respond_to?(:[]) && frontmatter.respond_to?(:key?)
|
|
98
119
|
end
|
|
99
120
|
end
|
|
100
121
|
end
|