elelem 0.3.0 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcd68905e2c0dd4aa7e6c22865d0353ab2fa0dff0ce8bfde3e319b10b7d4e548
4
- data.tar.gz: ed59f2ca17e210adbd7b371556d7994589e75e7a515d688bb6751d8bbd3380c5
3
+ metadata.gz: 2885faeaffa4f0ee7eff742f50bbf1ee78f257f7989c6401c787c4d4feb5d6a2
4
+ data.tar.gz: '0965d3a94ce8633cd01e7471e5688912216794e9b6af49450d0c1a5326e24645'
5
5
  SHA512:
6
- metadata.gz: 400ae6e321309f3bcb6503942a2a636f8c782c086b17afd9a7be048df98215b248d0bbf4858b35de578ac13d19ad15dd19b2edef4e774ca99ec8af2901fb4006
7
- data.tar.gz: 5f138cea24ee1faffdb173c2bed070bead948a7f9dbf17fdea6b44486998aca5517de3f3e7a99f7807fe8c5e969ab6783f09673f7158382742bb1056406d1cd7
6
+ metadata.gz: e00079252cb138588937776e7d37fc28de2e451c1ac72a190f842d29d27df537123a524b3358c6f36a685704b18e5c3a424cd819b3c8eb3ed1259f1937bdf02d
7
+ data.tar.gz: c77a7a8b34cf326e812db4ac23a38de0f7841fc1763f18f569a60d783be6719e251ff1eae59234b4da68cb8276c71fb617491ea46d97905ec39e05f5359b405f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-11-10
4
+
5
+ ### Added
6
+ - **Eval Tool**: Meta-programming tool that allows the LLM to dynamically create and register new tools at runtime
7
+ - Eval tool has access to the toolbox for enhanced capabilities
8
+ - Comprehensive test coverage with RSpec
9
+ - Agent specs
10
+ - Conversation specs
11
+ - Toolbox specs
12
+
13
+ ### Changed
14
+ - **Architecture Improvements**: Significant refactoring for better separation of concerns
15
+ - Extracted Tool class to separate file (`lib/elelem/tool.rb`)
16
+ - Extracted Toolbox class to separate file (`lib/elelem/toolbox.rb`)
17
+ - Extracted Shell class for command execution
18
+ - Improved tool registration through `#add_tool` method
19
+ - Tool constants moved to Toolbox for better organization
20
+ - Agent class simplified by delegating to Tool instances
21
+
22
+ ### Fixed
23
+ - `/context` command now correctly accounts for the current mode
24
+
3
25
  ## [0.3.0] - 2025-11-05
4
26
 
5
27
  ### Added
data/README.md CHANGED
@@ -1,74 +1,61 @@
1
1
  # Elelem
2
2
 
3
- Fast, correct, autonomous - Pick two
3
+ Fast, correct, autonomous pick two.
4
4
 
5
- PURPOSE:
5
+ ## Purpose
6
6
 
7
- Elelem is a minimal coding agent written in Ruby. It is intended to
8
- assist me (a software engineer and computer science student) with writing,
9
- editing, and managing code and text files from the command line. It acts
10
- as a direct interface to an LLM, providing it with a simple text-based
11
- UI and access to the local filesystem.
7
+ Elelem is a minimal coding agent written in Ruby. It is designed to help
8
+ you write, edit, and manage code and plain-text files from the command line
9
+ by delegating work to an LLM. The agent exposes a simple text-based UI and a
10
+ set of built-in tools that give the LLM access to the local file system
11
+ and Git.
12
12
 
13
- DESIGN PRINCIPLES:
13
+ ## Design Principles
14
14
 
15
- - Follows the Unix philosophy: simple, composable, minimal.
16
- - Convention over configuration.
17
- - Avoids unnecessary defensive checks, or complexity.
18
- - Assumes a mature and responsible LLM that behaves like a capable engineer.
19
- - Designed for my workflow and preferences.
20
- - Efficient and minimal like aider - https://aider.chat/
21
- - UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
15
+ * Unix philosophy simple, composable, minimal.
16
+ * Convention over configuration.
17
+ * No defensive checks or complexity beyond what is necessary.
18
+ * Assumes a mature, responsible LLM that behaves like a capable engineer.
19
+ * Optimised for my personal workflow and preferences.
20
+ * Efficient and minimal like *aider* https://aider.chat/.
21
+ * UX similar to Claude Code https://docs.claude.com/en/docs/claude-code/overview.
22
22
 
