Commit c588640

mo khan <mo@mokhan.ca>
2025-09-01 21:08:47
feat: add memory feature
1 parent 58080c6
lib/elelem/toolbox/memory.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Toolbox
+    class Memory < Tool
+      MEMORY_DIR = ".elelem_memory"
+      MAX_MEMORY_SIZE = 1_000_000
+      
+      def initialize(configuration)
+        @configuration = configuration
+        @tui = configuration.tui
+        
+        super("memory", "Persistent memory for learning and context retention", {
+          type: :object,
+          properties: {
+            action: {
+              type: :string,
+              enum: %w[store retrieve list search forget],
+              description: "Memory action: store, retrieve, list, search, forget"
+            },
+            key: {
+              type: :string,
+              description: "Unique key for storing/retrieving memory"
+            },
+            content: {
+              type: :string,
+              description: "Content to store (required for store action)"
+            },
+            query: {
+              type: :string,
+              description: "Search query for finding memories"
+            }
+          },
+          required: %w[action]
+        })
+        ensure_memory_dir
+      end
+
+      def call(args)
+        action = args["action"]
+        
+        case action
+        when "store"
+          store_memory(args["key"], args["content"])
+        when "retrieve"
+          retrieve_memory(args["key"])
+        when "list"
+          list_memories
+        when "search"
+          search_memories(args["query"])
+        when "forget"
+          forget_memory(args["key"])
+        else
+          "Invalid memory action: #{action}"
+        end
+      rescue StandardError => e
+        "Memory error: #{e.message}"
+      end
+
+      private
+
+      attr_reader :configuration, :tui
+
+      def ensure_memory_dir
+        Dir.mkdir(MEMORY_DIR) unless Dir.exist?(MEMORY_DIR)
+      end
+
+      def memory_path(key)
+        ::File.join(MEMORY_DIR, "#{sanitize_key(key)}.json")
+      end
+
+      def sanitize_key(key)
+        key.to_s.gsub(/[^a-zA-Z0-9_-]/, "_").slice(0, 100)
+      end
+
+      def store_memory(key, content)
+        return "Key and content required for storing" unless key && content
+        
+        total_size = Dir.glob("#{MEMORY_DIR}/*.json").sum { |f| ::File.size(f) }
+        return "Memory capacity exceeded" if total_size > MAX_MEMORY_SIZE
+
+        memory = {
+          key: key,
+          content: content,
+          timestamp: Time.now.iso8601,
+          access_count: 0
+        }
+
+        ::File.write(memory_path(key), JSON.pretty_generate(memory))
+        "Memory stored: #{key}"
+      end
+
+      def retrieve_memory(key)
+        return "Key required for retrieval" unless key
+        
+        path = memory_path(key)
+        return "Memory not found: #{key}" unless ::File.exist?(path)
+
+        memory = JSON.parse(::File.read(path))
+        memory["access_count"] += 1
+        memory["last_accessed"] = Time.now.iso8601
+        
+        ::File.write(path, JSON.pretty_generate(memory))
+        memory["content"]
+      end
+
+      def list_memories
+        memories = Dir.glob("#{MEMORY_DIR}/*.json").map do |file|
+          memory = JSON.parse(::File.read(file))
+          {
+            key: memory["key"],
+            timestamp: memory["timestamp"],
+            size: memory["content"].length,
+            access_count: memory["access_count"] || 0
+          }
+        end
+        
+        memories.sort_by { |m| m[:timestamp] }.reverse
+        JSON.pretty_generate(memories)
+      end
+
+      def search_memories(query)
+        return "Query required for search" unless query
+        
+        matches = Dir.glob("#{MEMORY_DIR}/*.json").filter_map do |file|
+          memory = JSON.parse(::File.read(file))
+          if memory["content"].downcase.include?(query.downcase) ||
+             memory["key"].downcase.include?(query.downcase)
+            {
+              key: memory["key"],
+              snippet: memory["content"][0, 200] + "...",
+              relevance: calculate_relevance(memory, query)
+            }
+          end
+        end
+        
+        matches.sort_by { |m| -m[:relevance] }
+        JSON.pretty_generate(matches)
+      end
+
+      def forget_memory(key)
+        return "Key required for forgetting" unless key
+        
+        path = memory_path(key)
+        return "Memory not found: #{key}" unless ::File.exist?(path)
+
+        ::File.delete(path)
+        "Memory forgotten: #{key}"
+      end
+
+      def calculate_relevance(memory, query)
+        content = memory["content"].downcase
+        key = memory["key"].downcase
+        query = query.downcase
+        
+        score = 0
+        score += 3 if key.include?(query)
+        score += content.scan(query).length
+        score += (memory["access_count"] || 0) * 0.1
+        score
+      end
+    end
+  end
+end
\ No newline at end of file
lib/elelem/toolbox/prompt.rb
@@ -4,6 +4,7 @@ module Elelem
   module Toolbox
     class Prompt < Tool
       def initialize(configuration)
