llmemory 0.1.10 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +183 -0
- data/exe/llmemory-mcp +44 -0
- data/lib/llmemory/cli/commands/mcp.rb +129 -0
- data/lib/llmemory/cli.rb +4 -1
- data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +32 -0
- data/lib/llmemory/long_term/file_based/storages/base.rb +8 -0
- data/lib/llmemory/long_term/file_based/storages/database_storage.rb +34 -0
- data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +36 -0
- data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +26 -0
- data/lib/llmemory/long_term/graph_based/storages/base.rb +4 -0
- data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +23 -0
- data/lib/llmemory/mcp/authentication.rb +70 -0
- data/lib/llmemory/mcp/server.rb +185 -0
- data/lib/llmemory/mcp/tools/memory_add_message.rb +47 -0
- data/lib/llmemory/mcp/tools/memory_consolidate.rb +57 -0
- data/lib/llmemory/mcp/tools/memory_info.rb +79 -0
- data/lib/llmemory/mcp/tools/memory_retrieve.rb +122 -0
- data/lib/llmemory/mcp/tools/memory_save.rb +58 -0
- data/lib/llmemory/mcp/tools/memory_search.rb +134 -0
- data/lib/llmemory/mcp/tools/memory_stats.rb +111 -0
- data/lib/llmemory/mcp/tools/memory_timeline.rb +120 -0
- data/lib/llmemory/mcp/tools/memory_timeline_context.rb +140 -0
- data/lib/llmemory/mcp.rb +8 -0
- data/lib/llmemory/version.rb +1 -1
- metadata +30 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MemorySearch < ::MCP::Tool
|
|
7
|
+
description "Search through user's memory for relevant information. Returns facts, observations, and context matching the query."
|
|
8
|
+
|
|
9
|
+
input_schema(
|
|
10
|
+
properties: {
|
|
11
|
+
query: { type: "string", description: "Search query to find relevant memories" },
|
|
12
|
+
user_id: { type: "string", description: "User identifier" },
|
|
13
|
+
search_type: {
|
|
14
|
+
type: "string",
|
|
15
|
+
enum: ["all", "short_term", "long_term"],
|
|
16
|
+
description: "Where to search: all (default), short_term, or long_term"
|
|
17
|
+
},
|
|
18
|
+
max_results: { type: "integer", description: "Maximum results (default: 10)" }
|
|
19
|
+
},
|
|
20
|
+
required: ["query", "user_id"]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def call(query:, user_id:, search_type: "all", max_results: 10, server_context: nil)
|
|
25
|
+
results = []
|
|
26
|
+
search_type = (search_type || "all").downcase
|
|
27
|
+
max_results = max_results || 10
|
|
28
|
+
|
|
29
|
+
if search_type == "all" || search_type == "short_term"
|
|
30
|
+
results.concat(search_short_term(user_id, query, max_results))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if search_type == "all" || search_type == "long_term"
|
|
34
|
+
results.concat(search_long_term(user_id, query, max_results))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
::MCP::Tool::Response.new([{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: format_results(results.first(max_results))
|
|
40
|
+
}])
|
|
41
|
+
rescue => e
|
|
42
|
+
::MCP::Tool::Response.new([{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: "Error searching memory: #{e.message}"
|
|
45
|
+
}], error: true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def search_short_term(user_id, query, limit)
|
|
51
|
+
store = build_short_term_store
|
|
52
|
+
sessions = store.list_sessions(user_id: user_id)
|
|
53
|
+
results = []
|
|
54
|
+
query_lower = query.downcase
|
|
55
|
+
|
|
56
|
+
sessions.each do |session_id|
|
|
57
|
+
state = store.load(user_id, session_id)
|
|
58
|
+
next unless state.is_a?(Hash)
|
|
59
|
+
messages = state[:messages] || state["messages"] || []
|
|
60
|
+
messages.each do |m|
|
|
61
|
+
content = (m[:content] || m["content"]).to_s
|
|
62
|
+
if content.downcase.include?(query_lower)
|
|
63
|
+
results << {
|
|
64
|
+
type: "short_term",
|
|
65
|
+
session_id: session_id,
|
|
66
|
+
role: m[:role] || m["role"],
|
|
67
|
+
content: content
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
results.first(limit)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def search_long_term(user_id, query, limit)
|
|
77
|
+
storage = build_long_term_storage
|
|
78
|
+
results = []
|
|
79
|
+
|
|
80
|
+
items = storage.search_items(user_id, query)
|
|
81
|
+
items.first(limit).each do |item|
|
|
82
|
+
results << {
|
|
83
|
+
type: "long_term_fact",
|
|
84
|
+
category: item[:category] || item["category"],
|
|
85
|
+
content: item[:content] || item["content"],
|
|
86
|
+
created_at: item[:created_at] || item["created_at"]
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
results
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_short_term_store
|
|
94
|
+
case Llmemory.configuration.short_term_store.to_sym
|
|
95
|
+
when :memory then ShortTerm::Stores::MemoryStore.new
|
|
96
|
+
when :redis then ShortTerm::Stores::RedisStore.new
|
|
97
|
+
when :postgres then ShortTerm::Stores::PostgresStore.new
|
|
98
|
+
when :active_record, :activerecord
|
|
99
|
+
require_relative "../../short_term/stores/active_record_store"
|
|
100
|
+
ShortTerm::Stores::ActiveRecordStore.new
|
|
101
|
+
else
|
|
102
|
+
ShortTerm::Stores::MemoryStore.new
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_long_term_storage
|
|
107
|
+
LongTerm::FileBased::Storages.build
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def format_results(results)
|
|
111
|
+
return "No memories found matching the query." if results.empty?
|
|
112
|
+
|
|
113
|
+
output = ["Found #{results.size} memories:\n"]
|
|
114
|
+
results.each_with_index do |r, i|
|
|
115
|
+
case r[:type]
|
|
116
|
+
when "short_term"
|
|
117
|
+
output << "#{i + 1}. [Session: #{r[:session_id]}] [#{r[:role]}] #{truncate(r[:content], 200)}"
|
|
118
|
+
when "long_term_fact"
|
|
119
|
+
cat_info = r[:category] ? "[#{r[:category]}]" : ""
|
|
120
|
+
output << "#{i + 1}. #{cat_info} #{truncate(r[:content], 200)}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
output.join("\n")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def truncate(text, max_length)
|
|
127
|
+
return text if text.length <= max_length
|
|
128
|
+
"#{text[0, max_length]}..."
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MemoryStats < ::MCP::Tool
|
|
7
|
+
description "Get memory statistics for a user including message counts, fact counts, and categories."
|
|
8
|
+
|
|
9
|
+
input_schema(
|
|
10
|
+
properties: {
|
|
11
|
+
user_id: { type: "string", description: "User identifier" }
|
|
12
|
+
},
|
|
13
|
+
required: ["user_id"]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def call(user_id:, server_context: nil)
|
|
18
|
+
stats = {
|
|
19
|
+
user_id: user_id,
|
|
20
|
+
short_term: {},
|
|
21
|
+
long_term: {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Short-term stats
|
|
25
|
+
store = build_short_term_store
|
|
26
|
+
sessions = store.list_sessions(user_id: user_id)
|
|
27
|
+
total_messages = 0
|
|
28
|
+
sessions.each do |session_id|
|
|
29
|
+
state = store.load(user_id, session_id)
|
|
30
|
+
next unless state.is_a?(Hash)
|
|
31
|
+
messages = state[:messages] || state["messages"] || []
|
|
32
|
+
total_messages += messages.size
|
|
33
|
+
end
|
|
34
|
+
stats[:short_term] = {
|
|
35
|
+
sessions: sessions.size,
|
|
36
|
+
total_messages: total_messages
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Long-term stats
|
|
40
|
+
storage = build_long_term_storage
|
|
41
|
+
begin
|
|
42
|
+
item_count = storage.count_items(user_id: user_id)
|
|
43
|
+
categories = storage.list_categories(user_id)
|
|
44
|
+
resources = storage.list_resources(user_id: user_id)
|
|
45
|
+
stats[:long_term] = {
|
|
46
|
+
facts: item_count,
|
|
47
|
+
categories: categories.size,
|
|
48
|
+
category_names: categories,
|
|
49
|
+
resources: resources.size
|
|
50
|
+
}
|
|
51
|
+
rescue => e
|
|
52
|
+
stats[:long_term] = { error: e.message }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
::MCP::Tool::Response.new([{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: format_stats(stats)
|
|
58
|
+
}])
|
|
59
|
+
rescue => e
|
|
60
|
+
::MCP::Tool::Response.new([{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: "Error getting stats: #{e.message}"
|
|
63
|
+
}], error: true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def build_short_term_store
|
|
69
|
+
case Llmemory.configuration.short_term_store.to_sym
|
|
70
|
+
when :memory then ShortTerm::Stores::MemoryStore.new
|
|
71
|
+
when :redis then ShortTerm::Stores::RedisStore.new
|
|
72
|
+
when :postgres then ShortTerm::Stores::PostgresStore.new
|
|
73
|
+
when :active_record, :activerecord
|
|
74
|
+
require_relative "../../short_term/stores/active_record_store"
|
|
75
|
+
ShortTerm::Stores::ActiveRecordStore.new
|
|
76
|
+
else
|
|
77
|
+
ShortTerm::Stores::MemoryStore.new
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_long_term_storage
|
|
82
|
+
LongTerm::FileBased::Storages.build
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def format_stats(stats)
|
|
86
|
+
output = ["Memory Statistics for user '#{stats[:user_id]}':\n"]
|
|
87
|
+
|
|
88
|
+
output << "SHORT-TERM MEMORY:"
|
|
89
|
+
output << " Sessions: #{stats[:short_term][:sessions]}"
|
|
90
|
+
output << " Total messages: #{stats[:short_term][:total_messages]}"
|
|
91
|
+
output << ""
|
|
92
|
+
|
|
93
|
+
output << "LONG-TERM MEMORY:"
|
|
94
|
+
if stats[:long_term][:error]
|
|
95
|
+
output << " Error: #{stats[:long_term][:error]}"
|
|
96
|
+
else
|
|
97
|
+
output << " Facts stored: #{stats[:long_term][:facts]}"
|
|
98
|
+
output << " Categories: #{stats[:long_term][:categories]}"
|
|
99
|
+
if stats[:long_term][:category_names]&.any?
|
|
100
|
+
output << " Category names: #{stats[:long_term][:category_names].join(', ')}"
|
|
101
|
+
end
|
|
102
|
+
output << " Resources: #{stats[:long_term][:resources]}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
output.join("\n")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MemoryTimeline < ::MCP::Tool
|
|
7
|
+
description "Get chronological timeline of recent memories and interactions for a user."
|
|
8
|
+
|
|
9
|
+
input_schema(
|
|
10
|
+
properties: {
|
|
11
|
+
user_id: { type: "string", description: "User identifier" },
|
|
12
|
+
hours: { type: "integer", description: "Hours to look back (default: 24)" },
|
|
13
|
+
include_messages: { type: "boolean", description: "Include short-term messages (default: true)" }
|
|
14
|
+
},
|
|
15
|
+
required: ["user_id"]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def call(user_id:, hours: nil, include_messages: nil, server_context: nil)
|
|
20
|
+
hours = hours || 24
|
|
21
|
+
include_msgs = include_messages.nil? ? true : include_messages
|
|
22
|
+
|
|
23
|
+
timeline = []
|
|
24
|
+
|
|
25
|
+
# Get recent items from long-term memory
|
|
26
|
+
storage = build_long_term_storage
|
|
27
|
+
begin
|
|
28
|
+
items = storage.get_items_since(user_id, hours: hours)
|
|
29
|
+
items.each do |item|
|
|
30
|
+
timeline << {
|
|
31
|
+
type: "fact",
|
|
32
|
+
timestamp: item[:created_at] || item["created_at"],
|
|
33
|
+
category: item[:category] || item["category"],
|
|
34
|
+
content: item[:content] || item["content"]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
rescue NotImplementedError
|
|
38
|
+
# Some storages may not implement get_items_since
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Get recent messages from short-term
|
|
42
|
+
if include_msgs
|
|
43
|
+
store = build_short_term_store
|
|
44
|
+
sessions = store.list_sessions(user_id: user_id)
|
|
45
|
+
sessions.each do |session_id|
|
|
46
|
+
state = store.load(user_id, session_id)
|
|
47
|
+
next unless state.is_a?(Hash)
|
|
48
|
+
messages = state[:messages] || state["messages"] || []
|
|
49
|
+
messages.each_with_index do |m, idx|
|
|
50
|
+
timeline << {
|
|
51
|
+
type: "message",
|
|
52
|
+
timestamp: state[:updated_at] || Time.now,
|
|
53
|
+
session_id: session_id,
|
|
54
|
+
role: m[:role] || m["role"],
|
|
55
|
+
content: m[:content] || m["content"],
|
|
56
|
+
order: idx
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Sort by timestamp (most recent first)
|
|
63
|
+
timeline.sort_by! { |t| t[:timestamp].to_s }.reverse!
|
|
64
|
+
|
|
65
|
+
::MCP::Tool::Response.new([{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: format_timeline(timeline, hours)
|
|
68
|
+
}])
|
|
69
|
+
rescue => e
|
|
70
|
+
::MCP::Tool::Response.new([{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: "Error getting timeline: #{e.message}"
|
|
73
|
+
}], error: true)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def build_short_term_store
|
|
79
|
+
case Llmemory.configuration.short_term_store.to_sym
|
|
80
|
+
when :memory then ShortTerm::Stores::MemoryStore.new
|
|
81
|
+
when :redis then ShortTerm::Stores::RedisStore.new
|
|
82
|
+
when :postgres then ShortTerm::Stores::PostgresStore.new
|
|
83
|
+
when :active_record, :activerecord
|
|
84
|
+
require_relative "../../short_term/stores/active_record_store"
|
|
85
|
+
ShortTerm::Stores::ActiveRecordStore.new
|
|
86
|
+
else
|
|
87
|
+
ShortTerm::Stores::MemoryStore.new
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_long_term_storage
|
|
92
|
+
LongTerm::FileBased::Storages.build
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def format_timeline(timeline, hours)
|
|
96
|
+
return "No activity in the last #{hours} hours." if timeline.empty?
|
|
97
|
+
|
|
98
|
+
output = ["Timeline (last #{hours} hours):\n"]
|
|
99
|
+
timeline.first(20).each do |entry|
|
|
100
|
+
case entry[:type]
|
|
101
|
+
when "fact"
|
|
102
|
+
cat_info = entry[:category] ? "[#{entry[:category]}]" : ""
|
|
103
|
+
output << "- [FACT] #{cat_info} #{truncate(entry[:content], 150)}"
|
|
104
|
+
when "message"
|
|
105
|
+
output << "- [MSG #{entry[:session_id]}] [#{entry[:role]}] #{truncate(entry[:content], 150)}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
output << "\n(Showing #{[timeline.size, 20].min} of #{timeline.size} entries)"
|
|
109
|
+
output.join("\n")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def truncate(text, max_length)
|
|
113
|
+
return text.to_s if text.to_s.length <= max_length
|
|
114
|
+
"#{text.to_s[0, max_length]}..."
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Llmemory
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class MemoryTimelineContext < ::MCP::Tool
|
|
7
|
+
description "Get temporal context around a specific memory. Shows N events before and after a given item or timestamp."
|
|
8
|
+
|
|
9
|
+
input_schema(
|
|
10
|
+
properties: {
|
|
11
|
+
user_id: { type: "string", description: "User identifier" },
|
|
12
|
+
item_id: { type: "string", description: "ID of the item to get context for (e.g., from search results)" },
|
|
13
|
+
timestamp: { type: "string", description: "ISO timestamp to get context around (alternative to item_id)" },
|
|
14
|
+
before: { type: "integer", description: "Number of items before the target (default: 5)" },
|
|
15
|
+
after: { type: "integer", description: "Number of items after the target (default: 5)" }
|
|
16
|
+
},
|
|
17
|
+
required: ["user_id"]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def call(user_id:, item_id: nil, timestamp: nil, before: nil, after: nil, server_context: nil)
|
|
22
|
+
before_count = before || 5
|
|
23
|
+
after_count = after || 5
|
|
24
|
+
|
|
25
|
+
reference = item_id || timestamp
|
|
26
|
+
unless reference
|
|
27
|
+
return ::MCP::Tool::Response.new([{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: "Error: Either item_id or timestamp must be provided"
|
|
30
|
+
}], error: true)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
storage = build_storage
|
|
34
|
+
result = storage.get_items_around(user_id, reference, before: before_count, after: after_count)
|
|
35
|
+
|
|
36
|
+
::MCP::Tool::Response.new([{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: format_context(result, reference)
|
|
39
|
+
}])
|
|
40
|
+
rescue => e
|
|
41
|
+
::MCP::Tool::Response.new([{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: "Error getting timeline context: #{e.message}"
|
|
44
|
+
}], error: true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_storage
|
|
50
|
+
if Llmemory.configuration.long_term_type.to_s == "graph_based"
|
|
51
|
+
LongTerm::GraphBased::Storages.build
|
|
52
|
+
else
|
|
53
|
+
LongTerm::FileBased::Storages.build
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def format_context(result, reference)
|
|
58
|
+
output = []
|
|
59
|
+
output << "Timeline Context around '#{reference}':\n"
|
|
60
|
+
|
|
61
|
+
if result[:before].empty? && result[:target].nil? && result[:after].empty?
|
|
62
|
+
return "No memories found around reference '#{reference}'"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Before section
|
|
66
|
+
output << "BEFORE (#{result[:before].size} items):"
|
|
67
|
+
if result[:before].empty?
|
|
68
|
+
output << " (no earlier items)"
|
|
69
|
+
else
|
|
70
|
+
result[:before].each do |item|
|
|
71
|
+
output << format_item(item)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Target section
|
|
76
|
+
output << "\nTARGET:"
|
|
77
|
+
if result[:target]
|
|
78
|
+
output << format_item(result[:target], highlight: true)
|
|
79
|
+
else
|
|
80
|
+
output << " (target not found)"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# After section
|
|
84
|
+
output << "\nAFTER (#{result[:after].size} items):"
|
|
85
|
+
if result[:after].empty?
|
|
86
|
+
output << " (no later items)"
|
|
87
|
+
else
|
|
88
|
+
result[:after].each do |item|
|
|
89
|
+
output << format_item(item)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
output.join("\n")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def format_item(item, highlight: false)
|
|
97
|
+
prefix = highlight ? ">>> " : " - "
|
|
98
|
+
timestamp = format_timestamp(item)
|
|
99
|
+
content = extract_content(item)
|
|
100
|
+
category = extract_category(item)
|
|
101
|
+
|
|
102
|
+
cat_info = category ? "[#{category}] " : ""
|
|
103
|
+
"#{prefix}[#{timestamp}] #{cat_info}#{truncate(content, 120)}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def format_timestamp(item)
|
|
107
|
+
ts = item[:created_at] || item["created_at"] || item.created_at rescue nil
|
|
108
|
+
return "unknown" unless ts
|
|
109
|
+
ts.respond_to?(:strftime) ? ts.strftime("%Y-%m-%d %H:%M") : ts.to_s[0, 16]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_content(item)
|
|
113
|
+
# Handle both hash and object representations
|
|
114
|
+
if item.respond_to?(:content)
|
|
115
|
+
item.content
|
|
116
|
+
elsif item.respond_to?(:predicate)
|
|
117
|
+
# Graph-based edge
|
|
118
|
+
"#{item.subject_id} -> #{item.predicate} -> #{item.target_id}"
|
|
119
|
+
else
|
|
120
|
+
item[:content] || item["content"] || item.to_s
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def extract_category(item)
|
|
125
|
+
if item.respond_to?(:category)
|
|
126
|
+
item.category
|
|
127
|
+
else
|
|
128
|
+
item[:category] || item["category"]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def truncate(text, max_length)
|
|
133
|
+
return text.to_s if text.to_s.length <= max_length
|
|
134
|
+
"#{text.to_s[0, max_length]}..."
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/llmemory/mcp.rb
ADDED
data/lib/llmemory/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llmemory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- llmemory
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mcp
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.6'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.6'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: rspec
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -71,6 +85,7 @@ email:
|
|
|
71
85
|
- ''
|
|
72
86
|
executables:
|
|
73
87
|
- llmemory
|
|
88
|
+
- llmemory-mcp
|
|
74
89
|
extensions: []
|
|
75
90
|
extra_rdoc_files: []
|
|
76
91
|
files:
|
|
@@ -94,6 +109,7 @@ files:
|
|
|
94
109
|
- app/views/llmemory/dashboard/users/show.html.erb
|
|
95
110
|
- config/routes.rb
|
|
96
111
|
- exe/llmemory
|
|
112
|
+
- exe/llmemory-mcp
|
|
97
113
|
- lib/generators/llmemory/install/install_generator.rb
|
|
98
114
|
- lib/generators/llmemory/install/templates/create_llmemory_tables.rb
|
|
99
115
|
- lib/llmemory.rb
|
|
@@ -106,6 +122,7 @@ files:
|
|
|
106
122
|
- lib/llmemory/cli/commands/long_term/graph.rb
|
|
107
123
|
- lib/llmemory/cli/commands/long_term/nodes.rb
|
|
108
124
|
- lib/llmemory/cli/commands/long_term/resources.rb
|
|
125
|
+
- lib/llmemory/cli/commands/mcp.rb
|
|
109
126
|
- lib/llmemory/cli/commands/search.rb
|
|
110
127
|
- lib/llmemory/cli/commands/short_term.rb
|
|
111
128
|
- lib/llmemory/cli/commands/stats.rb
|
|
@@ -150,6 +167,18 @@ files:
|
|
|
150
167
|
- lib/llmemory/maintenance/reindexer.rb
|
|
151
168
|
- lib/llmemory/maintenance/runner.rb
|
|
152
169
|
- lib/llmemory/maintenance/summarizer.rb
|
|
170
|
+
- lib/llmemory/mcp.rb
|
|
171
|
+
- lib/llmemory/mcp/authentication.rb
|
|
172
|
+
- lib/llmemory/mcp/server.rb
|
|
173
|
+
- lib/llmemory/mcp/tools/memory_add_message.rb
|
|
174
|
+
- lib/llmemory/mcp/tools/memory_consolidate.rb
|
|
175
|
+
- lib/llmemory/mcp/tools/memory_info.rb
|
|
176
|
+
- lib/llmemory/mcp/tools/memory_retrieve.rb
|
|
177
|
+
- lib/llmemory/mcp/tools/memory_save.rb
|
|
178
|
+
- lib/llmemory/mcp/tools/memory_search.rb
|
|
179
|
+
- lib/llmemory/mcp/tools/memory_stats.rb
|
|
180
|
+
- lib/llmemory/mcp/tools/memory_timeline.rb
|
|
181
|
+
- lib/llmemory/mcp/tools/memory_timeline_context.rb
|
|
153
182
|
- lib/llmemory/memory.rb
|
|
154
183
|
- lib/llmemory/retrieval.rb
|
|
155
184
|
- lib/llmemory/retrieval/context_assembler.rb
|