23
- SYSTEM ASSUMPTIONS:
23
+ ## System Assumptions
24
24
 
25
- - This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
26
- - It is always run inside a Git repository.
27
- - All project work is assumed to be version-controlled with Git.
28
- - Git is expected to be available and working; no checks are necessary.
25
+ * Linux host with Alacritty, tmux, Bash, Vim.
26
+ * Runs inside a Git repository.
27
+ * Git is available and functional.
29
28
 
30
- SCOPE:
29
+ ## Scope
31
30
 
32
- - This program operates only on code and plain-text files.
33
- - It does not need to support binary files.
34
- - The LLM has full access to execute system commands.
35
- - There are no sandboxing, permission, or validation layers.
36
- - Execution is not restricted or monitored - responsibility is delegated to the LLM.
31
+ Only plain-text and source-code files are supported. No binary handling,
32
+ sandboxing, or permission checks are performed - the LLM has full access.
37
33
 
38
- CONFIGURATION:
34
+ ## Configuration
39
35
 
40
- - Avoid adding configuration options unless absolutely necessary.
41
- - Prefer hard-coded values that can be changed later if needed.
42
- - Only introduce environment variables after repeated usage proves them worthwhile.
36
+ Prefer convention over configuration. Add environment variables only after
37
+ repeated use proves their usefulness.
43
38
 
44
- UI EXPECTATIONS:
39
+ ## UI Expectations
45
40
 
46
- - The TUI must remain simple, fast, and predictable.
47
- - No mouse support or complex UI components are required.
48
- - Interaction is strictly keyboard-driven.
41
+ Keyboard-driven, minimal TUI. No mouse support or complex widgets.
49
42
 
50
- CODING STANDARDS FOR LLM:
43
+ ## Coding Standards for the LLM
51
44
 
52
- - Do not add error handling or logging unless it is essential for functionality.
53
- - Keep methods short and single-purpose.
54
- - Use descriptive, conventional names.
55
- - Stick to Ruby's standard library whenever possible.
45
+ * No extra error handling unless essential.
46
+ * Keep methods short, single-purpose.
47
+ * Descriptive, conventional names.
48
+ * Use Ruby standard library where possible.
56
49
 
57
- HELPFUL LINKS:
50
+ ## Helpful Links
58
51
 
59
- - https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
60
- - https://www.anthropic.com/engineering/writing-tools-for-agents
61
- - https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
52
+ * https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
53
+ * https://www.anthropic.com/engineering/writing-tools-for-agents
54
+ * https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
62
55
 
63
56
  ## Installation
64
57
 
65
- Install the gem and add to the application's Gemfile by executing:
66
-
67
- ```bash
68
- bundle add elelem
69
- ```
70
-
71
- If bundler is not being used to manage dependencies, install the gem by executing:
58
+ Install the gem directly:
72
59
 
73
60
  ```bash
74
61
  gem install elelem
@@ -84,47 +71,86 @@ elelem chat
84
71
 
85
72
  ### Options
86
73
 
87
- - `--host`: Specify Ollama host (default: localhost:11434)
88
- - `--model`: Specify Ollama model (default: gpt-oss, currently only tested with gpt-oss)
89
- - `--token`: Provide authentication token
74
+ * `--host` Ollama host (default: `localhost:11434`).
75
+ * `--model` Ollama model (default: `gpt-oss`).
76
+ * `--token` Authentication token.
90
77
 
91
78
  ### Examples
92
79
 
93
80
  ```bash
94
- # Chat with default model
81
+ # Default model
95
82
  elelem chat
96
83
 
97
- # Chat with specific model and host
84
+ # Specific model and host
98
85
  elelem chat --model llama2 --host remote-host:11434
