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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +99 -73
- data/lib/elelem/agent.rb +7 -154
- data/lib/elelem/application.rb +1 -2
- data/lib/elelem/conversation.rb +30 -9
- data/lib/elelem/tool.rb +47 -0
- data/lib/elelem/toolbox.rb +84 -0
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +27 -0
- metadata +47 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2885faeaffa4f0ee7eff742f50bbf1ee78f257f7989c6401c787c4d4feb5d6a2
|
|
4
|
+
data.tar.gz: '0965d3a94ce8633cd01e7471e5688912216794e9b6af49450d0c1a5326e24645'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
3
|
+
Fast, correct, autonomous – pick two.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Purpose
|
|
6
6
|
|
|
7
|
-
Elelem is a minimal coding agent written in Ruby. It is
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
13
|
+
## Design Principles
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
23
|
+
## System Assumptions
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
29
|
+
## Scope
|
|
31
30
|
|
|
32
|
-
-
|
|
33
|
-
|
|
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
|
-
|
|
34
|
+
## Configuration
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
39
|
+
## UI Expectations
|
|
45
40
|
|
|
46
|
-
-
|
|
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
|
-
|
|
43
|
+
## Coding Standards for the LLM
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
50
|
+
## Helpful Links
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
#
|
|
81
|
+
# Default model
|
|
95
82
|
elelem chat
|
|
96
83
|
|
|
97
|
-
#
|
|
84
|
+
# Specific model and host
|
|
98
85
|
elelem chat --model llama2 --host remote-host:11434
|
|
99
86
|
```
|
|
100
87
|
|
|
101
|
-
|
|
88
|
+
## Mode System
|
|
102
89
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
Use the following commands inside the REPL:
|
|
112
101
|
|
|
113
|
-
```
|
|
114
|
-
/mode plan # Read
|
|
115
|
-
/mode build # Read + Write
|
|
116
|
-
/mode verify # Read + Execute
|
|
117
|
-
/mode auto # All tools
|
|
102
|
+
```text
|
|
103
|
+
/mode plan # Read‑only
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
## Contributing
|
|
125
150
|
|
|
126
|
-
|
|
151
|
+
Feel free to open issues or pull requests. The repository follows the
|
|
152
|
+
GitHub Flow.
|
|
127
153
|
|
|
128
154
|
## License
|
|
129
155
|
|
|
130
|
-
|
|
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, :
|
|
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
|
-
@
|
|
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.
|
|
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
|
data/lib/elelem/application.rb
CHANGED
data/lib/elelem/conversation.rb
CHANGED
|
@@ -8,8 +8,10 @@ module Elelem
|
|
|
8
8
|
@items = items
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def
|
|
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
|
|
32
|
-
|
|
33
|
+
def dump(mode)
|
|
34
|
+
JSON.pretty_generate(history_for(mode))
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def default_context(prompt = system_prompt_for([]))
|
|
40
|
+
[{ role: "system", content: prompt }]
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
|
|
43
|
+
def system_prompt_for(mode)
|
|
44
|
+
base = system_prompt
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
[
|
|
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
|
data/lib/elelem/tool.rb
ADDED
|
@@ -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
|
data/lib/elelem/version.rb
CHANGED
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.
|
|
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://
|
|
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://
|
|
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: []
|