elelem 0.9.2 → 0.11.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/README.md +8 -118
- data/Rakefile +0 -11
- data/exe/elelem +8 -79
- data/lib/elelem/agent.rb +51 -142
- data/lib/elelem/cli.rb +76 -0
- data/lib/elelem/commands.rb +49 -0
- data/lib/elelem/config.rb +17 -0
- data/lib/elelem/conversation.rb +25 -0
- data/lib/elelem/input.rb +53 -0
- data/lib/elelem/null_input.rb +8 -0
- data/lib/elelem/null_output.rb +7 -0
- data/lib/elelem/null_tool.rb +18 -0
- data/lib/elelem/output.rb +57 -0
- data/lib/elelem/plugins.rb +10 -21
- data/lib/elelem/prompts/default.erb +3 -0
- data/lib/elelem/registration.rb +37 -0
- data/lib/elelem/registry.rb +69 -0
- data/lib/elelem/result.rb +26 -0
- data/lib/elelem/slash_command.rb +18 -0
- data/lib/elelem/stub_provider.rb +15 -0
- data/lib/elelem/system_prompt.rb +4 -42
- data/lib/elelem/tool.rb +6 -14
- data/lib/elelem/toolbox.rb +23 -23
- data/lib/elelem/version.rb +2 -2
- data/lib/elelem.rb +29 -27
- metadata +49 -65
- data/CHANGELOG.md +0 -280
- data/lib/elelem/mcp.rb +0 -96
- data/lib/elelem/net/claude.rb +0 -200
- data/lib/elelem/net/ollama.rb +0 -78
- data/lib/elelem/net/openai.rb +0 -86
- data/lib/elelem/net.rb +0 -16
- data/lib/elelem/plugins/confirm.rb +0 -12
- data/lib/elelem/plugins/edit.rb +0 -15
- data/lib/elelem/plugins/eval.rb +0 -20
- data/lib/elelem/plugins/execute.rb +0 -18
- data/lib/elelem/plugins/mcp.rb +0 -14
- data/lib/elelem/plugins/read.rb +0 -21
- data/lib/elelem/plugins/verify.rb +0 -47
- data/lib/elelem/plugins/write.rb +0 -23
- data/lib/elelem/templates/system_prompt.erb +0 -53
- data/lib/elelem/terminal.rb +0 -97
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72a4cd636b39b6f30cc0520c3445a3b8748625fd5ca52f15b05d27349a0701b4
|
|
4
|
+
data.tar.gz: 12c55aaf15f42e7607fb60ed78500b44655ba6eaab857601ec58073378587866
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '018856c824b90cbbe658c3b117f683737f14c34f493ba3629281cd7318c11d039585b829e201a9a8bf591619747d2d6c7f1f3568f53a79c40222632a706303ac'
|
|
7
|
+
data.tar.gz: ab3132d4613f560dfb97e7bcfa406e5f49ac66f5e642ec85f77c1a25d030825f1a7d9f6401d54dd6eb3f767ec69506b8f114e3a269324fb5fd72f0ca97827a0d
|
data/README.md
CHANGED
|
@@ -1,76 +1,17 @@
|
|
|
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
|
|
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.
|
|
7
|
+
Elelem is a minimal coding harness. The agent exposes a simple text-based UI.
|
|
11
8
|
|
|
12
9
|
## Design Principles
|
|
13
10
|
|
|
14
|
-
* Unix philosophy
|
|
11
|
+
* Unix philosophy - simple, composable, minimal.
|
|
15
12
|
* Convention over configuration.
|
|
16
13
|
* No defensive checks or complexity beyond what is necessary.
|
|
17
14
|
* Assumes a mature, responsible LLM that behaves like a capable engineer.
|
|
18
|
-
* Optimised for my personal workflow and preferences.
|
|
19
|
-
* Efficient and minimal like *aider* – https://aider.chat/.
|
|
20
|
-
* UX similar to Claude Code – https://docs.claude.com/en/docs/claude-code/overview.
|
|
21
|
-
|
|
22
|
-
## System Assumptions
|
|
23
|
-
|
|
24
|
-
* Linux host with Alacritty, tmux, Bash, Vim.
|
|
25
|
-
* Runs inside a Git repository.
|
|
26
|
-
* Git is available and functional.
|
|
27
|
-
|
|
28
|
-
## Dependencies
|
|
29
|
-
|
|
30
|
-
Elelem relies on several external tools. Install the ones you need:
|
|
31
|
-
|
|
32
|
-
| Tool | Purpose | Install |
|
|
33
|
-
|------|---------|---------|
|
|
34
|
-
| [Ollama](https://ollama.ai/) | Default LLM provider | https://ollama.ai/download |
|
|
35
|
-
| [glow](https://github.com/charmbracelet/glow) | Markdown rendering | `brew install glow` / `go install github.com/charmbracelet/glow@latest` |
|
|
36
|
-
| [ctags](https://ctags.io/) | Repo map generation | `brew install universal-ctags` / `apt install universal-ctags` |
|
|
37
|
-
| [ripgrep](https://github.com/BurntSushi/ripgrep) | Text search (`rg`) | `brew install ripgrep` / `apt install ripgrep` |
|
|
38
|
-
| [fd](https://github.com/sharkdp/fd) | File discovery | `brew install fd` / `apt install fd-find` |
|
|
39
|
-
| [ast-grep](https://ast-grep.github.io/) | Structural search (`sg`) | `brew install ast-grep` / `cargo install ast-grep` |
|
|
40
|
-
| [Git](https://git-scm.com/) | Version control | `brew install git` / `apt install git` |
|
|
41
|
-
|
|
42
|
-
**Required:** Git, Ollama (or another LLM provider)
|
|
43
|
-
|
|
44
|
-
**Recommended:** glow, ctags, ripgrep, fd
|
|
45
|
-
|
|
46
|
-
**Optional:** ast-grep (for structural code search)
|
|
47
|
-
|
|
48
|
-
## Scope
|
|
49
|
-
|
|
50
|
-
Only plain-text and source-code files are supported. No binary handling,
|
|
51
|
-
sandboxing, or permission checks are performed - the LLM has full access.
|
|
52
|
-
|
|
53
|
-
## Configuration
|
|
54
|
-
|
|
55
|
-
Prefer convention over configuration. Add environment variables only after
|
|
56
|
-
repeated use proves their usefulness.
|
|
57
|
-
|
|
58
|
-
## UI Expectations
|
|
59
|
-
|
|
60
|
-
Keyboard-driven, minimal TUI. No mouse support or complex widgets.
|
|
61
|
-
|
|
62
|
-
## Coding Standards for the LLM
|
|
63
|
-
|
|
64
|
-
* No extra error handling unless essential.
|
|
65
|
-
* Keep methods short, single-purpose.
|
|
66
|
-
* Descriptive, conventional names.
|
|
67
|
-
* Use Ruby standard library where possible.
|
|
68
|
-
|
|
69
|
-
## Helpful Links
|
|
70
|
-
|
|
71
|
-
* https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
|
|
72
|
-
* https://www.anthropic.com/engineering/writing-tools-for-agents
|
|
73
|
-
* https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
|
|
74
15
|
|
|
75
16
|
## Installation
|
|
76
17
|
|
|
@@ -88,68 +29,17 @@ Start an interactive chat session:
|
|
|
88
29
|
elelem chat
|
|
89
30
|
```
|
|
90
31
|
|
|
91
|
-
|
|
32
|
+
## Tools
|
|
92
33
|
|
|
93
|
-
|
|
94
|
-
* `--model` – Override the default model for the selected provider.
|
|
34
|
+
Use the plugin API to add tools.
|
|
95
35
|
|
|
96
|
-
|
|
36
|
+
## Contributing
|
|
97
37
|
|
|
98
38
|
```bash
|
|
99
|
-
|
|
100
|
-
elelem chat
|
|
101
|
-
|
|
102
|
-
# Anthropic Claude
|
|
103
|
-
ANTHROPIC_API_KEY=sk-... elelem chat --provider anthropic
|
|
104
|
-
|
|
105
|
-
# OpenAI
|
|
106
|
-
OPENAI_API_KEY=sk-... elelem chat --provider openai
|
|
107
|
-
|
|
108
|
-
# VertexAI (uses gcloud ADC)
|
|
109
|
-
elelem chat --provider vertex-ai --model claude-sonnet-4@20250514
|
|
39
|
+
$ git clone https://git.mokhan.ca/elelem/elelem.git
|
|
110
40
|
```
|
|
111
41
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Each provider reads its configuration from environment variables:
|
|
115
|
-
|
|
116
|
-
| Provider | Environment Variables |
|
|
117
|
-
|-------------|---------------------------------------------------|
|
|
118
|
-
| ollama | `OLLAMA_HOST` (default: localhost:11434) |
|
|
119
|
-
| anthropic | `ANTHROPIC_API_KEY` |
|
|
120
|
-
| openai | `OPENAI_API_KEY`, `OPENAI_BASE_URL` |
|
|
121
|
-
| vertex-ai | `GOOGLE_CLOUD_PROJECT`, `GOOGLE_CLOUD_REGION` |
|
|
122
|
-
|
|
123
|
-
## Features
|
|
124
|
-
|
|
125
|
-
* **Interactive REPL** – clean, streaming chat.
|
|
126
|
-
* **Toolbox** – file I/O and shell execution.
|
|
127
|
-
* **Streaming Responses** – output appears in real time.
|
|
128
|
-
* **Conversation History** – persists across turns; can be cleared.
|
|
129
|
-
* **Context Dump** – `/context` shows the current conversation state.
|
|
130
|
-
|
|
131
|
-
## Toolbox Overview
|
|
132
|
-
|
|
133
|
-
The `Toolbox` class is defined in `lib/elelem/toolbox.rb`. It supplies
|
|
134
|
-
three tools, each represented by a JSON schema that the LLM can call.
|
|
135
|
-
|
|
136
|
-
| Tool | Purpose | Parameters |
|
|
137
|
-
| --------- | ------------------ | ------------------ |
|
|
138
|
-
| `read` | Read file contents | `path` |
|
|
139
|
-
| `write` | Write file | `path`, `content` |
|
|
140
|
-
| `execute` | Run shell command | `command` |
|
|
141
|
-
|
|
142
|
-
Aliases: `bash`, `sh`, `exec` → `execute`; `open` → `read`
|
|
143
|
-
|
|
144
|
-
## Known Limitations
|
|
145
|
-
|
|
146
|
-
* Assumes the current directory is a Git repository.
|
|
147
|
-
* No sandboxing – the LLM can run arbitrary commands.
|
|
148
|
-
* Error handling is minimal; exceptions are returned as an `error` field.
|
|
149
|
-
|
|
150
|
-
## Contributing
|
|
151
|
-
|
|
152
|
-
Send me an email. For instructions see https://git-send-email.io/.
|
|
42
|
+
Send me a patch via email. For instructions see https://git-send-email.io/.
|
|
153
43
|
|
|
154
44
|
## License
|
|
155
45
|
|
data/Rakefile
CHANGED
|
@@ -5,15 +5,4 @@ require "rspec/core/rake_task"
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
task :files do
|
|
9
|
-
IO.popen(%w[git ls-files], chdir: __dir__, err: IO::NULL) do |ls|
|
|
10
|
-
ls.readlines.each do |f|
|
|
11
|
-
next if f.start_with?(*%w[bin/ spec/ pkg/ .git .rspec Gemfile Rakefile])
|
|
12
|
-
next if f.strip.end_with?(*%w[.toml .txt .md])
|
|
13
|
-
|
|
14
|
-
puts f
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
8
|
task default: %i[spec]
|
data/exe/elelem
CHANGED
|
@@ -1,85 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env -S ruby -W0
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require "elelem"
|
|
5
|
-
require "optparse"
|
|
4
|
+
require "elelem/cli"
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"ollama" => "gpt-oss:latest",
|
|
12
|
-
"anthropic" => "claude-opus-4-5-20250514",
|
|
13
|
-
"vertex" => "claude-opus-4-5@20251101",
|
|
14
|
-
"openai" => "gpt-4o"
|
|
15
|
-
}.freeze
|
|
16
|
-
|
|
17
|
-
PROVIDERS = {
|
|
18
|
-
"ollama" => ->(model) { Elelem::Net::Ollama.new(model: model, host: ENV.fetch("OLLAMA_HOST", "localhost:11434")) },
|
|
19
|
-
"anthropic" => ->(model) { Elelem::Net::Claude.anthropic(model: model, api_key: ENV.fetch("ANTHROPIC_API_KEY")) },
|
|
20
|
-
"vertex" => ->(model) { Elelem::Net::Claude.vertex(model: model, project: ENV.fetch("GOOGLE_CLOUD_PROJECT"), region: ENV.fetch("GOOGLE_CLOUD_REGION", "us-east5")) },
|
|
21
|
-
"openai" => ->(model) { Elelem::Net::OpenAI.new(model: model, api_key: ENV.fetch("OPENAI_API_KEY")) }
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
def initialize(args)
|
|
25
|
-
@provider = "ollama"
|
|
26
|
-
@model = nil
|
|
27
|
-
@args = parse(args)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def run
|
|
31
|
-
command = @args.shift || "chat"
|
|
32
|
-
send(command.tr("-", "_"))
|
|
33
|
-
rescue NoMethodError
|
|
34
|
-
abort "Unknown command: #{command}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def parse(args)
|
|
40
|
-
@parser = OptionParser.new do |o|
|
|
41
|
-
o.banner = "Usage: elelem [command] [options] [args]"
|
|
42
|
-
o.separator "\nCommands:"
|
|
43
|
-
o.separator " chat Interactive REPL (default)"
|
|
44
|
-
o.separator " ask <prompt> One-shot query (reads stdin if piped)"
|
|
45
|
-
o.separator " files Output files as XML (no options)"
|
|
46
|
-
o.separator " help Show this help"
|
|
47
|
-
o.separator "\nOptions:"
|
|
48
|
-
o.on("-p", "--provider NAME", "ollama, anthropic, vertex, openai") { |p| @provider = p }
|
|
49
|
-
o.on("-m", "--model NAME", "Override default model") { |m| @model = m }
|
|
50
|
-
o.on("-h", "--help") { puts o; exit }
|
|
51
|
-
end
|
|
52
|
-
@parser.parse!(args)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def help
|
|
56
|
-
puts @parser
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def client
|
|
60
|
-
model = @model || MODELS.fetch(@provider)
|
|
61
|
-
PROVIDERS.fetch(@provider).call(model)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def chat = Elelem.start(client)
|
|
65
|
-
|
|
66
|
-
def ask
|
|
67
|
-
abort "Usage: elelem ask <prompt>" if @args.empty?
|
|
68
|
-
prompt = @args.join(" ")
|
|
69
|
-
prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
|
|
70
|
-
Elelem::Terminal.new.markdown Elelem.ask(client, prompt)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def files
|
|
74
|
-
files = $stdin.stat.pipe? ? $stdin.readlines : `git ls-files`.lines
|
|
75
|
-
puts "<documents>"
|
|
76
|
-
files.each_with_index do |line, i|
|
|
77
|
-
path = line.strip
|
|
78
|
-
next if path.empty? || !File.file?(path)
|
|
79
|
-
puts %Q{<document index="#{i + 1}"><source>#{path}</source><content><![CDATA[#{File.read(path)}]]></content></document>}
|
|
80
|
-
end
|
|
81
|
-
puts "</documents>"
|
|
82
|
-
end
|
|
6
|
+
begin
|
|
7
|
+
require "elelem/builtins"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
warn "elelem: gem install elelem-builtins for the default tools and commands"
|
|
83
10
|
end
|
|
84
11
|
|
|
85
|
-
|
|
12
|
+
Signal.trap("INT") { exit 1 }
|
|
13
|
+
|
|
14
|
+
Elelem::CLI.new(ARGV).run
|
data/lib/elelem/agent.rb
CHANGED
|
@@ -2,81 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
module Elelem
|
|
4
4
|
class Agent
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
INIT_PROMPT = <<~PROMPT
|
|
8
|
-
AGENTS.md generator. Analyze codebase and write AGENTS.md to project root.
|
|
5
|
+
attr_reader :conversation, :toolbox, :input, :output, :commands
|
|
6
|
+
attr_accessor :provider
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Recommended Sections
|
|
14
|
-
- Commands: build, test, lint commands
|
|
15
|
-
- Code Style: conventions, patterns
|
|
16
|
-
- Architecture: key components and flow
|
|
17
|
-
- Testing: how to run tests
|
|
18
|
-
|
|
19
|
-
## Process
|
|
20
|
-
1. Read README.md if present
|
|
21
|
-
2. Identify language (Gemfile, package.json, go.mod)
|
|
22
|
-
3. Find test scripts (bin/test, npm test)
|
|
23
|
-
4. Check linter configs
|
|
24
|
-
5. Write concise AGENTS.md
|
|
25
|
-
|
|
26
|
-
Keep it minimal. No fluff.
|
|
27
|
-
PROMPT
|
|
28
|
-
|
|
29
|
-
attr_reader :history, :client, :toolbox, :terminal
|
|
30
|
-
|
|
31
|
-
def initialize(client, toolbox, terminal: nil, history: nil, system_prompt: nil)
|
|
32
|
-
@client = client
|
|
8
|
+
def initialize(provider, toolbox: Toolbox.new, input: NullInput.new, output: NullOutput.new, system_prompt: nil, commands: nil)
|
|
9
|
+
@provider = provider
|
|
33
10
|
@toolbox = toolbox
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
@
|
|
37
|
-
@
|
|
38
|
-
|
|
11
|
+
@commands = commands || Commands.new
|
|
12
|
+
@output = output
|
|
13
|
+
@input = input
|
|
14
|
+
@conversation = Conversation.new
|
|
15
|
+
@system_prompt = SystemPrompt.new(system_prompt)
|
|
39
16
|
end
|
|
40
17
|
|
|
41
18
|
def repl
|
|
42
|
-
|
|
19
|
+
output.say "elelem v#{VERSION}"
|
|
43
20
|
loop do
|
|
44
|
-
|
|
45
|
-
break if
|
|
46
|
-
next if
|
|
47
|
-
|
|
21
|
+
line = input.ask("> ")
|
|
22
|
+
break if line.nil?
|
|
23
|
+
next if line.empty?
|
|
24
|
+
line.start_with?("/") ? command(line) : turn(line)
|
|
48
25
|
end
|
|
49
26
|
end
|
|
50
27
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
when "/exit" then exit(0)
|
|
54
|
-
when "/init" then init_agents_md
|
|
55
|
-
when "/reload" then reload_source!
|
|
56
|
-
when "/shell"
|
|
57
|
-
transcript = start_shell
|
|
58
|
-
history << { role: "user", content: transcript } unless transcript.strip.empty?
|
|
59
|
-
when "/clear"
|
|
60
|
-
@history = []
|
|
61
|
-
@memory = nil
|
|
62
|
-
terminal.say " → context cleared"
|
|
63
|
-
when "/context"
|
|
64
|
-
terminal.say JSON.pretty_generate(combined_history)
|
|
65
|
-
else
|
|
66
|
-
terminal.say COMMANDS.join(" ")
|
|
67
|
-
end
|
|
28
|
+
def context
|
|
29
|
+
@conversation.to_a(system_prompt: @system_prompt.render)
|
|
68
30
|
end
|
|
69
31
|
|
|
70
|
-
def turn(
|
|
71
|
-
|
|
72
|
-
history << { role: "user", content: input }
|
|
32
|
+
def turn(prompt)
|
|
33
|
+
@conversation.add(role: "user", content: prompt)
|
|
73
34
|
ctx = []
|
|
74
35
|
content = nil
|
|
75
36
|
|
|
76
37
|
loop do
|
|
77
|
-
|
|
38
|
+
output.waiting
|
|
78
39
|
content, tool_calls = fetch_response(ctx)
|
|
79
|
-
|
|
40
|
+
output.say(content, as: :markdown)
|
|
80
41
|
break if tool_calls.empty?
|
|
81
42
|
|
|
82
43
|
ctx << { role: "assistant", content: content, tool_calls: tool_calls }.compact
|
|
@@ -85,102 +46,50 @@ module Elelem
|
|
|
85
46
|
end
|
|
86
47
|
end
|
|
87
48
|
|
|
88
|
-
|
|
49
|
+
@conversation.add(role: "assistant", content: content)
|
|
89
50
|
content
|
|
90
51
|
end
|
|
91
52
|
|
|
92
53
|
private
|
|
93
54
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@toolbox.add("task",
|
|
102
|
-
description: "Delegate subtask to focused agent (complex searches, multi-file analysis)",
|
|
103
|
-
params: { prompt: { type: "string" } },
|
|
104
|
-
required: ["prompt"]
|
|
105
|
-
) do |a|
|
|
106
|
-
sub = Agent.new(client, toolbox, terminal: terminal,
|
|
107
|
-
system_prompt: "Research agent. Search, analyze, report. Be concise.")
|
|
108
|
-
sub.turn(a["prompt"])
|
|
109
|
-
{ result: sub.history.last[:content] }
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def init_agents_md
|
|
114
|
-
sub = Agent.new(client, toolbox, terminal: terminal, system_prompt: INIT_PROMPT)
|
|
115
|
-
sub.turn("Generate AGENTS.md for this project")
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def reload_source!
|
|
119
|
-
lib_dir = File.expand_path("..", __dir__)
|
|
120
|
-
original_verbose, $VERBOSE = $VERBOSE, nil
|
|
121
|
-
Dir["#{lib_dir}/**/*.rb"].sort.each { |f| load(f) }
|
|
122
|
-
$VERBOSE = original_verbose
|
|
123
|
-
@toolbox = Toolbox.new
|
|
124
|
-
Plugins.reload!(@toolbox)
|
|
125
|
-
register_task_tool
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def start_shell
|
|
129
|
-
Tempfile.create do |file|
|
|
130
|
-
system("script", "-q", file.path, chdir: Dir.pwd)
|
|
131
|
-
strip_ansi(File.read(file.path))
|
|
132
|
-
end
|
|
55
|
+
def command(line)
|
|
56
|
+
parts = line.delete_prefix("/").split(" ", 2)
|
|
57
|
+
name, args = parts[0], parts[1]
|
|
58
|
+
commands.run(name, args) || output.say(commands.names.join(" "))
|
|
59
|
+
rescue => e
|
|
60
|
+
Elelem.logger.warn("agent: #{e.message}")
|
|
61
|
+
output.say(e.message, as: :error)
|
|
133
62
|
end
|
|
134
63
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
64
|
+
def process(tool_call)
|
|
65
|
+
name, args = tool_call[:name], tool_call[:arguments]
|
|
66
|
+
output.doing(toolbox.tool_for(name).name, args)
|
|
67
|
+
Elelem.logger.debug("agent: #{name}(#{args.inspect})")
|
|
68
|
+
result = toolbox.run(name.to_s, args)
|
|
69
|
+
Elelem.logger.debug("agent: #{name} -> #{result.inspect}")
|
|
70
|
+
result
|
|
142
71
|
end
|
|
143
72
|
|
|
144
73
|
def fetch_response(ctx)
|
|
145
|
-
content =
|
|
146
|
-
tool_calls =
|
|
147
|
-
|
|
148
|
-
|
|
74
|
+
content = String.new
|
|
75
|
+
tool_calls = []
|
|
76
|
+
|
|
77
|
+
provider.fetch(@conversation.to_a(system_prompt: @system_prompt.render) + ctx, toolbox.to_a) do |event|
|
|
78
|
+
case event[:type]
|
|
79
|
+
when "saying"
|
|
80
|
+
content << event[:text].to_s
|
|
81
|
+
when "thinking"
|
|
82
|
+
output.thinking(event[:text])
|
|
83
|
+
when "doing"
|
|
84
|
+
tool_calls << { id: event[:id], name: event[:name], arguments: event[:arguments] }
|
|
85
|
+
end
|
|
149
86
|
end
|
|
87
|
+
|
|
150
88
|
[content, tool_calls]
|
|
151
89
|
rescue => e
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def combined_history
|
|
157
|
-
[{ role: "system", content: system_prompt }] + history
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def system_prompt
|
|
161
|
-
@system_prompt || SystemPrompt.new(memory: @memory).render
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def compact_if_needed
|
|
165
|
-
return if history.length <= MAX_CONTEXT_MESSAGES
|
|
166
|
-
|
|
167
|
-
terminal.say " → compacting context"
|
|
168
|
-
keep = MAX_CONTEXT_MESSAGES / 2
|
|
169
|
-
old = history.first(history.length - keep)
|
|
170
|
-
|
|
171
|
-
to_summarize = @memory ? [{ role: "memory", content: @memory }, *old] : old
|
|
172
|
-
@memory = summarize(to_summarize)
|
|
173
|
-
@history = history.last(keep)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def summarize(messages)
|
|
177
|
-
text = messages.map { |message| { role: message[:role], content: message[:content] } }.to_json
|
|
178
|
-
|
|
179
|
-
String.new.tap do |buffer|
|
|
180
|
-
client.fetch([{ role: "user", content: "Summarize key facts:\n#{text}" }], []) do |d|
|
|
181
|
-
buffer << d[:content].to_s
|
|
182
|
-
end
|
|
183
|
-
end
|
|
90
|
+
Elelem.logger.warn("agent: #{e.message}")
|
|
91
|
+
output.say(e.message, as: :error)
|
|
92
|
+
["Error: #{e.message}", []]
|
|
184
93
|
end
|
|
185
94
|
end
|
|
186
95
|
end
|
data/lib/elelem/cli.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "elelem"
|
|
4
|
+
require "io/console"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "reline"
|
|
7
|
+
|
|
8
|
+
module Elelem
|
|
9
|
+
class CLI
|
|
10
|
+
COMMANDS = %w[chat ask files help].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(args, provider: "stub")
|
|
13
|
+
@provider = provider
|
|
14
|
+
@args = parse(args)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
command = @args.shift || "chat"
|
|
19
|
+
abort "Unknown command: #{command}" unless COMMANDS.include?(command)
|
|
20
|
+
send(command)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def parse(args)
|
|
26
|
+
@parser = OptionParser.new do |o|
|
|
27
|
+
o.banner = "Usage: #{File.basename($PROGRAM_NAME)} [command] [options] [args]"
|
|
28
|
+
o.separator "\nCommands:"
|
|
29
|
+
o.separator " chat Interactive REPL (default)"
|
|
30
|
+
o.separator " ask <prompt> One-shot query (reads stdin if piped)"
|
|
31
|
+
o.separator " files Output files as XML (no options)"
|
|
32
|
+
o.separator " help Show this help"
|
|
33
|
+
o.separator "\nOptions:"
|
|
34
|
+
o.on("-p", "--provider NAME", "Provider to use (default: #{@provider})") { |p| @provider = p }
|
|
35
|
+
o.on("-h", "--help", "Show this help") { puts o; exit }
|
|
36
|
+
end
|
|
37
|
+
@parser.parse!(args)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def help
|
|
41
|
+
puts @parser
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def chat
|
|
45
|
+
Elelem.start(provider: @provider)
|
|
46
|
+
rescue => e
|
|
47
|
+
Elelem.logger.warn("cli: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
48
|
+
abort "elelem: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ask
|
|
52
|
+
abort "Usage: elelem-chat ask <prompt>" if @args.empty?
|
|
53
|
+
prompt = @args.join(" ")
|
|
54
|
+
prompt = "#{prompt}\n\n```\n#{$stdin.read}\n```" if $stdin.stat.pipe?
|
|
55
|
+
piped = !$stdout.tty?
|
|
56
|
+
output = Elelem::Output.new(stream: piped ? $stderr : $stdout)
|
|
57
|
+
reply = Elelem.ask(prompt, provider: @provider, output: output)
|
|
58
|
+
abort "elelem: no reply" if reply.to_s.strip.empty?
|
|
59
|
+
Elelem::Output.new.say(reply, as: :markdown) if piped
|
|
60
|
+
rescue => e
|
|
61
|
+
Elelem.logger.warn("cli: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
62
|
+
abort "elelem: #{e.message}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def files
|
|
66
|
+
files = $stdin.stat.pipe? ? $stdin.readlines : `git ls-files`.lines
|
|
67
|
+
puts "<documents>"
|
|
68
|
+
files.each_with_index do |line, i|
|
|
69
|
+
path = line.strip
|
|
70
|
+
next if path.empty? || !File.file?(path)
|
|
71
|
+
puts %Q{<document index="#{i + 1}"><source>#{path}</source><document_content><![CDATA[#{File.read(path)}]]></document_content></document>}
|
|
72
|
+
end
|
|
73
|
+
puts "</documents>"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Commands
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
def initialize(registry = {})
|
|
8
|
+
@registry = registry
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def clear!
|
|
12
|
+
@registry.clear
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register(name, description: "", completions: nil, &handler)
|
|
16
|
+
@registry[name] = SlashCommand.new(name, description, completions, &handler)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def completions_for(name, partial = "")
|
|
20
|
+
cmd = command_for(name)
|
|
21
|
+
return [] unless cmd && cmd.completions
|
|
22
|
+
|
|
23
|
+
options = cmd.completions.respond_to?(:call) ? cmd.completions.call : cmd.completions
|
|
24
|
+
options.select { |o| o.start_with?(partial) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(name, args = nil)
|
|
28
|
+
command = command_for(name)
|
|
29
|
+
return false unless command
|
|
30
|
+
|
|
31
|
+
command.call(args)
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def names
|
|
36
|
+
@registry.keys.map { |name| "/#{name}" }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each
|
|
40
|
+
@registry.each { |name, command| yield "/#{name}", command.description }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def command_for(name)
|
|
46
|
+
@registry[name]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
module Config
|
|
5
|
+
extend SingleForwardable
|
|
6
|
+
|
|
7
|
+
def_single_delegators :default, :build_provider, :build_agent, :apply, :reload, :names
|
|
8
|
+
|
|
9
|
+
def self.default
|
|
10
|
+
@default ||= Registry.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.configure(&block)
|
|
15
|
+
Config.default.configure(&block)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elelem
|
|
4
|
+
class Conversation
|
|
5
|
+
ROLES = %w[user assistant tool].freeze
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@messages = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(role:, content:, **extra)
|
|
12
|
+
raise ArgumentError, "invalid role: #{role}" unless ROLES.include?(role)
|
|
13
|
+
@messages << { role: role, content: content, **extra }.compact
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def last = @messages.last
|
|
17
|
+
def length = @messages.length
|
|
18
|
+
def clear! = @messages.clear
|
|
19
|
+
|
|
20
|
+
def to_a(system_prompt: nil)
|
|
21
|
+
base = system_prompt ? [{ role: "system", content: system_prompt }] : []
|
|
22
|
+
base + @messages
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|