99
86
  ```
100
87
 
101
- ### Features
88
+ ## Mode System
102
89
 
103
- - **Interactive REPL**: Clean command-line interface for chatting
104
- - **Mode System**: Control agent capabilities with workflow modes (plan, build, verify, auto)
105
- - **Tool Execution**: Execute shell commands, read/write files, search code
106
- - **Streaming Responses**: Real-time streaming of AI responses
107
- - **Conversation History**: Maintains context across the session
90
+ The agent exposes seven built‑in tools. You can switch which ones are
91
+ available by changing the *mode*:
108
92
 
109
- ### Mode System
93
+ | Mode | Enabled Tools |
94
+ |---------|------------------------------------------|
95
+ | plan | `grep`, `list`, `read` |
96
+ | build | `grep`, `list`, `read`, `patch`, `write` |
97
+ | verify | `grep`, `list`, `read`, `execute` |
98
+ | auto | All tools |
110
99
 
111
- Control what tools the agent can access:
100
+ Use the following commands inside the REPL:
112
101
 
113
- ```bash
114
- /mode plan # Read-only (grep, list, read)
115
- /mode build # Read + Write (grep, list, read, patch, write)
116
- /mode verify # Read + Execute (grep, list, read, execute)
117
- /mode auto # All tools enabled
102
+ ```text
103
+ /mode plan # Readonly
104
+ /mode build # Read + Write
105
+ /mode verify # Read + Execute
106
+ /mode auto # All tools
107
+ /mode # Show current mode
118
108
  ```
119
109
 
120
- Each mode adapts the system prompt to guide appropriate behavior.
110
+ The system prompt is adjusted per mode so the LLM knows which actions
111
+ are permissible.
112
+
113
+ ## Features
114
+
115
+ * **Interactive REPL** – clean, streaming chat.
116
+ * **Toolbox** – file I/O, Git, shell execution.
117
+ * **Streaming Responses** – output appears in real time.
118
+ * **Conversation History** – persists across turns; can be cleared.
119
+ * **Context Dump** – `/context` shows the current conversation state.
120
+
121
+ ## Toolbox Overview
122
+
123
+ The `Toolbox` class is defined in `lib/elelem/toolbox.rb`. It supplies
124
+ seven tools, each represented by a JSON schema that the LLM can call.
125
+
126
+ | Tool | Purpose | Parameters |
127
+ | ---- | ------- | ---------- |
128
+ | `eval` | Dynamically create new tools | `code` |
129
+ | `grep` | Search Git‑tracked files | `query` |
130
+ | `list` | List tracked files | `path` (optional) |
131
+ | `read` | Read file contents | `path` |
132
+ | `write` | Overwrite a file | `path`, `content` |
133
+ | `patch` | Apply a unified diff via `git apply` | `diff` |
134
+ | `execute` | Run shell commands | `cmd`, `args`, `env`, `cwd`, `stdin` |
135
+
136
+ ## Tool Definition
137
+
138
+ The core `Tool` wrapper is defined in `lib/elelem/tool.rb`. Each tool is
139
+ created with a name, description, JSON schema for arguments, and a block
140
+ that performs the operation. The LLM calls a tool by name and passes the
141
+ arguments as a hash.
142
+
143
+ ## Known Limitations
121
144
 
122
- ## Development
145
+ * Assumes the current directory is a Git repository.
146
+ * No sandboxing – the LLM can run arbitrary commands.
147
+ * Error handling is minimal; exceptions are returned as an `error` field.
123
148
 
124
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
149
+ ## Contributing
125
150
 
126
- 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).
151
+ Feel free to open issues or pull requests. The repository follows the
152
+ GitHub Flow.
127
153
 
128
154
  ## License
129
155
 
130
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
156
+ MIT see the bundled `LICENSE.txt`.
data/lib/elelem/agent.rb CHANGED
@@ -2,16 +2,12 @@
2
2
 
3
3
  module Elelem
4
4
  class Agent
5
- attr_reader :conversation, :client, :tools
5
+ attr_reader :conversation, :client, :toolbox
6
6
 
