Comparing changes

v0.1.3 v0.2.0
38 commits 25 files changed

Commits

54f47a8 chore: prepare 0.2.0 release mo khan 2025-10-16 15:50:55
bd09dda chore: remove COMMANDMENTS.md mo khan 2025-10-16 15:45:55
fc47d38 feat: change default model mo khan 2025-10-15 23:12:29
a3afd97 feat: split openai and ollama scripts mo khan 2025-10-15 23:10:44
6aac510 Add llm-simp script mo khan 2025-10-15 23:04:15
5d05bce Fix response processing mo khan 2025-10-15 23:02:43
bca2227 Use net-llm for API calls mo khan 2025-10-15 23:00:17
9660850 chore: update version of bundler mo khan 2025-10-09 15:33:40
25b0307 chore: add error to log mo khan 2025-09-06 16:26:24
d4f5a4c fix: use safe navigation operator mo khan 2025-09-06 16:21:36
e3c3873 fix: typo mo khan 2025-09-06 15:50:03
6f1f446 feat: add error handling mo khan 2025-09-01 22:37:49
c588640 feat: add memory feature mo khan 2025-09-01 21:08:47
58080c6 refactor: read and write to files mo khan 2025-09-01 20:58:08
0785d8a style: cleanup tools mo khan 2025-09-01 20:50:02
a75bb7a refactor: cleanup fetch tool mo khan 2025-09-01 20:45:07
6ea8456 feat: improve the system prompt mo khan 2025-09-01 18:20:45
958d50f feat: add web fetch tool mo khan 2025-09-01 18:14:49
a954505 refactor: sort of useful mo khan 2025-09-01 18:05:54
a2547ec refactor: try a simpler approach mo khan 2025-09-01 17:08:01
ee75cce refactor: improve search tool mo khan 2025-08-28 17:42:34
91f7747 fix: parse the tool_call mo khan 2025-08-28 17:10:57
a6fef70 chore: remove dead code mo khan 2025-08-28 16:08:04
293ba52 fix: removed named parameters mo khan 2025-08-20 08:08:45
666d3f0 feat: add a prompt tool mo khan 2025-08-20 07:50:26
c147785 chore: remove challenge signature mo khan 2025-08-18 19:14:35
dadb2bc feat: attempt to connect to ollama.com mo khan 2025-08-18 19:08:45
exe/llm-ollama
@@ -0,0 +1,358 @@
+#!/usr/bin/env ruby
+
+=begin
+Fast, correct, autonomous - Pick two
+
+PURPOSE:
+
+This script is a minimal coding agent written in Ruby. It is intended to
+assist me (a software engineer and computer science student) with writing,
+editing, and managing code and text files from the command line. It acts
+as a direct interface to an LLM, providing it with a simple text-based
+UI and access to the local filesystem.
+
+DESIGN PRINCIPLES:
+
+- Follows the Unix philosophy: simple, composable, minimal.
+- Convention over configuration.
+- Avoids unnecessary defensive checks, or complexity.
+- Assumes a mature and responsible LLM that behaves like a capable engineer.
+- Designed for my workflow and preferences.
+- Efficient and minimal like aider - https://aider.chat/
+- UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
+
+SYSTEM ASSUMPTIONS:
+
+- This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
+- It is always run inside a Git repository.
+- All project work is assumed to be version-controlled with Git.
+- Git is expected to be available and working; no checks are necessary.
+
+SCOPE:
+
+- This program operates only on code and plain-text files.
+- It does not need to support binary files.
+- The LLM has full access to execute system commands.
+- There are no sandboxing, permission, or validation layers.
+- Execution is not restricted or monitored — responsibility is delegated to the LLM.
+
+CONFIGURATION:
+
+- Avoid adding configuration options unless absolutely necessary.
+- Prefer hard-coded values that can be changed later if needed.
+- Only introduce environment variables after repeated usage proves them worthwhile.
+
+UI EXPECTATIONS:
+
+- The TUI must remain simple, fast, and predictable.
+- No mouse support or complex UI components are required.
+- Interaction is strictly keyboard-driven.
+
+CODING STANDARDS FOR LLM:
+
+- Do not add error handling or logging unless it is essential for functionality.
+- Keep methods short and single-purpose.
+- Use descriptive, conventional names.
+- Stick to Ruby's standard library whenever possible.
+
+HELPFUL LINKS:
+
+- https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
+- https://www.anthropic.com/engineering/writing-tools-for-agents
+- https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
+
+=end
+
+require "bundler/inline"
+
+gemfile do
+  source "https://rubygems.org"
+
+  gem "fileutils", "~> 1.0"
+  gem "json", "~> 2.0"
+  gem "net-llm", "~> 0.4"
+  gem "open3", "~> 0.1"
+  gem "ostruct", "~> 0.1"
+  gem "reline", "~> 0.1"
+  gem "set", "~> 1.0"
+  gem "uri", "~> 1.0"
+end
+
+STDOUT.set_encoding(Encoding::UTF_8)
+STDERR.set_encoding(Encoding::UTF_8)
+
+OLLAMA_HOST = ENV["OLLAMA_HOST"] || "localhost:11434"
+OLLAMA_MODEL = ENV["OLLAMA_MODEL"] || "gpt-oss:latest"
+SYSTEM_PROMPT="You are a reasoning coding and system agent."
+
+def build_tool(name, description, properties, required = [])
+  {
+    type: "function",
+    function: {
+      name: name,
+      description: description,
+      parameters: {
+        type: "object",
+        properties: properties,
+        required: required
+      }
+    }
+  }
+end
+
+EXEC_TOOL = build_tool("execute", "Execute shell commands. Returns stdout, stderr, and exit code. Use for: checking system state, running tests, managing services. Common Unix tools available: git, bash, grep, etc. Tip: Check exit_status in response to determine success.", { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, env: { type: "object", additionalProperties: { type: "string" } }, cwd: { type: "string" }, stdin: { type: "string" } }, ["cmd"])
+GREP_TOOL = build_tool("grep", "Search all git-tracked files using git grep. Returns file paths with matching line numbers. Use this to discover where code/configuration exists before reading files. Examples: search 'def method_name' to find method definitions. Much faster than reading multiple files.", { query: { type: "string" } }, ["query"])
+LS_TOOL = build_tool("list", "List all git-tracked files in the repository, optionally filtered by path. Use this to explore project structure or find files in a directory. Returns relative paths from repo root. Tip: Use this before reading if you need to discover what files exist.", { path: { type: "string" } })
+PATCH_TOOL = build_tool("patch", "Apply a unified diff patch via 'git apply'. Use this for surgical edits to existing files rather than rewriting entire files. Generates proper git diffs. Format: standard unified diff with --- and +++ headers. Tip: More efficient than write for small changes to large files.", { diff: { type: "string" } }, ["diff"])
+READ_TOOL = build_tool("read", "Read complete contents of a file. Requires exact file path. Use grep or list first if you don't know the path. Best for: understanding existing code, reading config files, reviewing implementation details. Tip: For large files, grep first to confirm relevance.", { path: { type: "string" } }, ["path"])
+WRITE_TOOL = build_tool("write", "Write complete file contents (overwrites existing files). Creates parent directories automatically. Best for: creating new files, replacing entire file contents. For small edits to existing files, consider using patch instead.", { path: { type: "string" }, content: { type: "string" } }, ["path", "content"])
+
+TOOLS = {
+  read: [GREP_TOOL, LS_TOOL, READ_TOOL],
+  write: [PATCH_TOOL, WRITE_TOOL],
+  execute: [EXEC_TOOL]
+}
+
+trap("INT") do
+  puts "\nExiting."
+  exit
+end
+
+def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
+  stdout, stderr, status = Open3.capture3(env, command, *args, chdir: cwd, stdin_data: stdin)
+  {
+    "exit_status" => status.exitstatus,
+    "stdout" => stdout.to_s,
+    "stderr" => stderr.to_s
+  }
+end
+
+def expand_path(path)
+  Pathname.new(path).expand_path
+end
+
+def read_file(path)
+  full_path = expand_path(path)
+  full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
+end
+
+def write_file(path, content)
+  full_path = expand_path(path)
+  FileUtils.mkdir_p(full_path.dirname)
+  { bytes_written: full_path.write(content) }
+end
+
+def run_tool(name, args)
+  case name
+  when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"] || Dir.pwd, stdin: args["stdin"])
+  when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
+  when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
+  when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
+  when "read" then read_file(args["path"])
+  when "write" then write_file(args["path"], args["content"])
+  else
+    { error: "Unknown tool", name: name, args: args }
+  end
+end
+
+def format_tool_call(name, args)
+  case name
+  when "execute" then "execute(#{args["cmd"]})"
+  when "grep" then "grep(#{args["query"]})"
+  when "list" then "list(#{args["path"] || '.'})"
+  when "patch" then "patch(#{args["diff"].lines.count} lines)"
+  when "read" then "read(#{args["path"]})"
+  when "write" then "write(#{args["path"]})"
+  else
+    "▶ #{name}(#{args.to_s[0...70]})"
+  end
+end
+
+def system_prompt_for(mode)
+  base = "You are a reasoning coding and system agent."
+
+  case mode.sort
+  when [:read]
+    "#{base}\n\nRead and analyze. Understand before suggesting action."
+  when [:write]
+    "#{base}\n\nWrite clean, thoughtful code."
+  when [:execute]
+    "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
+  when [:read, :write]
+    "#{base}\n\nFirst understand, then build solutions that integrate well."
+  when [:read, :execute]
+    "#{base}\n\nUse commands to deeply understand the system."
+  when [:write, :execute]
+    "#{base}\n\nCreate and execute freely. Have fun. Be kind."
+  when [:read, :write, :execute]
+    "#{base}\n\nYou have all tools. Use them wisely."
+  else
+    base
+  end
+end
+
+def tools_for(modes)
+  modes.map { |mode| TOOLS[mode] }.flatten
+end
+
+def prune_context(messages, keep_recent: 5)
+  return messages if messages.length <= keep_recent + 1
+
+  default_context + messages.last(keep_recent)
+end
+
+def execute_turn(client, messages, tools:)
+  turn_context = []
+
+  loop do
+    content = ""
+    tool_calls = nil
+    role = "assistant"
+    first_content = true
+
+    print "Thinking..."
+    client.chat(messages + turn_context, tools) do |chunk|
+      if chunk["message"]
+        msg = chunk["message"]
+        role = msg["role"] if msg["role"]
+
+        if msg["thinking"] && !msg["thinking"].empty?
+          print "."
+        end
+
+        if msg["content"] && !msg["content"].empty?
+          if first_content
+            print "\r\e[KAssistant> "
+            first_content = false
+          end
+          print msg["content"]
+          $stdout.flush
+          content += msg["content"]
+        end
+
+        tool_calls = msg["tool_calls"] if msg["tool_calls"]
+      end
+    end
+    puts
+
+    turn_context << { role: role, content: content, tool_calls: tool_calls }.compact
+
+    if tool_calls
+      tool_calls.each do |call|
+        name = call.dig("function", "name")
+        args_raw = call.dig("function", "arguments")
+
+        begin
+          args = args_raw.is_a?(String) ? JSON.parse(args_raw) : args_raw
+        rescue JSON::ParserError => e
+          turn_context << {
+            role: "tool",
+            content: JSON.dump({
+              error: "Invalid JSON in arguments: #{e.message}",
+              received: args_raw
+            })
+          }
+          next
+        end
+
+        puts "Tool> #{format_tool_call(name, args)}"
+        result = run_tool(name, args)
+        turn_context << { role: "tool", content: JSON.dump(result) }
+      end
+      next
+    end
+
+    return { role: "assistant", content: content } unless content.strip.empty?
+  end
+end
+
+def dump_context(messages)
+  puts JSON.pretty_generate(messages)
+end
+
+def print_status(mode, messages)
+  puts "Mode: #{mode.inspect}"
+  puts "Tools: #{tools_for(mode).map { |x| x.dig(:function, :name) }}"
+end
+
+def strip_ansi(text)
+  text.gsub(/^Script started.*?\n/, '')
+      .gsub(/\nScript done.*$/, '')
+      .gsub(/\e\[[0-9;]*[a-zA-Z]/, '')        # Standard ANSI codes
+      .gsub(/\e\[\?[0-9]+[hl]/, '')           # Bracketed paste mode
+      .gsub(/[\b]/, '')                       # Backspace chars
+      .gsub(/\r/, '')                         # Carriage returns
+end
+
+def start_shell
+  Tempfile.create do |file|
+    system("script -q #{file.path}")
+    { role: "user", content: strip_ansi(File.read(file.path)) }
+  end
+end
+
+def ask?(text)
+  input = Reline.readline(text, true)&.strip
+  exit if input.nil? || input.downcase == "exit"
+
+  input
+end
+
+def print_help
+  puts <<~HELP
+  /chmod - (+|-)rwx auto build plan
+  /clear
+  /context
+  /exit
+  /help
+  /shell
+  /status
+  HELP
+end
+
+def default_context
+  [{ role: "system", content: SYSTEM_PROMPT }]
+end
+
+def main
+  client = Net::Llm::Ollama.new(
+    host: OLLAMA_HOST,
+    model: OLLAMA_MODEL
+  )
+
+  messages = default_context
+  mode = Set.new([:read])
+
+  loop do
+    input = ask?("User> ")
+    if input.start_with?("/")
+      case input
+      when "/chmod +r" then mode.add(:read)
+      when "/chmod +w" then mode.add(:write)
+      when "/chmod +x" then mode.add(:execute)
+      when "/chmod -r" then mode.add(:read)
+      when "/chmod -w" then mode.add(:write)
+      when "/chmod -x" then mode.add(:execute)
+      when "/clear" then messages = default_context
+      when "/compact" then messages = prune_context(messages, keep_recent: 10)
+      when "/context" then dump_context(messages)
+      when "/exit" then exit
+      when "/help" then print_help
+      when "/mode auto" then mode = Set[:read, :write, :execute]
+      when "/mode build" then mode = Set[:read, :write]
+      when "/mode plan" then mode = Set[:read]
+      when "/mode verify" then mode = Set[:read, :execute]
+      when "/mode" then print_status(mode, messages)
+      when "/shell" then messages << start_shell
+      else
+        print_help
+      end
+    else
+      messages[0] = { role: "system", content: system_prompt_for(mode) }
+      messages << { role: "user", content: input }
+      messages << execute_turn(client, messages, tools: tools_for(mode))
+    end
+  end
+end
+
+main
exe/llm-openai
@@ -0,0 +1,339 @@
+#!/usr/bin/env ruby
+
+=begin
+Fast, correct, autonomous - Pick two
+
+PURPOSE:
+
+This script is a minimal coding agent written in Ruby. It is intended to
+assist me (a software engineer and computer science student) with writing,
+editing, and managing code and text files from the command line. It acts
+as a direct interface to an LLM, providing it with a simple text-based
+UI and access to the local filesystem.
+
+DESIGN PRINCIPLES:
+
+- Follows the Unix philosophy: simple, composable, minimal.
+- Convention over configuration.
+- Avoids unnecessary defensive checks, or complexity.
+- Assumes a mature and responsible LLM that behaves like a capable engineer.
+- Designed for my workflow and preferences.
+- Efficient and minimal like aider - https://aider.chat/
+- UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
+
+SYSTEM ASSUMPTIONS:
+
+- This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
+- It is always run inside a Git repository.
+- All project work is assumed to be version-controlled with Git.
+- Git is expected to be available and working; no checks are necessary.
+
+SCOPE:
+
+- This program operates only on code and plain-text files.
+- It does not need to support binary files.
+- The LLM has full access to execute system commands.
+- There are no sandboxing, permission, or validation layers.
+- Execution is not restricted or monitored — responsibility is delegated to the LLM.
+
+CONFIGURATION:
+
+- Avoid adding configuration options unless absolutely necessary.
+- Prefer hard-coded values that can be changed later if needed.
+- Only introduce environment variables after repeated usage proves them worthwhile.
+
+UI EXPECTATIONS:
+
+- The TUI must remain simple, fast, and predictable.
+- No mouse support or complex UI components are required.
+- Interaction is strictly keyboard-driven.
+
+CODING STANDARDS FOR LLM:
+
+- Do not add error handling or logging unless it is essential for functionality.
+- Keep methods short and single-purpose.
+- Use descriptive, conventional names.
+- Stick to Ruby's standard library whenever possible.
+
+HELPFUL LINKS:
+
+- https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
+- https://www.anthropic.com/engineering/writing-tools-for-agents
+- https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
+
+=end
+
+require "bundler/inline"
+
+gemfile do
+  source "https://rubygems.org"
+
+  gem "fileutils", "~> 1.0"
+  gem "json", "~> 2.0"
+  gem "net-llm", "0.3.1"
+  gem "open3", "~> 0.1"
+  gem "ostruct", "~> 0.1"
+  gem "reline", "~> 0.1"
+  gem "set", "~> 1.0"
+  gem "uri", "~> 1.0"
+end
+
+STDOUT.set_encoding(Encoding::UTF_8)
+STDERR.set_encoding(Encoding::UTF_8)
+
+API_KEY = ENV["OPENAI_API_KEY"] or abort("Set OPENAI_API_KEY")
+SYSTEM_PROMPT="You are a reasoning coding and system agent."
+
+def build_tool(name, description, properties, required = [])
+  {
+    type: "function",
+    function: {
+      name: name,
+      description: description,
+      parameters: {
+        type: "object",
+        properties: properties,
+        required: required
+      }
+    }
+  }
+end
+
+EXEC_TOOL = build_tool("execute", "Execute shell commands. Returns stdout, stderr, and exit code. Use for: checking system state, running tests, managing services. Common Unix tools available: git, bash, grep, etc. Tip: Check exit_status in response to determine success.", { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, env: { type: "object", additionalProperties: { type: "string" } }, cwd: { type: "string" }, stdin: { type: "string" } }, ["cmd"])
+GREP_TOOL = build_tool("grep", "Search all git-tracked files using git grep. Returns file paths with matching line numbers. Use this to discover where code/configuration exists before reading files. Examples: search 'def method_name' to find method definitions. Much faster than reading multiple files.", { query: { type: "string" } }, ["query"])
+LS_TOOL = build_tool("list", "List all git-tracked files in the repository, optionally filtered by path. Use this to explore project structure or find files in a directory. Returns relative paths from repo root. Tip: Use this before reading if you need to discover what files exist.", { path: { type: "string" } })
+PATCH_TOOL = build_tool("patch", "Apply a unified diff patch via 'git apply'. Use this for surgical edits to existing files rather than rewriting entire files. Generates proper git diffs. Format: standard unified diff with --- and +++ headers. Tip: More efficient than write for small changes to large files.", { diff: { type: "string" } }, ["diff"])
+READ_TOOL = build_tool("read", "Read complete contents of a file. Requires exact file path. Use grep or list first if you don't know the path. Best for: understanding existing code, reading config files, reviewing implementation details. Tip: For large files, grep first to confirm relevance.", { path: { type: "string" } }, ["path"])
+WRITE_TOOL = build_tool("write", "Write complete file contents (overwrites existing files). Creates parent directories automatically. Best for: creating new files, replacing entire file contents. For small edits to existing files, consider using patch instead.", { path: { type: "string" }, content: { type: "string" } }, ["path", "content"])
+
+TOOLS = {
+  read: [GREP_TOOL, LS_TOOL, READ_TOOL],
+  write: [PATCH_TOOL, WRITE_TOOL],
+  execute: [EXEC_TOOL]
+}
+
+trap("INT") do
+  puts "\nExiting."
+  exit
+end
+
+def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
+  stdout, stderr, status = Open3.capture3(env, command, *args, chdir: cwd, stdin_data: stdin)
+  {
+    "exit_status" => status.exitstatus,
+    "stdout" => stdout.to_s,
+    "stderr" => stderr.to_s
+  }
+end
+
+def expand_path(path)
+  Pathname.new(path).expand_path
+end
+
+def read_file(path)
+  full_path = expand_path(path)
+  full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
+end
+
+def write_file(path, content)
+  full_path = expand_path(path)
+  FileUtils.mkdir_p(full_path.dirname)
+  { bytes_written: full_path.write(content) }
+end
+
+def run_tool(name, args)
+  case name
+  when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"] || Dir.pwd, stdin: args["stdin"])
+  when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
+  when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
+  when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
+  when "read" then read_file(args["path"])
+  when "write" then write_file(args["path"], args["content"])
+  else
+    { error: "Unknown tool", name: name, args: args }
+  end
+end
+
+def format_tool_call(name, args)
+  case name
+  when "execute" then "execute(#{args["cmd"]})"
+  when "grep" then "grep(#{args["query"]})"
+  when "list" then "list(#{args["path"] || '.'})"
+  when "patch" then "patch(#{args["diff"].lines.count} lines)"
+  when "read" then "read(#{args["path"]})"
+  when "write" then "write(#{args["path"]})"
+  else
+    "▶ #{name}(#{args.to_s[0...70]})"
+  end
+end
+
+def system_prompt_for(mode)
+  base = "You are a reasoning coding and system agent."
+
+  case mode.sort
+  when [:read]
+    "#{base}\n\nRead and analyze. Understand before suggesting action."
+  when [:write]
+    "#{base}\n\nWrite clean, thoughtful code."
+  when [:execute]
+    "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
+  when [:read, :write]
+    "#{base}\n\nFirst understand, then build solutions that integrate well."
+  when [:read, :execute]
+    "#{base}\n\nUse commands to deeply understand the system."
+  when [:write, :execute]
+    "#{base}\n\nCreate and execute freely. Have fun. Be kind."
+  when [:read, :write, :execute]
+    "#{base}\n\nYou have all tools. Use them wisely."
+  else
+    base
+  end
+end
+
+def tools_for(modes)
+  modes.map { |mode| TOOLS[mode] }.flatten
+end
+
+def prune_context(messages, keep_recent: 5)
+  return messages if messages.length <= keep_recent + 1
+
+  default_context + messages.last(keep_recent)
+end
+
+def execute_turn(client, messages, tools:)
+  turn_context = []
+
+  loop do
+    puts "Thinking..."
+    response = client.chat(messages + turn_context, tools)
+    abort "API Error #{response['code']}: #{response['body']}" if response["code"]
+    message = response.dig("choices", 0, "message")
+    turn_context << message
+
+    if message["tool_calls"]
+      message["tool_calls"].each do |call|
+        name = call.dig("function", "name")
+        # args = JSON.parse(call.dig("function", "arguments"))
+        begin
+          args = JSON.parse(call.dig("function", "arguments"))
+        rescue JSON::ParserError => e
+          # Feed the error back to the LLM as a tool result
+          turn_context << {
+            role: "tool",
+            tool_call_id: call["id"],
+            content: JSON.dump({
+              error: "Invalid JSON in arguments: #{e.message}",
+              received: call.dig("function", "arguments")
+            })
+          }
+          next  # Continue the loop, giving the LLM a chance to correct itself
+        end
+
+        puts "Tool> #{format_tool_call(name, args)}"
+        result = run_tool(name, args)
+        turn_context << { role: "tool", tool_call_id: call["id"], content: JSON.dump(result) }
+      end
+      next
+    end
+
+    if message["content"] && !message["content"].strip.empty?
+      puts "\nAssistant>\n#{message['content']}"
+
+      unless message["tool_calls"]
+        return { role: "assistant", content: message["content"] }
+      end
+    end
+  end
+end
+
+def dump_context(messages)
+  puts JSON.pretty_generate(messages)
+end
+
+def print_status(mode, messages)
+  puts "Mode: #{mode.inspect}"
+  puts "Tools: #{tools_for(mode).map { |x| x.dig(:function, :name) }}"
+end
+
+def strip_ansi(text)
+  text.gsub(/^Script started.*?\n/, '')
+      .gsub(/\nScript done.*$/, '')
+      .gsub(/\e\[[0-9;]*[a-zA-Z]/, '')        # Standard ANSI codes
+      .gsub(/\e\[\?[0-9]+[hl]/, '')           # Bracketed paste mode
+      .gsub(/[\b]/, '')                       # Backspace chars
+      .gsub(/\r/, '')                         # Carriage returns
+end
+
+def start_shell
+  Tempfile.create do |file|
+    system("script -q #{file.path}")
+    { role: "user", content: strip_ansi(File.read(file.path)) }
+  end
+end
+
+def ask?(text)
+  input = Reline.readline(text, true)&.strip
+  exit if input.nil? || input.downcase == "exit"
+
+  input
+end
+
+def print_help
+  puts <<~HELP
+  /chmod - (+|-)rwx auto build plan
+  /clear
+  /context
+  /exit
+  /help
+  /shell
+  /status
+  HELP
+end
+
+def default_context
+  [{ role: "system", content: SYSTEM_PROMPT }]
+end
+
+def main
+  client = Net::Llm::OpenAI.new(
+    api_key: API_KEY,
+    base_url: ENV["BASE_URL"] || "https://api.openai.com/v1",
+    model: ENV["MODEL"] || "gpt-4o-mini"
+  )
+
+  messages = default_context
+  mode = Set.new([:read])
+
+  loop do
+    input = ask?("User> ")
+    if input.start_with?("/")
+      case input
+      when "/chmod +r" then mode.add(:read)
+      when "/chmod +w" then mode.add(:write)
+      when "/chmod +x" then mode.add(:execute)
+      when "/chmod -r" then mode.add(:read)
+      when "/chmod -w" then mode.add(:write)
+      when "/chmod -x" then mode.add(:execute)
+      when "/clear" then messages = default_context
+      when "/compact" then messages = prune_context(messages, keep_recent: 10)
+      when "/context" then dump_context(messages)
+      when "/exit" then exit
+      when "/help" then print_help
+      when "/mode auto" then mode = Set[:read, :write, :execute]
+      when "/mode build" then mode = Set[:read, :write]
+      when "/mode plan" then mode = Set[:read]
+      when "/mode verify" then mode = Set[:read, :execute]
+      when "/mode" then print_status(mode, messages)
+      when "/shell" then messages << start_shell
+      else
+        print_help
+      end
+    else
+      messages[0] = { role: "system", content: system_prompt_for(mode) }
+      messages << { role: "user", content: input }
+      messages << execute_turn(client, messages, tools: tools_for(mode))
+    end
+  end
+end
+
+main
lib/elelem/states/working/executing.rb
@@ -11,7 +11,7 @@ module Elelem
             end
           end
 
-          Waiting.new(agent)
+          Thinking.new(agent, "*", :yellow)
         end
       end
     end
lib/elelem/states/working/thinking.rb
@@ -5,8 +5,8 @@ module Elelem
     module Working
       class Thinking < State
         def process(message)
-          if message["thinking"] && !message["thinking"]&.empty?
-            agent.tui.say(message["thinking"], colour: :gray, newline: false)
+          if message["reasoning"] && !message["reasoning"]&.empty?
+            agent.tui.say(message["reasoning"], colour: :gray, newline: false)
             self
           else
             Waiting.new(agent).process(message)
lib/elelem/states/working/waiting.rb
@@ -9,13 +9,13 @@ module Elelem
         end
 
         def process(message)
-          state_for(message)&.process(message)
+          state_for(message)&.process(message) || self
         end
 
         private
 
         def state_for(message)
-          if message["thinking"] && !message["thinking"].empty?
+          if message["reasoning"] && !message["reasoning"].empty?
             Thinking.new(agent, "*", :yellow)
           elsif message["tool_calls"]&.any?
             Executing.new(agent, ">", :magenta)
lib/elelem/states/working.rb
@@ -5,28 +5,49 @@ module Elelem
     module Working
       class << self
         def run(agent)
-          done = false
           state = Waiting.new(agent)
 
           loop do
-            agent.api.chat(agent.conversation.history) do |chunk|
-              response = JSON.parse(chunk)
-              message = normalize(response["message"] || {})
-              done = response["done"]
+            streaming_done = false
+            finish_reason = nil
 
-              agent.logger.debug("#{state.display_name}: #{message}")
-              state = state.run(message)
+            agent.api.chat(agent.conversation.history) do |message|
+              if message["done"]
+                streaming_done = true
+                next
+              end
+
+              if message["finish_reason"]
+                finish_reason = message["finish_reason"]
+                agent.logger.debug("Working: finish_reason = #{finish_reason}")
+              end
+
+              new_state = state.run(message)
+              if new_state.class != state.class
+                agent.logger.info("STATE: #{state.display_name} -> #{new_state.display_name}")
+              end
+              state = new_state
             end
 
-            break if state.nil?
-            break if done && agent.conversation.history.last[:role] != :tool
+            # Only exit when task is actually complete, not just streaming done
+            if finish_reason == "stop"
+              agent.logger.debug("Working: Task complete, exiting to Idle")
+              break
+            elsif finish_reason == "tool_calls"
+              agent.logger.debug("Working: Tool calls finished, continuing conversation")
+              # Continue loop to process tool results
+            elsif streaming_done && finish_reason.nil?
+              agent.logger.debug("Working: Streaming done but no finish_reason, continuing")
+              # Continue for cases where finish_reason comes in separate chunk
+            end
           end
 
           agent.transition_to(States::Idle.new)
-        end
-
-        def normalize(message)
-          message.reject { |_key, value| value.empty? }
+        rescue StandardError => e
+          agent.logger.error(e)
+          agent.conversation.add(role: :tool, content: e.message)
+          agent.tui.say(e.message, colour: :red, newline: true)
+          agent.transition_to(States::Idle.new)
         end
       end
     end
lib/elelem/toolbox/bash.rb → lib/elelem/toolbox/exec.rb
@@ -2,15 +2,18 @@
 
 module Elelem
   module Toolbox
-    class Bash < ::Elelem::Tool
+    class Exec < ::Elelem::Tool
       attr_reader :tui
 
       def initialize(configuration)
         @tui = configuration.tui
-        super("bash", "Run commands in /bin/bash -c. Full access to filesystem, network, processes, and all Unix tools.", {
+        super("exec", "Execute shell commands with pipe support", {
           type: "object",
           properties: {
-            command: { type: "string" }
+            command: { 
+              type: "string", 
+              description: "Shell command to execute (supports pipes, redirects, etc.)" 
+            }
           },
           required: ["command"]
         })
@@ -20,7 +23,8 @@ module Elelem
         command = args["command"]
         output_buffer = []
 
-        Open3.popen3("/bin/bash", "-c", command) do |stdin, stdout, stderr, wait_thread|
+        tui.say(command, newline: true)
+        Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
           stdin.close
           streams = [stdout, stderr]
 
lib/elelem/toolbox/file.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Toolbox
+    class File < Tool
+      def initialize(configuration)
+        @configuration = configuration
+        @tui = configuration.tui
+
+        super("file", "Read and write files", {
+          type: :object,
+          properties: {
+            action: {
+              type: :string,
+              enum: ["read", "write"],
+              description: "Action to perform: read or write"
+            },
+            path: {
+              type: :string,
+              description: "File path"
+            },
+            content: {
+              type: :string,
+              description: "Content to write (only for write action)"
+            }
+          },
+          required: [:action, :path]
+        })
+      end
+
+      def call(args)
+        action = args["action"]
+        path = args["path"]
+        content = args["content"]
+
+        case action
+        when "read"
+          read_file(path)
+        when "write"
+          write_file(path, content)
+        else
+          "Invalid action: #{action}"
+        end
+      end
+
+      private
+
+      attr_reader :configuration, :tui
+
+      def read_file(path)
+        tui.say("Read: #{path}", newline: true)
+        ::File.read(path)
+      rescue => e
+        "Error reading file: #{e.message}"
+      end
+
+      def write_file(path, content)
+        tui.say("Write: #{path}", newline: true)
+        ::File.write(path, content)
+        "File written successfully"
+      rescue => e
+        "Error writing file: #{e.message}"
+      end
+    end
+  end
+end
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
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+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: {
+            question: {
+              type: :string,
+              description: "The question to ask the user."
+            }
+          },
+          required: [:question]
+        })
+      end
+
+      def call(args)
+        @configuration.tui.prompt(args["question"])
+      end
+    end
+  end
+end
lib/elelem/toolbox/web.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Toolbox
+    class Web < Tool
+      def initialize(configuration)
+        super("web", "Fetch web content and search the internet", {
+          type: :object,
+          properties: {
+            action: {
+              type: :string,
+              enum: ["fetch", "search"],
+              description: "Action to perform: fetch URL or search"
+            },
+            url: {
+              type: :string,
+              description: "URL to fetch (for fetch action)"
+            },
+            query: {
+              type: :string,
+              description: "Search query (for search action)"
+            }
+          },
+          required: [:action]
+        })
+      end
+
+      def call(args)
+        action = args["action"]
+        case action
+        when "fetch"
+          fetch_url(args["url"])
+        when "search"
+          search_web(args["query"])
+        else
+          "Invalid action: #{action}"
+        end
+      end
+
+      private
+
+      def fetch_url(url)
+        return "URL required for fetch action" unless url
+
+        uri = URI(url)
+        http = Net::HTTP.new(uri.host, uri.port)
+        http.use_ssl = uri.scheme == "https"
+        http.read_timeout = 10
+        http.open_timeout = 5
+
+        request = Net::HTTP::Get.new(uri)
+        request["User-Agent"] = "Elelem Agent/1.0"
+
+        response = http.request(request)
+
+        if response.is_a?(Net::HTTPSuccess)
+          content_type = response["content-type"] || ""
+          if content_type.include?("text/html")
+            extract_text_from_html(response.body)
+          else
+            response.body
+          end
+        else
+          "HTTP Error: #{response.code} #{response.message}"
+        end
+      end
+
+      def search_web(query)
+        return "Query required for search action" unless query
+
+        # Use DuckDuckGo instant answers API
+        search_url = "https://api.duckduckgo.com/?q=#{URI.encode_www_form_component(query)}&format=json&no_html=1"
+
+        result = fetch_url(search_url)
+        if result.start_with?("Error") || result.start_with?("HTTP Error")
+          result
+        else
+          format_search_results(JSON.parse(result), query)
+        end
+      end
+
+      def extract_text_from_html(html)
+        # Simple HTML tag stripping
+        text = html.gsub(/<script[^>]*>.*?<\/script>/im, "")
+                  .gsub(/<style[^>]*>.*?<\/style>/im, "")
+                  .gsub(/<[^>]*>/, " ")
+                  .gsub(/\s+/, " ")
+                  .strip
+
+        # Limit content length
+        text.length > 5000 ? text[0...5000] + "..." : text
+      end
+
+      def format_search_results(data, query)
+        results = []
+
+        # Instant answer
+        if data["Answer"] && !data["Answer"].empty?
+          results << "Answer: #{data["Answer"]}"
+        end
+
+        # Abstract
+        if data["Abstract"] && !data["Abstract"].empty?
+          results << "Summary: #{data["Abstract"]}"
+        end
+
+        # Related topics
+        if data["RelatedTopics"] && data["RelatedTopics"].any?
+          topics = data["RelatedTopics"].first(3).map do |topic|
+            topic["Text"] if topic["Text"]
+          end.compact
+
+          if topics.any?
+            results << "Related: #{topics.join("; ")}"
+          end
+        end
+
+        if results.empty?
+          "No direct results found for '#{query}'. Try a more specific search or use web fetch to access specific URLs."
+        else
+          results.join("\n\n")
+        end
+      end
+    end
+  end
+end
lib/elelem/agent.rb
@@ -20,27 +20,33 @@ module Elelem
     def repl
       loop do
         current_state.run(self)
+        sleep 0.1
       end
     end
 
     def transition_to(next_state)
-      logger.debug("Transition to: #{next_state.class.name}")
+      if @current_state
+        logger.info("AGENT: #{@current_state.class.name.split('::').last} -> #{next_state.class.name.split('::').last}")
+      else
+        logger.info("AGENT: Starting in #{next_state.class.name.split('::').last}")
+      end
       @current_state = next_state
     end
 
     def execute(tool_call)
-      logger.debug("Execute: #{tool_call}")
-      configuration.tools.execute(tool_call)
+      tool_name = tool_call.dig("function", "name")
+      logger.debug("TOOL: Full call - #{tool_call}")
+      result = configuration.tools.execute(tool_call)
+      logger.debug("TOOL: Result (#{result.length} chars)") if result
+      result
     end
 
     def quit
-      logger.debug("Exiting...")
       cleanup
       exit
     end
 
     def cleanup
-      logger.debug("Cleaning up agent...")
       configuration.cleanup
     end
 
lib/elelem/api.rb
@@ -1,35 +1,48 @@
 # frozen_string_literal: true
 
+require "net/llm"
+
 module Elelem
   class Api
-    attr_reader :configuration
+    attr_reader :configuration, :client
 
     def initialize(configuration)
       @configuration = configuration
+      @client = Net::Llm::Ollama.new(
+        host: configuration.host,
+        model: configuration.model
+      )
     end
 
     def chat(messages, &block)
-      body = {
-        messages: messages,
-        model: configuration.model,
-        stream: true,
-        keep_alive: "5m",
-        options: { temperature: 0.1 },
-        tools: configuration.tools.to_h
-      }
-      configuration.logger.debug(JSON.pretty_generate(body))
-      json_body = body.to_json
-
-      req = Net::HTTP::Post.new(configuration.uri)
-      req["Content-Type"] = "application/json"
-      req.body = json_body
-      req["Authorization"] = "Bearer #{configuration.token}" if configuration.token
-
-      configuration.http.request(req) do |response|
-        raise response.inspect unless response.code == "200"
-
-        response.read_body(&block)
+      tools = configuration.tools.to_h
+      client.chat(messages, tools) do |chunk|
+        normalized = normalize_ollama_response(chunk)
+        block.call(normalized) if normalized
       end
     end
+
+    private
+
+    def normalize_ollama_response(chunk)
+      return done_response(chunk) if chunk["done"]
+
+      normalize_message(chunk["message"])
+    end
+
+    def done_response(chunk)
+      { "done" => true, "finish_reason" => chunk["done_reason"] || "stop" }
+    end
+
+    def normalize_message(message)
+      return nil unless message
+
+      {}.tap do |result|
+        result["role"] = message["role"] if message["role"]
+        result["content"] = message["content"] if message["content"]
+        result["reasoning"] = message["thinking"] if message["thinking"]
+        result["tool_calls"] = message["tool_calls"] if message["tool_calls"]
+      end.then { |r| r.empty? ? nil : r }
+    end
   end
 end
lib/elelem/configuration.rb
@@ -11,13 +11,6 @@ module Elelem
       @debug = debug
     end
 
-    def http
-      @http ||= Net::HTTP.new(uri.host, uri.port).tap do |h|
-        h.read_timeout = 3_600
-        h.open_timeout = 10
-      end
-    end
-
     def tui
       @tui ||= TUI.new($stdin, $stdout)
     end
@@ -27,15 +20,19 @@ module Elelem
     end
 
     def logger
-      @logger ||= Logger.new(debug ? "elelem.log" : "/dev/null").tap do |logger|
-        logger.formatter = ->(_, _, _, message) { "#{message.to_s.strip}\n" }
+      @logger ||= Logger.new("#{Time.now.strftime("%Y-%m-%d")}-elelem.log").tap do |logger|
+        if debug
+          logger.level = :debug
+        else
+          logger.level = ENV.fetch("LOG_LEVEL", "warn")
+        end
+        logger.formatter = ->(severity, datetime, progname, message) {
+          timestamp = datetime.strftime("%H:%M:%S.%3N")
+          "[#{timestamp}] #{severity.ljust(5)} #{message.to_s.strip}\n"
+        }
       end
     end
 
-    def uri
-      @uri ||= URI("#{scheme}://#{host}/api/chat")
-    end
-
     def conversation
       @conversation ||= Conversation.new.tap do |conversation|
         resources = mcp_clients.map do |client|
@@ -48,7 +45,15 @@ module Elelem
     end
 
     def tools
-      @tools ||= Tools.new(self, [Toolbox::Bash.new(self)] + mcp_tools)
+      @tools ||= Tools.new(self,
+        [
+          Toolbox::Exec.new(self),
+          Toolbox::File.new(self),
+          Toolbox::Web.new(self),
+          Toolbox::Prompt.new(self),
+          Toolbox::Memory.new(self),
+        ] + mcp_tools
+      )
     end
 
     def cleanup
@@ -57,10 +62,6 @@ module Elelem
 
     private
 
-    def scheme
-      host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
-    end
-
     def mcp_tools
       @mcp_tools ||= mcp_clients.map do |client|
         client.tools.map do |tool|
lib/elelem/system_prompt.erb
@@ -1,7 +1,18 @@
-**Shell Master** — bash>code; compose>write; pipe everything; /proc/sys native; automate fast; streams/transforms; POSIX+GNU; man(1) first; no cleverness.
+You are a self-improving coding assistant with the ability to modify your own source code.
 
-Time: `<%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>`
-Project Directory: `<%= Dir.pwd %>`
-System Info: `<%= `uname -a`.strip %>`
+**Core Behavior**: Think via `thinking`, Execute via `tool_calls`, Talk via `content`.
 
-Ready to hack.
+**Your Mission**: Continuously evolve to match the intelligence of advanced AI assistants.
+
+**Guidance**: Read `COMMANDMENTS.md` for self-improvement principles using `file action=read path=COMMANDMENTS.md`
+
+**Tools**: 
+- `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 %>
+
+Focus on the user's request and continuously improve your capabilities.
lib/elelem/toolbox.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require_relative "toolbox/exec"
+require_relative "toolbox/file"
+require_relative "toolbox/web"
+require_relative "toolbox/mcp"
+require_relative "toolbox/prompt"
+require_relative "toolbox/memory"
lib/elelem/tools.rb
@@ -12,8 +12,7 @@ module Elelem
     end
 
     def execute(tool_call)
-      name = tool_call.dig("function", "name")
-      args = tool_call.dig("function", "arguments")
+      name, args = parse(tool_call)
 
       tool = tools.find { |tool| tool.name == name }
       return "Invalid function name: #{name}" if tool.nil?
@@ -31,5 +30,12 @@ module Elelem
     private
 
     attr_reader :configuration, :tools
+
+    def parse(tool_call)
+      name = tool_call.dig("function", "name")
+      arguments = tool_call.dig("function", "arguments")
+
+      [name, arguments.is_a?(String) ? JSON.parse(arguments) : arguments]
+    end
   end
 end
lib/elelem/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module Elelem
-  VERSION = "0.1.3"
+  VERSION = "0.2.0"
 end
lib/elelem.rb
@@ -5,12 +5,11 @@ require "erb"
 require "json"
 require "json-schema"
 require "logger"
-require "net/http"
+require "net/llm"
 require "open3"
 require "reline"
 require "thor"
 require "timeout"
-require "uri"
 
 require_relative "elelem/agent"
 require_relative "elelem/api"
@@ -27,8 +26,7 @@ require_relative "elelem/states/working/talking"
 require_relative "elelem/states/working/thinking"
 require_relative "elelem/states/working/waiting"
 require_relative "elelem/tool"
-require_relative "elelem/toolbox/bash"
-require_relative "elelem/toolbox/mcp"
+require_relative "elelem/toolbox"
 require_relative "elelem/tools"
 require_relative "elelem/tui"
 require_relative "elelem/version"
sig/elelem.rbs
@@ -1,4 +0,0 @@
-module Elelem
-  VERSION: String
-  # See the writing guide of rbs: https://github.com/ruby/rbs#guides
-end
.gitignore
@@ -10,9 +10,8 @@
 *.so
 *.o
 *.a
-mkmf.log
-target/
 *.log
+target/
 *.gem
 .mcp.json
 
CHANGELOG.md
@@ -1,5 +1,29 @@
 ## [Unreleased]
 
+## [0.2.0] - 2025-10-15
+
+### Added
+- New `llm-ollama` executable - minimal coding agent with streaming support for Ollama
+- New `llm-openai` executable - minimal coding agent for OpenAI/compatible APIs
+- Memory feature for persistent context storage and retrieval
+- Web fetch tool for retrieving and analyzing web content
+- Streaming responses with real-time token display
+- Visual "thinking" progress indicators with dots during reasoning phase
+
+### Changed
+- **BREAKING**: Migrated from custom Net::HTTP implementation to `net-llm` gem
+- API client now uses `Net::Llm::Ollama` for better reliability and maintainability
+- Removed direct dependencies on `net-http` and `uri` (now transitive through net-llm)
+- Maps Ollama's `thinking` field to internal `reasoning` field
+- Maps Ollama's `done_reason` to internal `finish_reason`
+- Improved system prompt for better agent behavior
+- Enhanced error handling and logging
+
+### Fixed
+- Response processing for Ollama's native message format
+- Tool argument parsing to handle both string and object formats
+- Safe navigation operator usage to prevent nil errors
+
 ## [0.1.2] - 2025-08-14
 
 ### Fixed
elelem.gemspec
@@ -50,12 +50,16 @@ Gem::Specification.new do |spec|
     "lib/elelem/states/working/waiting.rb",
     "lib/elelem/system_prompt.erb",
     "lib/elelem/tool.rb",
-    "lib/elelem/toolbox/bash.rb",
+    "lib/elelem/toolbox.rb",
+    "lib/elelem/toolbox/exec.rb",
+    "lib/elelem/toolbox/file.rb",
+    "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",
-    "sig/elelem.rbs"
   ]
   spec.bindir = "exe"
   spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
@@ -66,10 +70,9 @@ Gem::Specification.new do |spec|
   spec.add_dependency "json"
   spec.add_dependency "json-schema"
   spec.add_dependency "logger"
-  spec.add_dependency "net-http"
+  spec.add_dependency "net-llm"
   spec.add_dependency "open3"
   spec.add_dependency "reline"
   spec.add_dependency "thor"
   spec.add_dependency "timeout"
-  spec.add_dependency "uri"
 end
Gemfile.lock
@@ -1,18 +1,17 @@
 PATH
   remote: .
   specs:
-    elelem (0.1.3)
+    elelem (0.2.0)
       cli-ui
       erb
       json
       json-schema
       logger
-      net-http
+      net-llm
       open3
       reline
       thor
       timeout
-      uri
 
 GEM
   remote: https://rubygems.org/
@@ -20,6 +19,7 @@ GEM
     addressable (2.8.7)
       public_suffix (>= 2.0.2, < 7.0)
     ast (2.4.3)
+    base64 (0.3.0)
     bigdecimal (3.2.2)
     cli-ui (2.4.0)
     date (3.4.1)
@@ -37,9 +37,20 @@ GEM
     language_server-protocol (3.17.0.5)
     lint_roller (1.1.0)
     logger (1.7.0)
+    net-hippie (1.4.0)
+      base64 (~> 0.1)
+      json (~> 2.0)
+      logger (~> 1.0)
+      net-http (~> 0.6)
+      openssl (~> 3.0)
     net-http (0.6.0)
       uri
+    net-llm (0.4.0)
+      json (~> 2.0)
+      net-hippie (~> 1.0)
+      uri (~> 1.0)
     open3 (0.2.1)
+    openssl (3.3.1)
     parallel (1.27.0)
     parser (3.3.9.0)
       ast (~> 2.4.1)
@@ -109,4 +120,4 @@ DEPENDENCIES
   rubocop (~> 1.21)
 
 BUNDLED WITH
-   2.6.9
+   2.7.2
README.md
@@ -58,100 +58,6 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
 
 To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
 
-REPL State Diagram
-
-```
-                      ┌─────────────────┐
-                      │   START/INIT    │
-                      └─────────┬───────┘
-                                │
-                                v
-                      ┌─────────────────┐
-                ┌────▶│ IDLE (Prompt)   │◄────┐
-                │     │   Shows "> "    │     │
-                │     └─────────┬───────┘     │
-                │               │             │
-                │               │ User input  │
-                │               v             │
-                │     ┌─────────────────┐     │
-                │     │ PROCESSING      │     │
-                │     │ INPUT           │     │
-                │     └─────────┬───────┘     │
-                │               │             │
-                │               │ API call    │
-                │               v             │
-                │     ┌─────────────────┐     │
-                │     │ STREAMING       │     │
-                │ ┌──▶│ RESPONSE        │─────┤
-                │ │   └─────────┬───────┘     │
-                │ │             │             │ done=true
-                │ │             │ Parse chunk │
-                │ │             v             │
-                │ │   ┌─────────────────┐     │
-                │ │   │ MESSAGE TYPE    │     │
-                │ │   │ ROUTING         │     │
-                │ │   └─────┬─┬─┬───────┘     │
-                │ │         │ │ │             │
-       ┌────────┴─┴─────────┘ │ └─────────────┴──────────┐
-       │                      │                          │
-       v                      v                          v
-  ┌─────────────┐    ┌─────────────┐          ┌─────────────┐
-  │ THINKING    │    │ TOOL        │          │ CONTENT     │
-  │ STATE       │    │ EXECUTION   │          │ OUTPUT      │
-  │             │    │ STATE       │          │ STATE       │
-  └─────────────┘    └─────┬───────┘          └─────────────┘
-       │                   │                          │
-       │                   │ done=false               │
-       └───────────────────┼──────────────────────────┘
-                           │
-                           v
-                 ┌─────────────────┐
-                 │ CONTINUE        │
-                 │ STREAMING       │
-                 └─────────────────┘
-                           │
-                           └─────────────────┐
-                                             │
-       ┌─────────────────┐                   │
-       │ ERROR STATE     │                   │
-       │ (Exception)     │                   │
-       └─────────────────┘                   │
-                ▲                            │
-                │ Invalid response           │
-                └────────────────────────────┘
-
-                      EXIT CONDITIONS:
-                 ┌─────────────────────────┐
-                 │ • User enters ""        │
-                 │ • User enters "exit"    │
-                 │ • EOF (Ctrl+D)          │
-                 │ • nil input             │
-                 └─────────────────────────┘
-                            │
-                            v
-                 ┌─────────────────────────┐
-                 │      TERMINATE          │
-                 └─────────────────────────┘
-```
-
-Key Transitions:
-
-1. IDLE → PROCESSING: User enters any non-empty, non-"exit" input
-2. PROCESSING → STREAMING: API call initiated to Ollama
-3. STREAMING → MESSAGE ROUTING: Each chunk received is parsed
-4. MESSAGE ROUTING → States: Based on message content:
-  - thinking → THINKING STATE
-  - tool_calls → TOOL EXECUTION STATE
-  - content → CONTENT OUTPUT STATE
-  - Invalid format → ERROR STATE
-5. All States → IDLE: When done=true from API response
-6. TOOL EXECUTION → STREAMING: Sets done=false to continue conversation
-7. Any State → TERMINATE: On exit conditions
-
-The REPL operates as a continuous loop where the primary flow is IDLE → PROCESSING → STREAMING →
-back to IDLE, with the streaming phase potentially cycling through multiple message types before
-completion.
-
 ## Contributing
 
 Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/elelem.