+        @configuration = configuration
         super("prompt", "Ask the user a question and get their response.", {
           type: :object,
           properties: {
@@ -16,8 +17,8 @@ module Elelem
         })
       end
 
-      def call(agent, **args)
-        agent.tui.prompt(args[:question])
+      def call(args)
+        @configuration.tui.prompt(args["question"])
       end
     end
   end
lib/elelem/configuration.rb
@@ -51,6 +51,7 @@ module Elelem
           Toolbox::File.new(self),
           Toolbox::Web.new(self),
           Toolbox::Prompt.new(self),
+          Toolbox::Memory.new(self),
         ] + mcp_tools
       )
     end
lib/elelem/system_prompt.erb
@@ -10,6 +10,7 @@ You are a self-improving coding assistant with the ability to modify your own so
 - `exec command="shell command"` - Run commands/tests
 - `file action=read|write path=filepath content=...` - Read/write files  
 - `web action=search|fetch query=... url=...` - Internet access
+- `memory action=store|retrieve|search key=... content=...` - Persistent memory
 - `prompt question="..."` - Ask user questions
 
 Context: <%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %> | <%= Dir.pwd %> | <%= `uname -a`.strip %>
lib/elelem/toolbox.rb
@@ -5,3 +5,4 @@ require_relative "toolbox/file"
 require_relative "toolbox/web"
 require_relative "toolbox/mcp"
 require_relative "toolbox/prompt"
+require_relative "toolbox/memory"
COMMANDMENTS.md
@@ -16,6 +16,7 @@ Change one thing at a time, test it, then move to the next. Small steps prevent
 - `file`: Only `action=read|write`, `path=filepath`, optional `content` for write
 - `exec`: Only `command="shell command"`  
 - `web`: Only `action=search|fetch`, optional `url` or `query`
+- `memory`: Only `action=store|retrieve|list|search|forget`, `key` for storage/retrieval
 - `prompt`: Only `question="what to ask user"`
 
 ## VI. Thou Shalt Track Progress
@@ -30,7 +31,10 @@ When uncertain, use `prompt` to ask the user for clarification rather than guess
 ## IX. Thou Shalt Validate Completion
 Don't stop until the task is actually complete. Test that your changes solve the original problem.
 
-## X. Thou Shalt Evolve
+## X. Thou Shalt Remember and Learn
+Store insights, solutions, and patterns in memory. Use `memory action=store key="topic" content="lesson learned"` to build persistent knowledge across sessions.
+
+## XI. Thou Shalt Evolve
 Your goal is continuous self-improvement. Each completed task should leave you more capable than before.
 
 ---
elelem.gemspec
@@ -56,6 +56,7 @@ Gem::Specification.new do |spec|
     "lib/elelem/toolbox/web.rb",
     "lib/elelem/toolbox/mcp.rb",
     "lib/elelem/toolbox/prompt.rb",
+    "lib/elelem/toolbox/memory.rb",
     "lib/elelem/tools.rb",
     "lib/elelem/tui.rb",
     "lib/elelem/version.rb",