7
- def initialize(client)
7
+ def initialize(client, toolbox)
8
8
  @conversation = Conversation.new
9
9
  @client = client
10
- @tools = {
11
- read: [grep_tool, list_tool, read_tool],
12
- write: [patch_tool, write_tool],
13
- execute: [exec_tool]
14
- }
10
+ @toolbox = toolbox
15
11
  end
16
12
 
17
13
  def repl
@@ -36,19 +32,18 @@ module Elelem
36
32
  puts " → Mode: verify (read + execute)"
37
33
  when "/mode"
38
34
  puts " Mode: #{mode.to_a.inspect}"
39
- puts " Tools: #{tools_for(mode).map { |t| t.dig(:function, :name) }}"
35
+ puts " Tools: #{toolbox.tools_for(mode).map { |t| t.dig(:function, :name) }}"
40
36
  when "/exit" then exit
41
37
  when "/clear"
42
38
  conversation.clear
43
39
  puts " → Conversation cleared"
44
- when "/context" then puts conversation.dump
40
+ when "/context" then puts conversation.dump(mode)
45
41
  else
46
42
  puts help_banner
47
43
  end
48
44
  else
49
- conversation.set_system_prompt(system_prompt_for(mode))
50
45
  conversation.add(role: :user, content: input)
51
- result = execute_turn(conversation.history, tools: tools_for(mode))
46
+ result = execute_turn(conversation.history_for(mode), tools: toolbox.tools_for(mode))
52
47
  conversation.add(role: result[:role], content: result[:content])
53
48
  end
54
49
  end
@@ -70,33 +65,6 @@ module Elelem
70
65
  HELP
71
66
  end
72
67
 
73
- def tools_for(modes)
74
- modes.map { |mode| tools[mode] }.flatten
75
- end
76
-
77
- def system_prompt_for(mode)
78
- base = "You are a reasoning coding and system agent."
79
-
80
- case mode.to_a.sort
81
- when [:read]
82
- "#{base}\n\nRead and analyze. Understand before suggesting action."
83
- when [:write]
84
- "#{base}\n\nWrite clean, thoughtful code."
85
- when [:execute]
86
- "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
87
- when [:read, :write]
88
- "#{base}\n\nFirst understand, then build solutions that integrate well."
89
- when [:read, :execute]
90
- "#{base}\n\nUse commands to deeply understand the system."
91
- when [:write, :execute]
92
- "#{base}\n\nCreate and execute freely. Have fun. Be kind."
93
- when [:read, :write, :execute]
94
- "#{base}\n\nYou have all tools. Use them wisely."
95
- else
96
- base
97
- end
98
- end
99
-
100
68
  def format_tool_call(name, args)
101
69
  case name
102
70
  when "execute"
@@ -143,7 +111,7 @@ module Elelem
143
111
  args = call.dig("function", "arguments")
144
112
 
145
113
  puts "Tool> #{format_tool_call(name, args)}"
146
- result = run_tool(name, args)
114
+ result = toolbox.run_tool(name, args)
147
115
  turn_context << { role: "tool", content: JSON.dump(result) }
148
116
  end
149
117
 
@@ -154,120 +122,5 @@ module Elelem
154
122
  return { role: "assistant", content: content }
155
123
  end
156
124
  end
