Commit 326dc9d
Changed files (1)
exe
exe/elelem-ollama
@@ -1,359 +0,0 @@
-#!/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
- /mode auto build plan verify
- /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