elelem 0.2.1 → 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.
@@ -1,8 +1,84 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "toolbox/exec"
4
- require_relative "toolbox/file"
5
- require_relative "toolbox/web"
6
- require_relative "toolbox/mcp"
7
- require_relative "toolbox/prompt"
8
- require_relative "toolbox/memory"
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.2.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/elelem.rb CHANGED
@@ -1,40 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cli/ui"
4
3
  require "erb"
4
+ require "fileutils"
5
5
  require "json"
6
6
  require "json-schema"
7
7
  require "logger"
8
8
  require "net/llm"
9
9
  require "open3"
10
+ require "pathname"
10
11
  require "reline"
12
+ require "set"
11
13
  require "thor"
12
14
  require "timeout"
13
15
 
14
16
  require_relative "elelem/agent"
15
- require_relative "elelem/api"
16
17
  require_relative "elelem/application"
17
- require_relative "elelem/configuration"
18
18
  require_relative "elelem/conversation"
19
- require_relative "elelem/mcp_client"
20
- require_relative "elelem/states/idle"
21
- require_relative "elelem/states/working"
22
- require_relative "elelem/states/working/state"
23
- require_relative "elelem/states/working/error"
24
- require_relative "elelem/states/working/executing"
25
- require_relative "elelem/states/working/talking"
26
- require_relative "elelem/states/working/thinking"
27
- require_relative "elelem/states/working/waiting"
28
19
  require_relative "elelem/tool"
29
20
  require_relative "elelem/toolbox"
30
- require_relative "elelem/tools"
31
- require_relative "elelem/tui"
32
21
  require_relative "elelem/version"
33
22
 
34
- CLI::UI::StdoutRouter.enable
35
23
  Reline.input = $stdin
36
24
  Reline.output = $stdout
37
25
 
38
26
  module Elelem
39
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
40
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.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -10,7 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: cli-ui
13
+ name: erb
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - ">="
@@ -24,7 +24,7 @@ dependencies:
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: erb
27
+ name: fileutils
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - ">="
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - ">="
108
108
  - !ruby/object:Gem::Version
109
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'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: reline
112
126
  requirement: !ruby/object:Gem::Requirement
@@ -121,6 +135,20 @@ dependencies:
121
135
  - - ">="
122
136
  - !ruby/object:Gem::Version
123
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'
124
152
  - !ruby/object:Gem::Dependency
125
153
  name: thor
126
154
  requirement: !ruby/object:Gem::Requirement
@@ -154,8 +182,6 @@ email:
154
182
  - mo@mokhan.ca
155
183
  executables:
156
184
  - elelem
157
- - llm-ollama
158
- - llm-openai
159
185
  extensions: []
160
186
  extra_rdoc_files: []
161
187
  files:
@@ -164,41 +190,20 @@ files:
164
190
  - README.md
165
191
  - Rakefile
166
192
  - exe/elelem
167
- - exe/llm-ollama
168
- - exe/llm-openai
169
193
  - lib/elelem.rb
170
194
  - lib/elelem/agent.rb
171
- - lib/elelem/api.rb
172
195
  - lib/elelem/application.rb
173
- - lib/elelem/configuration.rb
174
196
  - lib/elelem/conversation.rb
175
- - lib/elelem/mcp_client.rb
176
- - lib/elelem/states/idle.rb
177
- - lib/elelem/states/working.rb
178
- - lib/elelem/states/working/error.rb
179
- - lib/elelem/states/working/executing.rb
180
- - lib/elelem/states/working/state.rb
181
- - lib/elelem/states/working/talking.rb
182
- - lib/elelem/states/working/thinking.rb
183
- - lib/elelem/states/working/waiting.rb
184
197
  - lib/elelem/system_prompt.erb
185
198
  - lib/elelem/tool.rb
186
199
  - lib/elelem/toolbox.rb
187
- - lib/elelem/toolbox/exec.rb
188
- - lib/elelem/toolbox/file.rb
189
- - lib/elelem/toolbox/mcp.rb
190
- - lib/elelem/toolbox/memory.rb
191
- - lib/elelem/toolbox/prompt.rb
192
- - lib/elelem/toolbox/web.rb
193
- - lib/elelem/tools.rb
194
- - lib/elelem/tui.rb
195
200
  - lib/elelem/version.rb
196
- homepage: https://www.mokhan.ca
201
+ homepage: https://gitlab.com/mokhax/elelem
197
202
  licenses:
198
203
  - MIT
199
204
  metadata:
200
205
  allowed_push_host: https://rubygems.org
201
- homepage_uri: https://www.mokhan.ca
206
+ homepage_uri: https://gitlab.com/mokhax/elelem
202
207
  source_code_uri: https://gitlab.com/mokhax/elelem
203
208
  changelog_uri: https://gitlab.com/mokhax/elelem/-/blob/main/CHANGELOG.md
204
209
  rdoc_options: []
data/exe/llm-ollama DELETED
@@ -1,358 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- =begin
4
- Fast, correct, autonomous - Pick two
5
-
6
- PURPOSE:
7
-
8
- This script is a minimal coding agent written in Ruby. It is intended to
9
- assist me (a software engineer and computer science student) with writing,
10
- editing, and managing code and text files from the command line. It acts
11
- as a direct interface to an LLM, providing it with a simple text-based
12
- UI and access to the local filesystem.
13
-
14
- DESIGN PRINCIPLES:
15
-
16
- - Follows the Unix philosophy: simple, composable, minimal.
17
- - Convention over configuration.
18
- - Avoids unnecessary defensive checks, or complexity.
19
- - Assumes a mature and responsible LLM that behaves like a capable engineer.
20
- - Designed for my workflow and preferences.
21
- - Efficient and minimal like aider - https://aider.chat/
22
- - UX like Claude Code - https://docs.claude.com/en/docs/claude-code/overview
23
-
24
- SYSTEM ASSUMPTIONS:
25
-
26
- - This script is used on a Linux system with the following tools: Alacritty, tmux, Bash, and Vim.
27
- - It is always run inside a Git repository.
28
- - All project work is assumed to be version-controlled with Git.
29
- - Git is expected to be available and working; no checks are necessary.
30
-
31
- SCOPE:
32
-
33
- - This program operates only on code and plain-text files.
34
- - It does not need to support binary files.
35
- - The LLM has full access to execute system commands.
36
- - There are no sandboxing, permission, or validation layers.
37
- - Execution is not restricted or monitored — responsibility is delegated to the LLM.
38
-
39
- CONFIGURATION:
40
-
41
- - Avoid adding configuration options unless absolutely necessary.
42
- - Prefer hard-coded values that can be changed later if needed.
43
- - Only introduce environment variables after repeated usage proves them worthwhile.
44
-
45
- UI EXPECTATIONS:
46
-
47
- - The TUI must remain simple, fast, and predictable.
48
- - No mouse support or complex UI components are required.
49
- - Interaction is strictly keyboard-driven.
50
-
51
- CODING STANDARDS FOR LLM:
52
-
53
- - Do not add error handling or logging unless it is essential for functionality.
54
- - Keep methods short and single-purpose.
55
- - Use descriptive, conventional names.
56
- - Stick to Ruby's standard library whenever possible.
57
-
58
- HELPFUL LINKS:
59
-
60
- - https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents
61
- - https://www.anthropic.com/engineering/writing-tools-for-agents
62
- - https://simonwillison.net/2025/Sep/30/designing-agentic-loops/
63
-
64
- =end
65
-
66
- require "bundler/inline"
67
-
68
- gemfile do
69
- source "https://rubygems.org"
70
-
71
- gem "fileutils", "~> 1.0"
72
- gem "json", "~> 2.0"
73
- gem "net-llm", "~> 0.4"
74
- gem "open3", "~> 0.1"
75
- gem "ostruct", "~> 0.1"
76
- gem "reline", "~> 0.1"
77
- gem "set", "~> 1.0"
78
- gem "uri", "~> 1.0"
79
- end
80
-
81
- STDOUT.set_encoding(Encoding::UTF_8)
82
- STDERR.set_encoding(Encoding::UTF_8)
83
-
84
- OLLAMA_HOST = ENV["OLLAMA_HOST"] || "localhost:11434"
85
- OLLAMA_MODEL = ENV["OLLAMA_MODEL"] || "gpt-oss:latest"
86
- SYSTEM_PROMPT="You are a reasoning coding and system agent."
87
-
88
- def build_tool(name, description, properties, required = [])
89
- {
90
- type: "function",
91
- function: {
92
- name: name,
93
- description: description,
94
- parameters: {
95
- type: "object",
96
- properties: properties,
97
- required: required
98
- }
99
- }
100
- }
101
- end
102
-
103
- 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"])
104
- 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"])
105
- 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" } })
106
- 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"])
107
- 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"])
108
- 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"])
109
-
110
- TOOLS = {
111
- read: [GREP_TOOL, LS_TOOL, READ_TOOL],
112
- write: [PATCH_TOOL, WRITE_TOOL],
113
- execute: [EXEC_TOOL]
114
- }
115
-
116
- trap("INT") do
117
- puts "\nExiting."
118
- exit
119
- end
120
-
121
- def run_exec(command, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
122
- stdout, stderr, status = Open3.capture3(env, command, *args, chdir: cwd, stdin_data: stdin)
123
- {
124
- "exit_status" => status.exitstatus,
125
- "stdout" => stdout.to_s,
126
- "stderr" => stderr.to_s
127
- }
128
- end
129
-
130
- def expand_path(path)
131
- Pathname.new(path).expand_path
132
- end
133
-
134
- def read_file(path)
135
- full_path = expand_path(path)
136
- full_path.exist? ? { content: full_path.read } : { error: "File not found: #{path}" }
137
- end
138
-
139
- def write_file(path, content)
140
- full_path = expand_path(path)
141
- FileUtils.mkdir_p(full_path.dirname)
142
- { bytes_written: full_path.write(content) }
143
- end
144
-
145
- def run_tool(name, args)
146
- case name
147
- when "execute" then run_exec(args["cmd"], args: args["args"] || [], env: args["env"] || {}, cwd: args["cwd"] || Dir.pwd, stdin: args["stdin"])
148
- when "grep" then run_exec("git", args: ["grep", "-nI", args["query"]])
149
- when "list" then run_exec("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
150
- when "patch" then run_exec("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
151
- when "read" then read_file(args["path"])
152
- when "write" then write_file(args["path"], args["content"])
153
- else
154
- { error: "Unknown tool", name: name, args: args }
155
- end
156
- end
157
-
158
- def format_tool_call(name, args)
159
- case name
160
- when "execute" then "execute(#{args["cmd"]})"
161
- when "grep" then "grep(#{args["query"]})"
162
- when "list" then "list(#{args["path"] || '.'})"
163
- when "patch" then "patch(#{args["diff"].lines.count} lines)"
164
- when "read" then "read(#{args["path"]})"
165
- when "write" then "write(#{args["path"]})"
166
- else
167
- "▶ #{name}(#{args.to_s[0...70]})"
168
- end
169
- end
170
-
171
- def system_prompt_for(mode)
172
- base = "You are a reasoning coding and system agent."
173
-
174
- case mode.sort
175
- when [:read]
176
- "#{base}\n\nRead and analyze. Understand before suggesting action."
177
- when [:write]
178
- "#{base}\n\nWrite clean, thoughtful code."
179
- when [:execute]
180
- "#{base}\n\nUse shell commands creatively to understand and manipulate the system."
181
- when [:read, :write]
182
- "#{base}\n\nFirst understand, then build solutions that integrate well."
183
- when [:read, :execute]
184
- "#{base}\n\nUse commands to deeply understand the system."
185
- when [:write, :execute]
186
- "#{base}\n\nCreate and execute freely. Have fun. Be kind."
187
- when [:read, :write, :execute]
188
- "#{base}\n\nYou have all tools. Use them wisely."
189
- else
190
- base
191
- end
192
- end
193
-
194
- def tools_for(modes)
195
- modes.map { |mode| TOOLS[mode] }.flatten
196
- end
197
-
198
- def prune_context(messages, keep_recent: 5)
199
- return messages if messages.length <= keep_recent + 1
200
-
201
- default_context + messages.last(keep_recent)
202
- end
203
-
204
- def execute_turn(client, messages, tools:)
205
- turn_context = []
206
-
207
- loop do
208
- content = ""
209
- tool_calls = nil
210
- role = "assistant"
211
- first_content = true
212
-
213
- print "Thinking..."
214
- client.chat(messages + turn_context, tools) do |chunk|
215
- if chunk["message"]
216
- msg = chunk["message"]
217
- role = msg["role"] if msg["role"]
218
-
219
- if msg["thinking"] && !msg["thinking"].empty?
220
- print "."
221
- end
222
-
223
- if msg["content"] && !msg["content"].empty?
224
- if first_content
225
- print "\r\e[KAssistant> "
226
- first_content = false
227
- end
228
- print msg["content"]
229
- $stdout.flush
230
- content += msg["content"]
231
- end
232
-
233
- tool_calls = msg["tool_calls"] if msg["tool_calls"]
234
- end
235
- end
236
- puts
237
-
238
- turn_context << { role: role, content: content, tool_calls: tool_calls }.compact
239
-
240
- if tool_calls
241
- tool_calls.each do |call|
242
- name = call.dig("function", "name")
243
- args_raw = call.dig("function", "arguments")
244
-
245
- begin
246
- args = args_raw.is_a?(String) ? JSON.parse(args_raw) : args_raw
247
- rescue JSON::ParserError => e
248
- turn_context << {
249
- role: "tool",
250
- content: JSON.dump({
251
- error: "Invalid JSON in arguments: #{e.message}",
252
- received: args_raw
253
- })
254
- }
255
- next
256
- end
257
-
258
- puts "Tool> #{format_tool_call(name, args)}"
259
- result = run_tool(name, args)
260
- turn_context << { role: "tool", content: JSON.dump(result) }
261
- end
262
- next
263
- end
264
-
265
- return { role: "assistant", content: content } unless content.strip.empty?
266
- end
267
- end
268
-
269
- def dump_context(messages)
270
- puts JSON.pretty_generate(messages)
271
- end
272
-
273
- def print_status(mode, messages)
274
- puts "Mode: #{mode.inspect}"
275
- puts "Tools: #{tools_for(mode).map { |x| x.dig(:function, :name) }}"
276
- end
277
-
278
- def strip_ansi(text)
279
- text.gsub(/^Script started.*?\n/, '')
280
- .gsub(/\nScript done.*$/, '')
281
- .gsub(/\e\[[0-9;]*[a-zA-Z]/, '') # Standard ANSI codes
282
- .gsub(/\e\[\?[0-9]+[hl]/, '') # Bracketed paste mode
283
- .gsub(/[\b]/, '') # Backspace chars
284
- .gsub(/\r/, '') # Carriage returns
285
- end
286
-
287
- def start_shell
288
- Tempfile.create do |file|
289
- system("script -q #{file.path}")
290
- { role: "user", content: strip_ansi(File.read(file.path)) }
291
- end
292
- end
293
-
294
- def ask?(text)
295
- input = Reline.readline(text, true)&.strip
296
- exit if input.nil? || input.downcase == "exit"
297
-
298
- input
299
- end
300
-
301
- def print_help
302
- puts <<~HELP
303
- /chmod - (+|-)rwx auto build plan
304
- /clear
305
- /context
306
- /exit
307
- /help
308
- /shell
309
- /status
310
- HELP
311
- end
312
-
313
- def default_context
314
- [{ role: "system", content: SYSTEM_PROMPT }]
315
- end
316
-
317
- def main
318
- client = Net::Llm::Ollama.new(
319
- host: OLLAMA_HOST,
320
- model: OLLAMA_MODEL
321
- )
322
-
323
- messages = default_context
324
- mode = Set.new([:read])
325
-
326
- loop do
327
- input = ask?("User> ")
328
- if input.start_with?("/")
329
- case input
330
- when "/chmod +r" then mode.add(:read)
331
- when "/chmod +w" then mode.add(:write)
332
- when "/chmod +x" then mode.add(:execute)
333
- when "/chmod -r" then mode.add(:read)
334
- when "/chmod -w" then mode.add(:write)
335
- when "/chmod -x" then mode.add(:execute)
336
- when "/clear" then messages = default_context
337
- when "/compact" then messages = prune_context(messages, keep_recent: 10)
338
- when "/context" then dump_context(messages)
339
- when "/exit" then exit
340
- when "/help" then print_help
341
- when "/mode auto" then mode = Set[:read, :write, :execute]
342
- when "/mode build" then mode = Set[:read, :write]
343
- when "/mode plan" then mode = Set[:read]
344
- when "/mode verify" then mode = Set[:read, :execute]
345
- when "/mode" then print_status(mode, messages)
346
- when "/shell" then messages << start_shell
347
- else
348
- print_help
349
- end
350
- else
351
- messages[0] = { role: "system", content: system_prompt_for(mode) }
352
- messages << { role: "user", content: input }
353
- messages << execute_turn(client, messages, tools: tools_for(mode))
354
- end
355
- end
356
- end
357
-
358
- main