157
-
158
- def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
159
- cmd = command.is_a?(Array) ? command.first : command
160
- cmd_args = command.is_a?(Array) ? command[1..] + args : args
161
- stdout, stderr, status = Open3.capture3(env, cmd, *cmd_args, chdir: cwd, stdin_data: stdin)
162
- {
163
- "exit_status" => status.exitstatus,
164
- "stdout" => stdout.to_s,
165
- "stderr" => stderr.to_s
166
- }
167
- end
168
-
169
- def expand_path(path)
170
- Pathname.new(path).expand_path
171
- end
172
-
173
- def read_file(path)
174
- full_path = expand_path(path)
175
- full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
176
- end
177
-
178
- def write_file(path, content)
179
- full_path = expand_path(path)
180
- FileUtils.mkdir_p(full_path.dirname)
181
- { bytes_written: full_path.write(content) }
182
- end
183
-
184
- def run_tool(name, args)
185
- case name
186
- when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"].to_s.empty? ? Dir.pwd : args["cwd"], stdin: args["stdin"])
187
- when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
188
- when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
189
- when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
190
- when "read" then read_file(args["path"])
191
- when "write" then write_file(args["path"], args["content"])
192
- else
193
- { error: "Unknown tool", name: name, args: args }
194
- end
195
- rescue => error
196
- { error: error.message, name: name, args: args }
197
- end
198
-
199
- def exec_tool
200
- build_tool(
201
- "execute",
202
- "Execute shell commands directly. Commands run in a shell context. Examples: 'date', 'git status'.",
203
- {
204
- cmd: { type: "string" },
205
- args: { type: "array", items: { type: "string" } },
206
- env: { type: "object", additionalProperties: { type: "string" } },
207
- cwd: { type: "string", description: "Working directory (defaults to current)" },
208
- stdin: { type: "string" }
209
- },
210
- ["cmd"]
211
- )
212
- end
213
-
214
- def grep_tool
215
- build_tool(
216
- "grep",
217
- "Search all git-tracked files using git grep. Returns file paths with matching line numbers.",
218
- { query: { type: "string" } },
219
- ["query"]
220
- )
221
- end
222
-
223
- def list_tool
224
- build_tool(
225
- "list",
226
- "List all git-tracked files in the repository, optionally filtered by path.",
227
- { path: { type: "string" } }
228
- )
229
- end
230
-
231
- def patch_tool
232
- build_tool(
233
- "patch",
234
- "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.",
235
- { diff: { type: "string" } },
236
- ["diff"]
237
- )
238
- end
239
-
240
- def read_tool
241
- build_tool(
242
- "read",
243
- "Read complete contents of a file. Requires exact file path.",
244
- { path: { type: "string" } },
245
- ["path"]
246
- )
247
- end
248
-
249
- def write_tool
250
- build_tool(
251
- "write",
252
- "Write complete file contents (overwrites existing files). Creates parent directories automatically.",
253
- { path: { type: "string" }, content: { type: "string" } },
254
- ["path", "content"]
255
- )
256
- end
257
-
258
- def build_tool(name, description, properties, required = [])
259
- {
260
- type: "function",
261
- function: {
262
- name: name,
263
- description: description,
264
- parameters: {
265
- type: "object",
266
- properties: properties,
267
- required: required
268
- }
269
- }
270
- }
271
- end
272
125
  end
273
126
  end
@@ -20,8 +20,7 @@ module Elelem
20
20
  model: options[:model],
21
21
  )
22
22
  say "Agent (#{options[:model]})", :green
23
- agent = Agent.new(client)
24
-
23
+ agent = Agent.new(client, Toolbox.new)
25
24
  agent.repl
26
25
  end
27
26
 
@@ -8,8 +8,10 @@ module Elelem
8
8
  @items = items
9
9
  end
10
10
 
11
- def history
12
- @items
11
+ def history_for(mode)
12
+ history = @items.dup
13
+ history[0] = { role: "system", content: system_prompt_for(mode) }
14
+ history
13
15
  end
14
16
 
15
17
  def add(role: :user, content: "")
@@ -28,18 +30,37 @@ module Elelem
28
30
  @items = default_context
29
31
  end
30
32
 
31
- def set_system_prompt(prompt)
32
- @items[0] = { role: :system, content: prompt }
33
+ def dump(mode)
34
+ JSON.pretty_generate(history_for(mode))
33
35
  end
34
36
 
35
- def dump
36
- JSON.pretty_generate(@items)
37
+ private
38
+
39
+ def default_context(prompt = system_prompt_for([]))
40
+ [{ role: "system", content: prompt }]
37
41
  end
38
42
 
39
- private
43
+ def system_prompt_for(mode)
44
+ base = system_prompt
40
45
 
