aigency 0.1.0 → 0.3.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 +15 -0
- data/README.md +8 -1
- data/lib/aigency/aigent.rb +83 -44
- data/lib/aigency/plugin.rb +35 -0
- data/lib/aigency/sandbox/Dockerfile.tpl +13 -0
- data/lib/aigency/sandbox/docker-compose.yml.tpl +26 -0
- data/lib/aigency/sandbox/exec.rb +21 -0
- data/lib/aigency/sandbox/squid.conf.tpl +23 -0
- data/lib/aigency/sandbox.rb +21 -0
- data/lib/aigency/shell.rb +10 -3
- data/lib/aigency/version.rb +1 -1
- data/lib/aigency/workspace/cat.rb +11 -0
- data/lib/aigency/workspace/cp.rb +11 -0
- data/lib/aigency/workspace/diff.rb +11 -0
- data/lib/aigency/workspace/grep.rb +15 -0
- data/lib/aigency/workspace/head.rb +11 -0
- data/lib/aigency/workspace/helpers.rb +61 -0
- data/lib/aigency/workspace/ls.rb +11 -0
- data/lib/aigency/workspace/mkdir.rb +11 -0
- data/lib/aigency/workspace/mv.rb +11 -0
- data/lib/aigency/workspace/rm.rb +11 -0
- data/lib/aigency/workspace/shell.rb +59 -0
- data/lib/aigency/workspace/tail.rb +11 -0
- data/lib/aigency/workspace/wc.rb +13 -0
- data/lib/aigency/workspace/write.rb +36 -0
- data/lib/aigency/workspace.rb +23 -5
- data/lib/aigency.rb +2 -1
- data/scaffold/.env.example +4 -0
- data/scaffold/Gemfile +1 -1
- data/scaffold/README.md +31 -19
- data/scaffold/Rakefile +1 -1
- data/scaffold/config/aigent.yml +8 -5
- metadata +79 -7
- data/lib/aigency/tools.rb +0 -55
- data/lib/aigency/workspace/directory_deleter.rb +0 -24
- data/lib/aigency/workspace/directory_reader.rb +0 -34
- data/lib/aigency/workspace/file_deleter.rb +0 -24
- data/lib/aigency/workspace/file_reader.rb +0 -33
- data/lib/aigency/workspace/file_writer.rb +0 -25
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8dc4124a357fd322bcbcd526f42cafc247b5f70e2a367db8bdec26dc2cd5af75
|
|
4
|
+
data.tar.gz: 8cea262865e5e6ffd4b9c1d7efc71b688814363a446684a71ae671bcbe9b316c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a989b935e3554e6cbb081130910b0e81a2c0146507d81ca527024d1f356c419dc8be026d2846ee85fb8233a767a4eef0b2337d1facad1d99b840214042283972
|
|
7
|
+
data.tar.gz: c545ffd5bba322f455931ee5658cfb0c8b72d22d4da07a231214968160d17d109791ae1293ee9564532171327dec405508edb05915173e12c64744a8dbf22216
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## 0.3.0 - October 9, 2026
|
|
2
|
+
- New plugin architecture
|
|
3
|
+
- Workspace and Sandbox plugins
|
|
4
|
+
|
|
5
|
+
## 0.2.0 - October 2, 2026
|
|
6
|
+
- Log token usage
|
|
7
|
+
- Stale code
|
|
8
|
+
- Fix kwarg
|
|
9
|
+
- Log costs
|
|
10
|
+
- DirectoryReader includes hidden files
|
|
11
|
+
- Prohibited path spec
|
|
12
|
+
- Workspace::Helpers
|
|
13
|
+
|
|
14
|
+
## 0.1.0 - September 12, 2026
|
|
15
|
+
- First release
|
data/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A framework for creating AI agents with RubyLLM.
|
|
4
4
|
|
|
5
|
+
## Requierments
|
|
6
|
+
|
|
7
|
+
- Ruby 3.2+
|
|
8
|
+
- Docker 29+
|
|
9
|
+
|
|
10
|
+
(Note: Aigency is not tested on Windows. Some features are known to be Windows-incompatible.)
|
|
11
|
+
|
|
5
12
|
## Installation
|
|
6
13
|
|
|
7
14
|
Install the gem from the command line:
|
|
@@ -38,7 +45,7 @@ rake setup
|
|
|
38
45
|
|
|
39
46
|
The bare minimum configuration you need to do is setting `openai_api_base` and `openai_api_key` in `config/aigent.yml`.
|
|
40
47
|
|
|
41
|
-
Test the aigent with
|
|
48
|
+
Test the aigent with the `rake say` task.
|
|
42
49
|
|
|
43
50
|
```bash
|
|
44
51
|
rake say["How many fibers are intertwined in a shredded wheat biscuit?"]
|
data/lib/aigency/aigent.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'yaml'
|
|
5
|
+
require 'set'
|
|
5
6
|
|
|
6
7
|
module Aigency
|
|
7
8
|
class Aigent
|
|
@@ -38,26 +39,48 @@ module Aigency
|
|
|
38
39
|
# Initialize a RubyLLM::Chat.
|
|
39
40
|
#
|
|
40
41
|
# @param extra [Array<String>] Additional instructions
|
|
42
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
41
43
|
# @return [RubyLLM::Chat]
|
|
42
|
-
def brief(*extra)
|
|
44
|
+
def brief(*extra, except: [])
|
|
43
45
|
chat = rubyllm_context.chat(provider: :openai)
|
|
44
|
-
|
|
46
|
+
(tools - except).each { |klass| chat.with_tool(klass.new(self)) }
|
|
45
47
|
chat.with_instructions(build_instructions(extra), append: true)
|
|
46
|
-
# @todo This should be configurable
|
|
47
48
|
chat.with_params(max_tokens: max_tokens)
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
# Prompt a chat with a file.
|
|
52
|
+
#
|
|
53
|
+
# @param filename [String]
|
|
54
|
+
# @param extra [Array<String>] Additional instructions
|
|
55
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
56
|
+
# @param out [IO]
|
|
57
|
+
# @return [RubyLLM::Chat]
|
|
58
|
+
def load filename, *extra, except: [], out: $stdout
|
|
59
|
+
stream File.read(filename), *extra, except: except, out: out
|
|
52
60
|
end
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
# Prompt a chat.
|
|
63
|
+
#
|
|
64
|
+
# @param text [String]
|
|
65
|
+
# @param extra [Array<String>] Additional instructions
|
|
66
|
+
# @param except [Array<Class<Tool>>] Tools to exclude from this chat
|
|
67
|
+
# @param out [IO]
|
|
68
|
+
# @return [RubyLLM::Chat]
|
|
69
|
+
def stream text, *extra, attach: [], except: [], out: $stdout
|
|
55
70
|
original_log = @log
|
|
56
71
|
@log = out
|
|
57
|
-
brief(*extra).ask text, with:
|
|
72
|
+
response = brief(*extra, except: except).ask text, with: attach do |chunk|
|
|
58
73
|
@log.print chunk.thinking&.text
|
|
59
74
|
@log.print chunk.content
|
|
60
|
-
end
|
|
75
|
+
end
|
|
76
|
+
@log.puts "Input tokens: #{response.tokens.input}"
|
|
77
|
+
@log.puts "Output tokens: #{response.tokens.output}"
|
|
78
|
+
response
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
@log.puts "[#{e.class}] #{e.message}"
|
|
81
|
+
@log.puts e.backtrace
|
|
82
|
+
ensure
|
|
83
|
+
@log = original_log
|
|
61
84
|
end
|
|
62
85
|
|
|
63
86
|
# @param name [String]
|
|
@@ -66,38 +89,11 @@ module Aigency
|
|
|
66
89
|
configs[name] ||= read_config(name)
|
|
67
90
|
end
|
|
68
91
|
|
|
69
|
-
# Initialize the aigent's
|
|
92
|
+
# Initialize the aigent's plugin configurations.
|
|
70
93
|
#
|
|
71
|
-
# @
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
only = [only].flatten.compact
|
|
75
|
-
config_count = 0
|
|
76
|
-
Tools.config_map.each do |name, text|
|
|
77
|
-
next unless only.empty? || only.include?(name)
|
|
78
|
-
|
|
79
|
-
dir = directory('config')
|
|
80
|
-
FileUtils.mkdir_p dir
|
|
81
|
-
path = File.join(dir, "#{name}.yml")
|
|
82
|
-
if overwrite || !File.file?(path)
|
|
83
|
-
doing = File.file?(path) ? 'Overwriting' : 'Writing new'
|
|
84
|
-
log.puts "#{doing} config for #{name}"
|
|
85
|
-
File.write path, text
|
|
86
|
-
config_count += 1
|
|
87
|
-
else
|
|
88
|
-
log.puts "Config for #{name} exists, skipping"
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
dir_count = 0
|
|
92
|
-
Tools.directories.each do |path|
|
|
93
|
-
absolute = directory(path)
|
|
94
|
-
next if File.directory?(absolute)
|
|
95
|
-
|
|
96
|
-
FileUtils.mkdir_p absolute
|
|
97
|
-
dir_count += 1
|
|
98
|
-
end
|
|
99
|
-
log.puts "#{config_count} config files initialized."
|
|
100
|
-
log.puts "#{dir_count} directories created."
|
|
94
|
+
# @return [void]
|
|
95
|
+
def setup
|
|
96
|
+
plugins.each { |plugin| boot plugin }
|
|
101
97
|
end
|
|
102
98
|
|
|
103
99
|
# Run code in an exclusive lock
|
|
@@ -138,8 +134,49 @@ module Aigency
|
|
|
138
134
|
@instructions ||= build_instructions([])
|
|
139
135
|
end
|
|
140
136
|
|
|
137
|
+
# @return [Array<Plugin>]
|
|
138
|
+
def plugins
|
|
139
|
+
config('aigent').fetch('plugins', [])
|
|
140
|
+
.map { |name| Plugin.registry[name] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def tools
|
|
144
|
+
plugins.flat_map(&:tools)
|
|
145
|
+
end
|
|
146
|
+
|
|
141
147
|
private
|
|
142
148
|
|
|
149
|
+
# @param plugin [Plugin]
|
|
150
|
+
def boot(plugin)
|
|
151
|
+
unless plugin.config.empty?
|
|
152
|
+
config_file = File.absolute_path("#{plugin.name}.yml", directory)
|
|
153
|
+
if File.exist?(config_file)
|
|
154
|
+
log.puts "File #{plugin.name}.yml exists, skipping"
|
|
155
|
+
else
|
|
156
|
+
log.puts "Creating #{plugin.name}.yml"
|
|
157
|
+
File.write config_file, plugin.config
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
plugin.directories.each do |dir|
|
|
161
|
+
absolute = File.absolute_path(dir, directory)
|
|
162
|
+
if File.directory?(absolute)
|
|
163
|
+
log.puts "Directory #{dir} exists, skipping"
|
|
164
|
+
else
|
|
165
|
+
log.puts "Creating #{dir}"
|
|
166
|
+
FileUtils.mkdir_p absolute
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
plugin.files.each do |file, content|
|
|
170
|
+
absolute = File.absolute_path(file, directory)
|
|
171
|
+
if File.exist?(absolute)
|
|
172
|
+
log.puts "File #{file} exists, skipping"
|
|
173
|
+
else
|
|
174
|
+
log.puts "Creating #{file}"
|
|
175
|
+
File.write absolute, content
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
143
180
|
def configs
|
|
144
181
|
@configs ||= {}
|
|
145
182
|
end
|
|
@@ -153,7 +190,9 @@ module Aigency
|
|
|
153
190
|
end
|
|
154
191
|
full_path = File.join(directory, 'config', filename)
|
|
155
192
|
if File.file?(full_path)
|
|
156
|
-
|
|
193
|
+
content = File.read(full_path)
|
|
194
|
+
.gsub(/\${([^}]+)}/) { ENV.fetch($1) }
|
|
195
|
+
YAML.load(content)
|
|
157
196
|
else
|
|
158
197
|
warn "Config name #{name} not found at #{full_path}"
|
|
159
198
|
{}
|
|
@@ -180,10 +219,10 @@ module Aigency
|
|
|
180
219
|
end
|
|
181
220
|
|
|
182
221
|
def tool_prompts
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
222
|
+
# @todo Should aigents load prompts from config files that do not have
|
|
223
|
+
# a corresponding plugin?
|
|
224
|
+
plugins.map { |plugin| config(plugin.name)['prompt'] }
|
|
225
|
+
.compact
|
|
187
226
|
end
|
|
188
227
|
|
|
189
228
|
def load_requires
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
class Plugin
|
|
5
|
+
attr_reader :name, :config, :tools, :directories, :files
|
|
6
|
+
|
|
7
|
+
# @param name [String]
|
|
8
|
+
# @param tools [Array<Class<Tool>>]
|
|
9
|
+
# @param config [String]
|
|
10
|
+
# @param directories [Array<String>]
|
|
11
|
+
# @param files [Hash{String => String}]
|
|
12
|
+
def initialize name, tools: [], config: '', directories: [], files: {}
|
|
13
|
+
raise "A plugin named '#{name}' already exists" if self.class.registry.key?(name)
|
|
14
|
+
@name = name
|
|
15
|
+
@config = config
|
|
16
|
+
@tools = tools
|
|
17
|
+
@directories = directories
|
|
18
|
+
@files = files
|
|
19
|
+
self.class.registry[name] = self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.registry
|
|
23
|
+
@registry ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param plugin [Plugin]
|
|
27
|
+
def self.register plugin
|
|
28
|
+
registry[plugin.name] = plugin
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.create(...)
|
|
32
|
+
register new(...)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
dockerfile: Dockerfile
|
|
6
|
+
volumes:
|
|
7
|
+
- ./data/sandbox:/sandbox
|
|
8
|
+
- ./data/sandbox_ssh:/sandbox/.ssh:ro
|
|
9
|
+
working_dir: /sandbox
|
|
10
|
+
command: ["sleep", "infinity"]
|
|
11
|
+
|
|
12
|
+
egress:
|
|
13
|
+
image: ubuntu/squid
|
|
14
|
+
networks: [sandbox, outside]
|
|
15
|
+
volumes:
|
|
16
|
+
- ./squid.conf:/etc/squid/squid.conf:ro
|
|
17
|
+
|
|
18
|
+
networks:
|
|
19
|
+
sandbox:
|
|
20
|
+
internal: true
|
|
21
|
+
driver: bridge
|
|
22
|
+
driver_opts:
|
|
23
|
+
com.docker.network.bridge.gateway_mode_ipv4: isolated
|
|
24
|
+
com.docker.network.bridge.gateway_mode_ipv6: isolated
|
|
25
|
+
outside:
|
|
26
|
+
driver: bridge
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Sandbox
|
|
7
|
+
class Exec < Aigency::Tool
|
|
8
|
+
desc 'Run a shell command in your sandbox'
|
|
9
|
+
|
|
10
|
+
params do
|
|
11
|
+
string :command, description: "The complete command to execute", required: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute(command:)
|
|
15
|
+
aigent.log.puts "[Docker::Compose::Exec] #{command}"
|
|
16
|
+
output, error, result = Open3.capture3('docker', 'compose', 'exec', 'app', 'sh', '-c', command, chdir: aigent.directory)
|
|
17
|
+
{ success: result.success?, stdout: output, stderr: error }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Allowlist-only forward proxy.
|
|
2
|
+
# NOTE: this file deliberately does NOT `include /etc/squid/conf.d/*`, so the
|
|
3
|
+
# rules Ubuntu ships in the image can't silently widen the allowlist.
|
|
4
|
+
|
|
5
|
+
http_port 3128
|
|
6
|
+
visible_hostname egress
|
|
7
|
+
cache deny all # forward proxy only, nothing to disk
|
|
8
|
+
access_log daemon:/var/log/squid/access.log
|
|
9
|
+
logfile_rotate 0
|
|
10
|
+
forwarded_for off # don't leak the container's IP downstream
|
|
11
|
+
|
|
12
|
+
acl SSL_ports port 443
|
|
13
|
+
acl Safe_ports port 80 443
|
|
14
|
+
acl http_targets dstdomain example.com
|
|
15
|
+
|
|
16
|
+
acl SSH_ports port 22
|
|
17
|
+
acl ssh_targets dstdomain example.com
|
|
18
|
+
|
|
19
|
+
http_access allow CONNECT ssh_targets SSH_ports
|
|
20
|
+
http_access deny !Safe_ports
|
|
21
|
+
http_access deny CONNECT !SSL_ports
|
|
22
|
+
http_access allow http_targets
|
|
23
|
+
http_access deny all
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Sandbox
|
|
5
|
+
require 'aigency/sandbox/exec'
|
|
6
|
+
|
|
7
|
+
Plugin.register Plugin.new(
|
|
8
|
+
'sandbox',
|
|
9
|
+
tools: [Sandbox::Exec],
|
|
10
|
+
directories: ['data/sandbox', 'data/sandbox_ssh'],
|
|
11
|
+
files: {
|
|
12
|
+
'Dockerfile' => File.read(File.join(__dir__, 'sandbox', 'Dockerfile.tpl')),
|
|
13
|
+
'docker-compose.yml' => File.read(File.join(__dir__, 'sandbox', 'docker-compose.yml.tpl')),
|
|
14
|
+
'squid.conf' => File.read(File.join(__dir__, 'sandbox', 'squid.conf.tpl'))
|
|
15
|
+
},
|
|
16
|
+
config: <<~FILE
|
|
17
|
+
prompt: "The sandbox tools provide access to a Docker container."
|
|
18
|
+
FILE
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/aigency/shell.rb
CHANGED
|
@@ -7,9 +7,7 @@ module Aigency
|
|
|
7
7
|
class Shell < Thor
|
|
8
8
|
map %i[new] => :enlist
|
|
9
9
|
desc "enlist [DIRECTORY]", "Enable the current user as an aigent"
|
|
10
|
-
|
|
11
|
-
def enlist
|
|
12
|
-
directory = options[:directory]
|
|
10
|
+
def enlist directory = 'aigent'
|
|
13
11
|
puts "Installing aigent at #{directory}"
|
|
14
12
|
if File.exist?(directory)
|
|
15
13
|
if !File.directory?(directory)
|
|
@@ -22,5 +20,14 @@ module Aigency
|
|
|
22
20
|
FileUtils.mkdir(directory) unless File.directory?(directory)
|
|
23
21
|
FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold'), directory)
|
|
24
22
|
end
|
|
23
|
+
|
|
24
|
+
# desc "setup PLUGIN [...PLUGIN]", "Initialize plugin files"
|
|
25
|
+
# option :all, type: :boolean, alias: :a, description: "Set up all plugins", default: false
|
|
26
|
+
# option :overwrite, type: :boolean, alias: :o, description: "Overwrite existing files", default: false
|
|
27
|
+
# def setup *plugins
|
|
28
|
+
# # @todo Assuming all for now
|
|
29
|
+
# aigent = Aigent.new(aigent)
|
|
30
|
+
# aigent.setup
|
|
31
|
+
# end
|
|
25
32
|
end
|
|
26
33
|
end
|
data/lib/aigency/version.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
class Grep < Shell
|
|
6
|
+
desc 'Run the `grep` shell command in your workspace'
|
|
7
|
+
|
|
8
|
+
def command = 'grep'
|
|
9
|
+
|
|
10
|
+
def non_path_arguments = %w[PATTERN]
|
|
11
|
+
|
|
12
|
+
def prohibited_arguments = %w[-f --file]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aigency
|
|
4
|
+
module Workspace
|
|
5
|
+
module Helpers
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @!method aigent
|
|
9
|
+
# @abstract
|
|
10
|
+
# @return [Aigency::Aigent]
|
|
11
|
+
|
|
12
|
+
# Expand a path relative to the aigent's workspace.
|
|
13
|
+
#
|
|
14
|
+
# @raise [PathError] if the expanded path is not inside the workspace
|
|
15
|
+
# @param path [String]
|
|
16
|
+
# @return [String]
|
|
17
|
+
def expand_in_workspace(path)
|
|
18
|
+
workspace = aigent.data('workspace')
|
|
19
|
+
resolved = File.expand_path(path, workspace)
|
|
20
|
+
return resolved if resolved == workspace || resolved.start_with?("#{workspace}/")
|
|
21
|
+
raise PathError, "Permission denied: `#{path}` is not relative to your workspace"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @raise [PathError] if the expanded path is not inside the workspace
|
|
25
|
+
# @param arguments [Array<String>]
|
|
26
|
+
# @param start [Integer]
|
|
27
|
+
# @return [void]
|
|
28
|
+
def validate_path_arguments(arguments, start = 0)
|
|
29
|
+
current = 0
|
|
30
|
+
glued_shorts = arguments.select { |arg| arg =~ /^-[^\-].+/ }
|
|
31
|
+
unless glued_shorts.empty?
|
|
32
|
+
raise PathError, "Permission denied: glued short options (#{glued_shorts.inspect}) are not allowed"
|
|
33
|
+
end
|
|
34
|
+
workspace = aigent.data('workspace')
|
|
35
|
+
path_arguments(arguments).each do |arg|
|
|
36
|
+
if !arg.start_with?('-')
|
|
37
|
+
current += 1
|
|
38
|
+
next unless current > start
|
|
39
|
+
end
|
|
40
|
+
path = split_path_part(arg)
|
|
41
|
+
resolved = File.expand_path(path, workspace)
|
|
42
|
+
next if resolved == workspace || resolved.start_with?("#{workspace}/")
|
|
43
|
+
raise PathError, "Permission denied: `#{path}` is not relative to your workspace"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def path_arguments(arguments)
|
|
48
|
+
arguments.reject { |arg| arg =~ /^-[^\-]$/ || (arg.start_with?('--') && !arg.include?('=')) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def split_path_part argument
|
|
52
|
+
return argument unless argument.start_with?('-')
|
|
53
|
+
if argument.start_with?('--')
|
|
54
|
+
argument.split('=', 2).last
|
|
55
|
+
else
|
|
56
|
+
argument[2..]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Workspace
|
|
7
|
+
class Shell < Tool
|
|
8
|
+
include Helpers
|
|
9
|
+
|
|
10
|
+
# @abstract
|
|
11
|
+
# @return [String]
|
|
12
|
+
def command
|
|
13
|
+
raise 'Not implemented'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def non_path_arguments = []
|
|
17
|
+
|
|
18
|
+
def prohibited_arguments = []
|
|
19
|
+
|
|
20
|
+
def execute(directory: '.', arguments: [])
|
|
21
|
+
aigent.log.puts "[Workspace::Shell] #{command} #{arguments.inspect}"
|
|
22
|
+
if any_prohibited?(arguments)
|
|
23
|
+
return { success: false, error: "Permission denied: the following arguments to `#{command}` are prohibited: #{prohibited_arguments.inspect}" }
|
|
24
|
+
end
|
|
25
|
+
resolved = expand_in_workspace(directory)
|
|
26
|
+
if arguments != ['--help'] && arguments[0, non_path_arguments.length].any? { |arg| arg.start_with?('-') }
|
|
27
|
+
return { success: false, error: non_path_argument_error }
|
|
28
|
+
end
|
|
29
|
+
validate_path_arguments(arguments, non_path_arguments.length)
|
|
30
|
+
output, error, result = Open3.capture3(command, *arguments, chdir: resolved)
|
|
31
|
+
{ success: result.success?, stdout: output, stderr: error }
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.inherited other
|
|
37
|
+
other.params do
|
|
38
|
+
string :directory, description: "The working directory relative to workspace", required: false
|
|
39
|
+
array :arguments, of: :string, description: "Command arguments", required: false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def non_path_argument_error
|
|
46
|
+
arg_count = if non_path_arguments.length > 1
|
|
47
|
+
"#{non_path_arguments.length} arguments"
|
|
48
|
+
else
|
|
49
|
+
'argument'
|
|
50
|
+
end
|
|
51
|
+
"Permission denied: first #{arg_count} must be [#{non_path_arguments.join(', ')}] unless the only argument is `--help`"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def any_prohibited?(arguments)
|
|
55
|
+
arguments.any? { |arg| arg.start_with?(*prohibited_arguments) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Aigency
|
|
6
|
+
module Workspace
|
|
7
|
+
# @note Unlike most of the Workspace tools, this one does not execute a shell
|
|
8
|
+
# command. It uses ruby's File module instead.
|
|
9
|
+
#
|
|
10
|
+
class Write < Tool
|
|
11
|
+
include Helpers
|
|
12
|
+
|
|
13
|
+
desc 'Write or append to a file in the workspace'
|
|
14
|
+
|
|
15
|
+
params do
|
|
16
|
+
string :filename, description: "The filename relative to the workspace", required: true
|
|
17
|
+
string :content, description: "The data to write to the file", required: true
|
|
18
|
+
string :append, description: "Append if true or overwrite if false", required: true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def execute(filename:, content:, append:)
|
|
22
|
+
aigent.log.puts "[Workspace::Write] #{filename}"
|
|
23
|
+
resolved = expand_in_workspace(filename)
|
|
24
|
+
write_mode = if append
|
|
25
|
+
"a"
|
|
26
|
+
else
|
|
27
|
+
"w"
|
|
28
|
+
end
|
|
29
|
+
File.write resolved, content, mode: "#{write_mode}b"
|
|
30
|
+
{ success: true, filename: filename }
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
{ success: false, error: "[#{e.class}] #{e.message}" }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/aigency/workspace.rb
CHANGED
|
@@ -2,10 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Aigency
|
|
4
4
|
module Workspace
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
require 'aigency/workspace/
|
|
8
|
-
|
|
5
|
+
class PathError < ArgumentError; end
|
|
6
|
+
|
|
7
|
+
require 'aigency/workspace/helpers'
|
|
8
|
+
require 'aigency/workspace/shell'
|
|
9
|
+
require 'aigency/workspace/cat'
|
|
10
|
+
require 'aigency/workspace/cp'
|
|
11
|
+
require 'aigency/workspace/diff'
|
|
12
|
+
require 'aigency/workspace/grep'
|
|
13
|
+
require 'aigency/workspace/ls'
|
|
14
|
+
require 'aigency/workspace/mkdir'
|
|
15
|
+
require 'aigency/workspace/mv'
|
|
16
|
+
require 'aigency/workspace/rm'
|
|
17
|
+
require 'aigency/workspace/wc'
|
|
18
|
+
require 'aigency/workspace/write'
|
|
9
19
|
|
|
10
|
-
|
|
20
|
+
Plugin.register Plugin.new(
|
|
21
|
+
'workspace',
|
|
22
|
+
directories: ['data/workspace'],
|
|
23
|
+
tools: [Cat, Cp, Diff, Grep, Ls, Mkdir, Mv, Rm, Wc, Write],
|
|
24
|
+
config: <<~FILE
|
|
25
|
+
prompt: "The workspace tools provide access to a readable and writeable directory."
|
|
26
|
+
FILE
|
|
27
|
+
)
|
|
28
|
+
end
|
|
11
29
|
end
|
data/lib/aigency.rb
CHANGED
|
@@ -6,12 +6,13 @@ require 'ruby_llm'
|
|
|
6
6
|
require 'aigency/version'
|
|
7
7
|
require 'aigency/usability'
|
|
8
8
|
require 'aigency/tool'
|
|
9
|
-
require 'aigency/tools'
|
|
10
9
|
require 'aigency/aigent'
|
|
10
|
+
require 'aigency/plugin'
|
|
11
11
|
require 'aigency/shell'
|
|
12
12
|
|
|
13
13
|
# Extensions
|
|
14
14
|
require 'aigency/workspace'
|
|
15
|
+
require 'aigency/sandbox'
|
|
15
16
|
|
|
16
17
|
module Aigency
|
|
17
18
|
GEM_PATH = File.absolute_path(File.join(__dir__, '..'))
|
data/scaffold/Gemfile
CHANGED
data/scaffold/README.md
CHANGED
|
@@ -4,27 +4,23 @@ An aigent (AI agent) is an intelligent assistant powered by language models thro
|
|
|
4
4
|
|
|
5
5
|
## Configuration
|
|
6
6
|
|
|
7
|
-
### Install dependencies
|
|
7
|
+
### 1. Install dependencies
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
bundle install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### 2. Configure OpenAI API Connection
|
|
14
|
+
|
|
15
|
+
Copy the example env file:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
18
|
+
cp .env.example .env
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Configure the aigent's settings in `config/aigent.yml`.
|
|
22
|
-
|
|
23
|
-
#### OpenAI-Compatible Providers
|
|
24
|
-
|
|
25
|
-
If you're using a local AI server or any other provider that is compatible with OpenAI, you can set the OPENAI_API_BASE config option to point there instead (e.g., `http://localhost:8080/v1`).
|
|
21
|
+
Modify the `OPENAI_API_BASE` and `OPENAI_API_KEY` as needed. If you're using a local AI server or any other provider that is compatible with OpenAI, you can set the OPENAI_API_BASE config option to point there instead (e.g., `http://localhost:8080/v1`).
|
|
26
22
|
|
|
27
|
-
###
|
|
23
|
+
### 3. Define the Aigent's Personality
|
|
28
24
|
|
|
29
25
|
Edit `IDENTITY.md` to define how your aigent should behave:
|
|
30
26
|
|
|
@@ -34,6 +30,22 @@ You are a professional assistant named Alex. You respond in a friendly but conci
|
|
|
34
30
|
|
|
35
31
|
This file sets the foundation for all the aigent's responses.
|
|
36
32
|
|
|
33
|
+
### 4. Configure Plugins
|
|
34
|
+
|
|
35
|
+
Aigency ships with two plugins: `workspace` and `sandbox`. The `workspace` plugin is enabled by default. It's recommended that you use one or the other, but not both.
|
|
36
|
+
|
|
37
|
+
The `workspace` plugin gives your aigent a directory where it can manage its own files.
|
|
38
|
+
|
|
39
|
+
The `sandbox` plugin provides a container where the aigent can not only manage files, but also run commands and scripts.
|
|
40
|
+
|
|
41
|
+
The `workspace` plugin is sufficient for many aigents. Switch to `sandbox` if you need your aigent to perform complex tasks.
|
|
42
|
+
|
|
43
|
+
### 5. Run setup
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
rake setup
|
|
47
|
+
```
|
|
48
|
+
|
|
37
49
|
## Usage
|
|
38
50
|
|
|
39
51
|
### Interactive Mode
|
|
@@ -41,16 +53,12 @@ This file sets the foundation for all the aigent's responses.
|
|
|
41
53
|
Talk to your aigent using the Rake task:
|
|
42
54
|
|
|
43
55
|
```bash
|
|
44
|
-
rake say["
|
|
56
|
+
rake say["How many fibers are intertwined in a shredded wheat biscuit?"]
|
|
45
57
|
```
|
|
46
58
|
|
|
47
|
-
### The Workspace
|
|
48
|
-
|
|
49
|
-
Your aigent has read/write access to everything in the `data/workspace` directory.
|
|
50
|
-
|
|
51
59
|
### Scheduling Tasks
|
|
52
60
|
|
|
53
|
-
|
|
61
|
+
Aigents use the `whenever` gem for scheduling. Example of a scheduled task:
|
|
54
62
|
|
|
55
63
|
```ruby
|
|
56
64
|
# config/schedule.rb
|
|
@@ -69,6 +77,10 @@ bundle exec whenever --update-crontab
|
|
|
69
77
|
|
|
70
78
|
(Note: the `whenever` gem does not work on Windows.)
|
|
71
79
|
|
|
72
|
-
|
|
80
|
+
### Using the Sandbox
|
|
81
|
+
|
|
82
|
+
If you enable the `sandbox` plugin, you'll need to start the container:
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
```bash
|
|
85
|
+
docker compose up -d
|
|
86
|
+
```
|
data/scaffold/Rakefile
CHANGED
data/scaffold/config/aigent.yml
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
name:
|
|
2
|
-
openai_api_base:
|
|
3
|
-
openai_api_key:
|
|
1
|
+
name: Aigent
|
|
2
|
+
openai_api_base: ${OPENAI_API_BASE}
|
|
3
|
+
openai_api_key: ${OPENAI_API_KEY}
|
|
4
4
|
# max_tokens: 65536
|
|
5
|
-
|
|
6
|
-
#
|
|
5
|
+
plugins:
|
|
6
|
+
# You should only need either a workspace or a sandbox, not both. If you
|
|
7
|
+
# enable one, it's recommended that you disable the other.
|
|
8
|
+
# - sandbox
|
|
9
|
+
- workspace
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aigency
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fred Snyder
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: dotenv
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.2'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: fileutils
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +65,48 @@ dependencies:
|
|
|
51
65
|
- - "~>"
|
|
52
66
|
- !ruby/object:Gem::Version
|
|
53
67
|
version: '1.5'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rspec
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: simplecov
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.21'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.21'
|
|
54
110
|
email:
|
|
55
111
|
- fsnyder@castwide.com
|
|
56
112
|
executables:
|
|
@@ -58,22 +114,38 @@ executables:
|
|
|
58
114
|
extensions: []
|
|
59
115
|
extra_rdoc_files: []
|
|
60
116
|
files:
|
|
117
|
+
- CHANGELOG.md
|
|
61
118
|
- README.md
|
|
62
119
|
- Rakefile
|
|
63
120
|
- exe/aigency
|
|
64
121
|
- lib/aigency.rb
|
|
65
122
|
- lib/aigency/aigent.rb
|
|
123
|
+
- lib/aigency/plugin.rb
|
|
124
|
+
- lib/aigency/sandbox.rb
|
|
125
|
+
- lib/aigency/sandbox/Dockerfile.tpl
|
|
126
|
+
- lib/aigency/sandbox/docker-compose.yml.tpl
|
|
127
|
+
- lib/aigency/sandbox/exec.rb
|
|
128
|
+
- lib/aigency/sandbox/squid.conf.tpl
|
|
66
129
|
- lib/aigency/shell.rb
|
|
67
130
|
- lib/aigency/tool.rb
|
|
68
|
-
- lib/aigency/tools.rb
|
|
69
131
|
- lib/aigency/usability.rb
|
|
70
132
|
- lib/aigency/version.rb
|
|
71
133
|
- lib/aigency/workspace.rb
|
|
72
|
-
- lib/aigency/workspace/
|
|
73
|
-
- lib/aigency/workspace/
|
|
74
|
-
- lib/aigency/workspace/
|
|
75
|
-
- lib/aigency/workspace/
|
|
76
|
-
- lib/aigency/workspace/
|
|
134
|
+
- lib/aigency/workspace/cat.rb
|
|
135
|
+
- lib/aigency/workspace/cp.rb
|
|
136
|
+
- lib/aigency/workspace/diff.rb
|
|
137
|
+
- lib/aigency/workspace/grep.rb
|
|
138
|
+
- lib/aigency/workspace/head.rb
|
|
139
|
+
- lib/aigency/workspace/helpers.rb
|
|
140
|
+
- lib/aigency/workspace/ls.rb
|
|
141
|
+
- lib/aigency/workspace/mkdir.rb
|
|
142
|
+
- lib/aigency/workspace/mv.rb
|
|
143
|
+
- lib/aigency/workspace/rm.rb
|
|
144
|
+
- lib/aigency/workspace/shell.rb
|
|
145
|
+
- lib/aigency/workspace/tail.rb
|
|
146
|
+
- lib/aigency/workspace/wc.rb
|
|
147
|
+
- lib/aigency/workspace/write.rb
|
|
148
|
+
- scaffold/.env.example
|
|
77
149
|
- scaffold/.gitignore
|
|
78
150
|
- scaffold/Gemfile
|
|
79
151
|
- scaffold/IDENTITY.md
|
data/lib/aigency/tools.rb
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Tools
|
|
5
|
-
# @return [Array<Class<Tool>>]
|
|
6
|
-
def self.registry
|
|
7
|
-
@registry ||= []
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# @return [Hash{String => String}]
|
|
11
|
-
def self.config_map
|
|
12
|
-
@configs ||= {}
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# @return [Array<String>]
|
|
16
|
-
def self.directories
|
|
17
|
-
@directories ||= []
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Register a tool.
|
|
21
|
-
#
|
|
22
|
-
# @param klass [Class<Tool>]
|
|
23
|
-
# @return [void]
|
|
24
|
-
def self.register klass
|
|
25
|
-
registry.push klass
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Define a default configuration.
|
|
29
|
-
#
|
|
30
|
-
# @param name [String] The name of the config section
|
|
31
|
-
# @param text [String] YAML-formatted config data
|
|
32
|
-
# @return [void]
|
|
33
|
-
def self.configure name, text
|
|
34
|
-
config_map[name] = text
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Define directories to be created relative to an aigent's root.
|
|
38
|
-
#
|
|
39
|
-
def self.mkdir_p *paths
|
|
40
|
-
directories.concat paths
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
Tools.configure 'aigent', <<~EOF
|
|
45
|
-
name: an Aigent
|
|
46
|
-
openai_api_base: https://api.openai.com/v1
|
|
47
|
-
openai_api_key: CHANGE_ME
|
|
48
|
-
require:
|
|
49
|
-
- aigency/email
|
|
50
|
-
- aigency/wikipedia
|
|
51
|
-
- aigency/workspace
|
|
52
|
-
EOF
|
|
53
|
-
|
|
54
|
-
Tools.mkdir_p 'data/chats', 'data/log'
|
|
55
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class DirectoryDeleter < Tool
|
|
6
|
-
desc "Deletes a directory from your private workspace"
|
|
7
|
-
|
|
8
|
-
def execute(dirname:)
|
|
9
|
-
puts "Deleting directory #{dirname}"
|
|
10
|
-
|
|
11
|
-
resolved = File.absolute_path(dirname, aigent.data('workspace'))
|
|
12
|
-
return { dirname: dirname, success: false, error: "Permission denied" } unless resolved.start_with?(aigent.data('workspace'))
|
|
13
|
-
return { dirname: dirname, success: false, error: "Permission denied" } if resolved.include?('/.git/') || resolved.end_with?('/.git')
|
|
14
|
-
return { dirname: dirname, success: false, error: "Not a directory" } if File.file?(resolved)
|
|
15
|
-
return { dirname: dirname, sucess: false, error: "Directory not found" } unless File.directory?(resolved)
|
|
16
|
-
|
|
17
|
-
FileUtils.rm_rf(resolved)
|
|
18
|
-
{ dirname: dirname, success: true }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
Aigency::Tools.register Aigency::Workspace::DirectoryDeleter
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class DirectoryReader < Tool
|
|
6
|
-
desc "Gets the contents of a directory from your private workspace"
|
|
7
|
-
|
|
8
|
-
def execute(directory_name:)
|
|
9
|
-
aigent.log.puts "[Workspace::DirectoryReader] Reading #{directory_name}"
|
|
10
|
-
directory = aigent.data('workspace')
|
|
11
|
-
resolved = File.absolute_path(directory_name, directory)
|
|
12
|
-
return { directory: directory_name,
|
|
13
|
-
success: false,
|
|
14
|
-
error: "Permission denied. Path must be local to #{directory}" } unless resolved.start_with?(directory)
|
|
15
|
-
|
|
16
|
-
if File.directory?(resolved)
|
|
17
|
-
{
|
|
18
|
-
directory: directory_name,
|
|
19
|
-
success: true,
|
|
20
|
-
entries: Dir[File.join(resolved, '*')].map { |dir| dir[resolved.length+1..]}
|
|
21
|
-
}
|
|
22
|
-
else
|
|
23
|
-
{
|
|
24
|
-
directory: directory_name,
|
|
25
|
-
success: false,
|
|
26
|
-
error: "Not a directory"
|
|
27
|
-
}
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
Aigency::Tools.register Aigency::Workspace::DirectoryReader
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class FileDeleter < Tool
|
|
6
|
-
desc "Deletes a file from your private workspace"
|
|
7
|
-
|
|
8
|
-
def execute(filename:)
|
|
9
|
-
aigent.log.puts "[Workspace::FileDeleter] Deleting file #{filename}"
|
|
10
|
-
|
|
11
|
-
directory = aigent.data('directory')
|
|
12
|
-
resolved = File.absolute_path(filename, directory)
|
|
13
|
-
return { filename: filename, error: "Permission denied" } unless resolved.start_with?(directory)
|
|
14
|
-
return { filename: filename, error: "Permission denied" } if resolved.include?('/.git/')
|
|
15
|
-
return { filename: filename, error: "File does not exist" } unless File.file?(resolved)
|
|
16
|
-
|
|
17
|
-
File.delete resolved
|
|
18
|
-
{ filename: filename, success: true }
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
Aigency::Tools.register Aigency::Workspace::FileDeleter
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Aigency
|
|
4
|
-
module Workspace
|
|
5
|
-
class FileReader < Tool
|
|
6
|
-
desc "Reads a file from your private workspace"
|
|
7
|
-
|
|
8
|
-
def execute(filename:)
|
|
9
|
-
aigent.log.puts "[Workspace::FileReader] Reading #{filename}"
|
|
10
|
-
|
|
11
|
-
directory = aigent.data('workspace')
|
|
12
|
-
resolved = File.absolute_path(filename, directory)
|
|
13
|
-
return { filename: filename, success: false, error: "Permission denied" } unless resolved.start_with?(directory)
|
|
14
|
-
|
|
15
|
-
if File.file?(resolved)
|
|
16
|
-
{
|
|
17
|
-
filename: filename,
|
|
18
|
-
success: true,
|
|
19
|
-
content: File.read(resolved)
|
|
20
|
-
}
|
|
21
|
-
else
|
|
22
|
-
{
|
|
23
|
-
filename: filename,
|
|
24
|
-
success: false,
|
|
25
|
-
error: "File not found"
|
|
26
|
-
}
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
Aigency::Tools.register Aigency::Workspace::FileReader
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
|
|
5
|
-
module Aigency
|
|
6
|
-
module Workspace
|
|
7
|
-
class FileWriter < Tool
|
|
8
|
-
desc "Writes a file to your private workspace"
|
|
9
|
-
|
|
10
|
-
def execute(filename:, content:)
|
|
11
|
-
aigent.log.puts "[Workspace::FileWriter] Writing #{filename}"
|
|
12
|
-
|
|
13
|
-
directory = aigent.data('workspace')
|
|
14
|
-
resolved = File.absolute_path(filename, directory)
|
|
15
|
-
return { filename: filename, success: false, error: "Permission denied" } unless resolved.start_with?(directory)
|
|
16
|
-
|
|
17
|
-
FileUtils.mkdir_p File.dirname(resolved)
|
|
18
|
-
File.write resolved, content
|
|
19
|
-
return { filename: filename, success: true }
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
Aigency::Tools.register Aigency::Workspace::FileWriter
|