41
- def default_context
42
- [{ role: "system", content: system_prompt }]
46
+ case mode.sort
47
+ when [:read]
48
+ "#{base}\n\nRead and analyze. Understand before suggesting action."
49
+ when [:write]
50
+ "#{base}\n\nWrite clean, thoughtful code."
51
+ when [:execute]
52
+ "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
53
+ when [:read, :write]
54
+ "#{base}\n\nFirst understand, then build solutions that integrate well."
55
+ when [:execute, :read]
56
+ "#{base}\n\nUse commands to deeply understand the system."
57
+ when [:execute, :write]
58
+ "#{base}\n\nCreate and execute freely. Have fun. Be kind."
59
+ when [:execute, :read, :write]
60
+ "#{base}\n\nYou have all tools. Use them wisely."
61
+ else
62
+ base
63
+ end
43
64
  end
44
65
 
45
66
  def system_prompt
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Tool
5
+ attr_reader :name
6
+
7
+ def initialize(schema, &block)
8
+ @name = schema.dig(:function, :name)
9
+ @schema = schema
10
+ @block = block
11
+ end
12
+
13
+ def call(args)
14
+ return ArgumentError.new(args) unless valid?(args)
15
+
16
+ @block.call(args)
17
+ end
18
+
19
+ def valid?(args)
20
+ # TODO:: Use JSON Schema Validator
21
+ true
22
+ end
23
+
24
+ def to_h
25
+ @schema&.to_h
26
+ end
27
+
28
+ class << self
29
+ def build(name, description, properties, required = [])
30
+ new({
31
+ type: "function",
32
+ function: {
33
+ name: name,
34
+ description: description,
35
+ parameters: {
36
+ type: "object",
37
+ properties: properties,
38
+ required: required
39
+ }
40
+ }
41
+ }) do |args|
42
+ yield args
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ class Toolbox
5
+ READ_TOOL = Tool.build("read", "Read complete contents of a file. Requires exact file path.", { path: { type: "string" } }, ["path"]) do |args|
6
+ path = args["path"]
7
+ full_path = Pathname.new(path).expand_path
8
+ full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
9
+ end
10
+
11
+ EXEC_TOOL = Tool.build("execute", "Execute shell commands directly. Commands run in a shell context. Examples: 'date', 'git status'.", { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, env: { type: "object", additionalProperties: { type: "string" } }, cwd: { type: "string", description: "Working directory (defaults to current)" }, stdin: { type: "string" } }, ["cmd"]) do |args|
12
+ Elelem.shell.execute(
13
+ args["cmd"],
14
+ args: args["args"] || [],
15
+ env: args["env"] || {},
16
+ cwd: args["cwd"].to_s.empty? ? Dir.pwd : args["cwd"],
17
+ stdin: args["stdin"]
18
+ )
19
+ end
20
+
21
+ GREP_TOOL = Tool.build("grep", "Search all git-tracked files using git grep. Returns file paths with matching line numbers.", { query: { type: "string" } }, ["query"]) do |args|
22
+ Elelem.shell.execute("git", args: ["grep", "-nI", args["query"]])
23
+ end
24
+
25
+ LIST_TOOL = Tool.build("list", "List all git-tracked files in the repository, optionally filtered by path.", { path: { type: "string" } }) do |args|
26
+ Elelem.shell.execute("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
27
+ end
28
+
29
+ PATCH_TOOL = Tool.build( "patch", "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.", { diff: { type: "string" } }, ["diff"]) do |args|
30
+ Elelem.shell.execute("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
31
+ end
32
+
33
+ WRITE_TOOL = Tool.build("write", "Write complete file contents (overwrites existing files). Creates parent directories automatically.", { path: { type: "string" }, content: { type: "string" } }, ["path", "content"]) do |args|
34
+ full_path = Pathname.new(args["path"]).expand_path
35
+ FileUtils.mkdir_p(full_path.dirname)
36
+ { bytes_written: full_path.write(args["content"]) }
37
+ end
38
+
39
+ attr_reader :tools
40
+
41
+ def initialize
42
+ @tools_by_name = {}
43
+ @tools = { read: [], write: [], execute: [] }
44
+ add_tool(eval_tool(binding), :execute)
45
+ add_tool(EXEC_TOOL, :execute)
46
+ add_tool(GREP_TOOL, :read)
47
+ add_tool(LIST_TOOL, :read)
48
+ add_tool(PATCH_TOOL, :write)
49
+ add_tool(READ_TOOL, :read)
50
+ add_tool(WRITE_TOOL, :write)
51
+ end
52
+
53
+ def add_tool(tool, mode)
54
+ @tools[mode] << tool
55
+ @tools_by_name[tool.name] = tool
56
+ end
57
+
58
+ def register_tool(name, description, properties = {}, required = [], mode: :execute, &block)
59
+ add_tool(Tool.build(name, description, properties, required, &block), mode)
60
+ end
61
+
62
+ def tools_for(modes)
63
+ Array(modes).map { |mode| tools[mode].map(&:to_h) }.flatten
64
+ end
65
+
66
+ def run_tool(name, args)
67
+ @tools_by_name[name]&.call(args) || { error: "Unknown tool", name: name, args: args }
68
+ rescue => error
69
+ { error: error.message, name: name, args: args, backtrace: error.backtrace.first(5) }
70
+ end
71
+
72
+ def tool_schema(name)
73
+ @tools_by_name[name]&.to_h
74
+ end
75
+
76
+ private
77
+
78
+ def eval_tool(target_binding)
79
+ Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required, mode: :execute) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
80
+ { result: target_binding.eval(args["ruby"]) }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Elelem
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -16,6 +16,8 @@ require "timeout"
16
16
  require_relative "elelem/agent"
17
17
  require_relative "elelem/application"
18
18
  require_relative "elelem/conversation"
19
+ require_relative "elelem/tool"
20
+ require_relative "elelem/toolbox"
19
21
  require_relative "elelem/version"
20
22
 
21
23
  Reline.input = $stdin
@@ -23,4 +25,29 @@ Reline.output = $stdout
23
25
 
24
26
  module Elelem
25
27
  class Error < StandardError; end
28
+
29
+ class Shell
30
+ def execute(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
31
+ cmd = command.is_a?(Array) ? command.first : command
32
+ cmd_args = command.is_a?(Array) ? command[1..] + args : args
33
+ stdout, stderr, status = Open3.capture3(
34
+ env,
35
+ cmd,
36
+ *cmd_args,
37
+ chdir: cwd,
38
+ stdin_data: stdin
39
+ )
40
+ {
41
+ "exit_status" => status.exitstatus,
42
+ "stdout" => stdout.to_s,
43
+ "stderr" => stderr.to_s
44
+ }
45
+ end
46
+ end
47
+
48
+ class << self
49
+ def shell
50
+ @shell ||= Shell.new
51
+ end
52
+ end
26
53
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elelem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: fileutils
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: json
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +107,20 @@ dependencies:
93
107
  - - ">="
94
108
  - !ruby/object:Gem::Version
95
109
  version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: pathname
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
96
124
  - !ruby/object:Gem::Dependency
97
125
  name: reline
98
126
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +135,20 @@ dependencies:
107
135
  - - ">="
108
136
  - !ruby/object:Gem::Version
109
137
  version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: set
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
110
152
  - !ruby/object:Gem::Dependency
111
153
  name: thor
112
154
  requirement: !ruby/object:Gem::Requirement
@@ -153,13 +195,15 @@ files:
153
195
  - lib/elelem/application.rb
154
196
  - lib/elelem/conversation.rb
155
197
  - lib/elelem/system_prompt.erb
198
+ - lib/elelem/tool.rb
199
+ - lib/elelem/toolbox.rb
156
200
  - lib/elelem/version.rb
157
- homepage: https://www.mokhan.ca
201
+ homepage: https://gitlab.com/mokhax/elelem
158
202
  licenses:
159
203
  - MIT
160
204
  metadata:
161
205
  allowed_push_host: https://rubygems.org
162
- homepage_uri: https://www.mokhan.ca
206
+ homepage_uri: https://gitlab.com/mokhax/elelem
163
207
  source_code_uri: https://gitlab.com/mokhax/elelem
164
208
  changelog_uri: https://gitlab.com/mokhax/elelem/-/blob/main/CHANGELOG.md
165
209
  rdoc_options: []