aigency 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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +7 -15
  4. data/lib/aigency/aigent/current_time.rb +33 -0
  5. data/lib/aigency/aigent/shell.rb +108 -0
  6. data/lib/aigency/aigent.rb +52 -35
  7. data/lib/aigency/delegator.rb +11 -0
  8. data/lib/aigency/plugin/shell.rb +11 -0
  9. data/lib/aigency/plugin.rb +6 -2
  10. data/lib/aigency/rake.rb +13 -0
  11. data/lib/aigency/sandbox/exec.rb +2 -2
  12. data/lib/aigency/sandbox.rb +5 -4
  13. data/lib/aigency/shell.rb +4 -11
  14. data/lib/aigency/tempdir/helpers.rb +15 -0
  15. data/lib/aigency/tempdir/list.rb +17 -0
  16. data/lib/aigency/tempdir/read.rb +32 -0
  17. data/lib/aigency/tempdir/size.rb +27 -0
  18. data/lib/aigency/tempdir/write.rb +28 -0
  19. data/lib/aigency/tempdir.rb +19 -0
  20. data/lib/aigency/tool.rb +5 -0
  21. data/lib/aigency/usability.rb +1 -0
  22. data/lib/aigency/version.rb +1 -1
  23. data/lib/aigency/workspace/cat.rb +2 -2
  24. data/lib/aigency/workspace/{shell.rb → cmd.rb} +2 -2
  25. data/lib/aigency/workspace/copy_tempdir_file.rb +28 -0
  26. data/lib/aigency/workspace/cp.rb +2 -2
  27. data/lib/aigency/workspace/diff.rb +2 -2
  28. data/lib/aigency/workspace/grep.rb +2 -2
  29. data/lib/aigency/workspace/head.rb +2 -2
  30. data/lib/aigency/workspace/ls.rb +2 -2
  31. data/lib/aigency/workspace/mkdir.rb +2 -2
  32. data/lib/aigency/workspace/mv.rb +2 -2
  33. data/lib/aigency/workspace/rm.rb +2 -2
  34. data/lib/aigency/workspace/tail.rb +2 -2
  35. data/lib/aigency/workspace/wc.rb +2 -2
  36. data/lib/aigency/workspace/write.rb +2 -2
  37. data/lib/aigency/workspace.rb +3 -4
  38. data/lib/aigency.rb +16 -14
  39. data/scaffold/{Rakefile → aigent/aigent.rake} +1 -10
  40. data/scaffold/{.gitignore → enlist/.gitignore} +1 -0
  41. data/scaffold/{README.md → enlist/README.md} +7 -3
  42. data/scaffold/enlist/Rakefile +9 -0
  43. data/scaffold/enlist/bin/aigent +11 -0
  44. data/scaffold/{config → enlist/config}/schedule.rb +5 -0
  45. data/scaffold/sandbox/Dockerfile +37 -0
  46. data/scaffold/sandbox/docker-compose.yml +37 -0
  47. data/{lib/aigency/sandbox/squid.conf.tpl → scaffold/sandbox/squid.conf} +4 -2
  48. data/scaffold/sandbox/ssh_config +2 -0
  49. metadata +43 -15
  50. data/lib/aigency/sandbox/Dockerfile.tpl +0 -13
  51. data/lib/aigency/sandbox/docker-compose.yml.tpl +0 -26
  52. data/scaffold/config/aigent.yml +0 -9
  53. /data/scaffold/{.env.example → enlist/.env.example} +0 -0
  54. /data/scaffold/{Gemfile → enlist/Gemfile} +0 -0
  55. /data/scaffold/{IDENTITY.md → enlist/IDENTITY.md} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dc4124a357fd322bcbcd526f42cafc247b5f70e2a367db8bdec26dc2cd5af75
4
- data.tar.gz: 8cea262865e5e6ffd4b9c1d7efc71b688814363a446684a71ae671bcbe9b316c
3
+ metadata.gz: 4fce24d7da34c83fac5e0d3ce82d8bb79c31bede2b99ddfacec3b7efa4d157e3
4
+ data.tar.gz: 1a36424698a8a9b4bba0a0a0cf6a14e9b4cf8318a40a166893be3e093c909f9e
5
5
  SHA512:
6
- metadata.gz: a989b935e3554e6cbb081130910b0e81a2c0146507d81ca527024d1f356c419dc8be026d2846ee85fb8233a767a4eef0b2337d1facad1d99b840214042283972
7
- data.tar.gz: c545ffd5bba322f455931ee5658cfb0c8b72d22d4da07a231214968160d17d109791ae1293ee9564532171327dec405508edb05915173e12c64744a8dbf22216
6
+ metadata.gz: 9ce7989e062eb10afbb767d3e61f297ddf521f0b7c5fe0459c8021428843e6b05069db1ae88afbda7c29ab4ea0ca9f9f277b7472393f085f87dc5b36866cd786
7
+ data.tar.gz: 9a7a1efaad6467200964715b3db7920b52cc1cc39c7292fd8a52a2b4d4d023b16502ea511ee6824664970fecbdab19b2529dd95a2a5d0a8dfc49f9c53a5e062d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## 0.4.0 - October 21, 2026
2
+ - Config file directory
3
+ - Add openssh to sandbox
4
+ - Sandbox configuration
5
+ - Tempdir tools
6
+ - Aigent plugin
7
+ - Proxy environment variables
8
+ - apk update && apk upgrade
9
+ - Plugins add rake files
10
+ - Aigent setup in README
11
+ - Rearrange sandbox scaffold
12
+ - Add bin/aigent to aigent directory
13
+ - Fix grep spec for BusyBox
14
+ - CurrentTime tool
15
+ - Dockerfile packages and mise setup
16
+ - Instructions subcommand
17
+ - Aigent#stream errors write to error.log
18
+ - Aigent stream command
19
+ - Remove adduser recommendation
20
+ - Clarify Docker requirement
21
+ - Plugin shells
22
+ - Tempdir file size management
23
+ - CopyTempdirFile tool
24
+ - Aigent plugin setup
25
+ - Update chat calls for RubyLLM 2.0
26
+ - Use the chat completions protocol
27
+
1
28
  ## 0.3.0 - October 9, 2026
2
29
  - New plugin architecture
3
30
  - Workspace and Sandbox plugins
data/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  A framework for creating AI agents with RubyLLM.
4
4
 
5
- ## Requierments
5
+ ## Requirements
6
6
 
7
7
  - Ruby 3.2+
8
- - Docker 29+
8
+ - Docker 29+ (required for sandbox)
9
9
 
10
10
  (Note: Aigency is not tested on Windows. Some features are known to be Windows-incompatible.)
11
11
 
@@ -27,28 +27,20 @@ gem 'aigency'
27
27
 
28
28
  ### Creating a New Aigent
29
29
 
30
- The recommended way to create an aigent is to give it a dedicated system user.
31
-
32
- ```bash
33
- useradd aigent_user
34
- sudo su - aigent_user
35
- ```
36
-
37
- Once you're logged in as the aigent user, create the aigent directory:
38
-
39
30
  ```bash
40
31
  aigency enlist
41
32
  cd aigent
42
33
  bundle install
43
34
  rake setup
35
+ cp .env.example .env
44
36
  ```
45
37
 
46
- The bare minimum configuration you need to do is setting `openai_api_base` and `openai_api_key` in `config/aigent.yml`.
38
+ The bare minimum configuration you need to do is setting `OPENAI_API_BASE` and `OPENAI_API_KEY` in `.env`.
47
39
 
48
- Test the aigent with the `rake say` task.
40
+ Test the aigent with the `say` command.
49
41
 
50
42
  ```bash
51
- rake say["How many fibers are intertwined in a shredded wheat biscuit?"]
43
+ bin/aigent say "How many fibers are intertwined in a shredded wheat biscuit?"
52
44
  ```
53
45
 
54
- See the aigent's README (scaffold/README.md in this codebase) for more information.
46
+ See the aigent's README for more information (`README.md` in your aigent directory or `scaffold/enlist/README.md` in this codebase).
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Aigency
6
+ class Aigent
7
+ class CurrentTime < Aigency::Tool
8
+ description 'Get the current time'
9
+
10
+ parameters do
11
+ any_of :format, description: "Date/time format" do
12
+ string enum: %w[iso8601 unix]
13
+ 'iso8601'
14
+ end
15
+ end
16
+
17
+ def execute(format: 'iso8601')
18
+ aigent.log.puts "[Aigent::CurrentTime] #{format}"
19
+ now = DateTime.now
20
+ case format
21
+ when 'iso8601'
22
+ { success: true, current_time: now.iso8601 }
23
+ when 'unix'
24
+ { success: true, current_time: now.to_time.to_i }
25
+ else
26
+ { success: false, error: "Invalid format: #{format}" }
27
+ end
28
+ rescue StandardError => e
29
+ { success: false, error: "[#{e.class}] #{e.message}"}
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Aigency
6
+ class Aigent
7
+ # Commands that can be executed in aigent directories.
8
+ #
9
+ class Shell < Thor
10
+ include Delegator
11
+
12
+ desc "say PROMPT", "Send a prompt to the aigent"
13
+ def say(prompt)
14
+ aigent.stream prompt
15
+ end
16
+
17
+ desc "load FILE", "Load a prompt from a file"
18
+ def load(file)
19
+ aigent.load filename
20
+ end
21
+
22
+ desc "setup", "Initialize aigent plugins"
23
+ def setup
24
+ aigent.setup
25
+ end
26
+
27
+ desc "tools", "Get a list of available tools"
28
+ def tools
29
+ puts aigent.tools.map(&:to_s).sort
30
+ end
31
+
32
+ desc "status", "Get the aigent's current status"
33
+ def status
34
+ puts "Name: #{aigent.config('aigent')['name']}"
35
+ puts "Busy: #{aigent.busy?}"
36
+ if aigent.busy?
37
+ puts "Process: #{File.read(aigent.data('lock'))}"
38
+ end
39
+ end
40
+
41
+ desc "instructions", "List base instructions"
42
+ long_desc <<~DESC
43
+ Show all instructions that will be given to the aigent before the user
44
+ prompt. They typically include the IDENTITY.md file and any prompts injected
45
+ by installed plugins.
46
+ DESC
47
+ def instructions
48
+ puts aigent.instructions
49
+ end
50
+
51
+ desc "stream", "Tail the currently active chat stream"
52
+ option :follow, type: :boolean, aliases: :f, desc: "Follow the stream (tail -f)"
53
+ option :lines, type: :numeric, aliases: :n, desc: "Number of lines (tail -n)", default: 10
54
+ long_desc <<~DESC
55
+ This is useful for monitoring non-interactive chat streams, e.g., chat sessions
56
+ that were started by cron jobs or rake tasks. It runs `tail` on the current chat
57
+ file in the aigent's data/chats folder.
58
+ DESC
59
+ def stream
60
+ if aigent.busy?
61
+ file = Dir.glob(aigent.data('chats', '*.txt')).max { |file| File.mtime(file).to_i }
62
+ cmd = ['tail', file, '-n', options[:lines].to_s]
63
+ cmd.push '-f' if options[:follow]
64
+ puts "(#{file})"
65
+ exec *cmd
66
+ else
67
+ puts "No active chat stream."
68
+ end
69
+ end
70
+
71
+ desc "kill", "Kill a blocking process"
72
+ long_desc <<~DESC
73
+ Kill the process that owns an exclusive lock on the aigent's data/lock file.
74
+ DESC
75
+ def kill
76
+ if aigent.busy?
77
+ pid = File.read(aigent.data('lock')).to_i
78
+ puts "Killing process #{pid}"
79
+ Process.kill 'TERM', pid
80
+ else
81
+ puts "No blocking process exists."
82
+ end
83
+ end
84
+
85
+ class << self
86
+ def mounted? = false
87
+
88
+ def start(args = ARGV, config = {})
89
+ return super if mounted?
90
+ build('.').start(args, config)
91
+ end
92
+
93
+ def build(directory)
94
+ Class.new(self) do
95
+ def self.mounted? = true
96
+ Aigent.new(directory).plugins.each do |plugin|
97
+ next unless plugin.shell
98
+ if all_commands.key?(plugin.name)
99
+ raise "Plugin '#{plugin.name}' cannot register CLI command '#{plugin.name}': already taken"
100
+ end
101
+ register(plugin.shell, plugin.name, "#{plugin.name} [SUBCOMMAND ...ARGS]", "#{plugin.name} plugin commands")
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -6,13 +6,15 @@ require 'set'
6
6
 
7
7
  module Aigency
8
8
  class Aigent
9
+ require 'aigency/aigent/current_time'
10
+ require 'aigency/aigent/shell'
11
+
9
12
  # @return [IO]
10
13
  attr_reader :log
11
14
 
12
15
  def initialize(directory)
13
16
  @base_directory = File.absolute_path(directory)
14
17
  @log = $stdout
15
- load_requires
16
18
  end
17
19
 
18
20
  # @return [String]
@@ -42,10 +44,10 @@ module Aigency
42
44
  # @param except [Array<Class<Tool>>] Tools to exclude from this chat
43
45
  # @return [RubyLLM::Chat]
44
46
  def brief(*extra, except: [])
45
- chat = rubyllm_context.chat(provider: :openai)
46
- (tools - except).each { |klass| chat.with_tool(klass.new(self)) }
47
- chat.with_instructions(build_instructions(extra), append: true)
48
- chat.with_params(max_tokens: max_tokens)
47
+ chat = rubyllm_context.chat(provider: :openai, protocol: :chat_completions)
48
+ (tools - except).each { |klass| chat.with_tools(klass.new(self)) }
49
+ chat.with_instructions(build_instructions(extra).join, append: true)
50
+ chat.with_provider_options(max_tokens: max_tokens)
49
51
  end
50
52
 
51
53
  # Prompt a chat with a file.
@@ -69,16 +71,17 @@ module Aigency
69
71
  def stream text, *extra, attach: [], except: [], out: $stdout
70
72
  original_log = @log
71
73
  @log = out
72
- response = brief(*extra, except: except).ask text, with: attach do |chunk|
73
- @log.print chunk.thinking&.text
74
- @log.print chunk.content
75
- end
74
+ response = brief(*extra, except: except).ask text, with: attach #do |chunk|
75
+ # @log.print chunk.thinking&.text
76
+ #end
77
+ @log.puts response.content
76
78
  @log.puts "Input tokens: #{response.tokens.input}"
77
79
  @log.puts "Output tokens: #{response.tokens.output}"
78
80
  response
79
81
  rescue StandardError => e
80
- @log.puts "[#{e.class}] #{e.message}"
81
- @log.puts e.backtrace
82
+ message = "[#{e.class}] #{e.message}\n" + e.backtrace.map { |line| " #{line}\n" }.join
83
+ @log.puts message
84
+ File.write data('log', 'error.log'), "#{DateTime.now.iso8601} #{message}", mode: 'a'
82
85
  ensure
83
86
  @log = original_log
84
87
  end
@@ -98,7 +101,7 @@ module Aigency
98
101
 
99
102
  # Run code in an exclusive lock
100
103
  #
101
- def lock(&block)
104
+ def block(&block)
102
105
  File.open(data('lock'), 'w') do |file|
103
106
  file.flock(File::LOCK_EX)
104
107
  file.write(Process.pid)
@@ -106,6 +109,8 @@ module Aigency
106
109
  block&.call
107
110
  end
108
111
  end
112
+ alias lock block
113
+ alias flock block
109
114
 
110
115
  # Run code if an exclusive lock is available or skip if a lock exists
111
116
  #
@@ -130,33 +135,27 @@ module Aigency
130
135
  end
131
136
  alias busy? locked?
132
137
 
138
+ # @return [String]
133
139
  def instructions
134
- @instructions ||= build_instructions([])
140
+ @instructions ||= build_instructions([]).join("\n\n")
135
141
  end
136
142
 
137
143
  # @return [Array<Plugin>]
138
144
  def plugins
139
145
  config('aigent').fetch('plugins', [])
146
+ .unshift('aigent')
147
+ .uniq
140
148
  .map { |name| Plugin.registry[name] }
141
149
  end
142
150
 
143
151
  def tools
144
- plugins.flat_map(&:tools)
152
+ plugins.flat_map(&:tools).uniq
145
153
  end
146
154
 
147
155
  private
148
156
 
149
157
  # @param plugin [Plugin]
150
158
  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
159
  plugin.directories.each do |dir|
161
160
  absolute = File.absolute_path(dir, directory)
162
161
  if File.directory?(absolute)
@@ -166,6 +165,15 @@ module Aigency
166
165
  FileUtils.mkdir_p absolute
167
166
  end
168
167
  end
168
+ unless plugin.config.empty?
169
+ config_file = File.absolute_path(File.join('config', "#{plugin.name}.yml"), directory)
170
+ if File.exist?(config_file)
171
+ log.puts "File config/#{plugin.name}.yml exists, skipping"
172
+ else
173
+ log.puts "Creating config/#{plugin.name}.yml"
174
+ File.write config_file, plugin.config
175
+ end
176
+ end
169
177
  plugin.files.each do |file, content|
170
178
  absolute = File.absolute_path(file, directory)
171
179
  if File.exist?(absolute)
@@ -211,11 +219,10 @@ module Aigency
211
219
  @base_instructions ||= identity_file? ? File.read(identity_file) : "You are a helpful AI assistant."
212
220
  end
213
221
 
222
+ # @return [Array<String>]
214
223
  def build_instructions(extra)
215
- [base_instructions].concat tool_prompts
216
- .push "Today's date is #{DateTime.now.strftime('%B %e, %Y')}."
217
- .concat extra
218
- .join("\n\n")
224
+ [base_instructions].concat(tool_prompts)
225
+ .concat(extra)
219
226
  end
220
227
 
221
228
  def tool_prompts
@@ -225,14 +232,6 @@ module Aigency
225
232
  .compact
226
233
  end
227
234
 
228
- def load_requires
229
- read_requires.each { |name| require name }
230
- end
231
-
232
- def read_requires
233
- config('aigent')['require'] || []
234
- end
235
-
236
235
  def max_tokens
237
236
  @max_tokens ||= (config('aigent')['max_tokens'] || -1).to_i
238
237
  end
@@ -243,5 +242,23 @@ module Aigency
243
242
  config.openai_api_key = config('aigent')['openai_api_key']
244
243
  end
245
244
  end
245
+
246
+ Plugin.register Plugin.new(
247
+ 'aigent',
248
+ tools: [CurrentTime],
249
+ directories: ['config', 'data/chats', 'data/log'],
250
+ rakefile: File.join(GEM_PATH, 'scaffold', 'aigent', 'aigent.rake'),
251
+ config: <<~FILE
252
+ name: Aigent
253
+ openai_api_base: ${OPENAI_API_BASE}
254
+ openai_api_key: ${OPENAI_API_KEY}
255
+ # max_tokens: 65536
256
+ plugins:
257
+ # You should only need either a workspace or a sandbox, not both. If you
258
+ # enable one, it's recommended that you disable the other.
259
+ # - sandbox
260
+ - workspace
261
+ FILE
262
+ )
246
263
  end
247
264
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Delegator
5
+ module_function
6
+
7
+ def aigent
8
+ @aigent ||= Aigent.new(Dir.pwd)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Aigency
6
+ class Plugin
7
+ class Shell < Thor
8
+ include Delegator
9
+ end
10
+ end
11
+ end
@@ -2,20 +2,24 @@
2
2
 
3
3
  module Aigency
4
4
  class Plugin
5
- attr_reader :name, :config, :tools, :directories, :files
5
+ require 'aigency/plugin/shell'
6
+
7
+ attr_reader :name, :config, :tools, :directories, :files, :rakefile, :shell
6
8
 
7
9
  # @param name [String]
8
10
  # @param tools [Array<Class<Tool>>]
9
11
  # @param config [String]
10
12
  # @param directories [Array<String>]
11
13
  # @param files [Hash{String => String}]
12
- def initialize name, tools: [], config: '', directories: [], files: {}
14
+ def initialize name, tools: [], config: '', directories: [], files: {}, rakefile: nil, shell: nil
13
15
  raise "A plugin named '#{name}' already exists" if self.class.registry.key?(name)
14
16
  @name = name
15
17
  @config = config
16
18
  @tools = tools
17
19
  @directories = directories
18
20
  @files = files
21
+ @rakefile = rakefile
22
+ @shell = shell
19
23
  self.class.registry[name] = self
20
24
  end
21
25
 
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Rake
5
+ def self.define(directory = '.')
6
+ aigent = Aigent.new(directory)
7
+ aigent.plugins
8
+ .map(&:rakefile)
9
+ .compact
10
+ .each { |rakefile| ::Rake.load_rakefile(rakefile) }
11
+ end
12
+ end
13
+ end
@@ -5,9 +5,9 @@ require 'open3'
5
5
  module Aigency
6
6
  module Sandbox
7
7
  class Exec < Aigency::Tool
8
- desc 'Run a shell command in your sandbox'
8
+ description 'Run a shell command in your sandbox'
9
9
 
10
- params do
10
+ parameters do
11
11
  string :command, description: "The complete command to execute", required: true
12
12
  end
13
13
 
@@ -7,11 +7,12 @@ module Aigency
7
7
  Plugin.register Plugin.new(
8
8
  'sandbox',
9
9
  tools: [Sandbox::Exec],
10
- directories: ['data/sandbox', 'data/sandbox_ssh'],
10
+ directories: ['sandbox', 'data/sandbox', 'data/sandbox_ssh'],
11
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'))
12
+ 'sandbox/Dockerfile' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'Dockerfile')),
13
+ 'sandbox/squid.conf' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'squid.conf')),
14
+ 'docker-compose.yml' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'docker-compose.yml')),
15
+ 'data/sandbox_ssh/config' => File.read(File.join(GEM_PATH, 'scaffold', 'sandbox', 'ssh_config'))
15
16
  },
16
17
  config: <<~FILE
17
18
  prompt: "The sandbox tools provide access to a Docker container."
data/lib/aigency/shell.rb CHANGED
@@ -4,9 +4,11 @@ require 'fileutils'
4
4
  require 'thor'
5
5
 
6
6
  module Aigency
7
+ # Top-level aigency commands for creating aigents.
8
+ #
7
9
  class Shell < Thor
8
10
  map %i[new] => :enlist
9
- desc "enlist [DIRECTORY]", "Enable the current user as an aigent"
11
+ desc "enlist [DIRECTORY]", "Create an aigent directory"
10
12
  def enlist directory = 'aigent'
11
13
  puts "Installing aigent at #{directory}"
12
14
  if File.exist?(directory)
@@ -18,16 +20,7 @@ module Aigency
18
20
  end
19
21
  end
20
22
  FileUtils.mkdir(directory) unless File.directory?(directory)
21
- FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold'), directory)
23
+ FileUtils.copy_entry(File.join(GEM_PATH, 'scaffold', 'enlist'), directory)
22
24
  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
32
25
  end
33
26
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Tempdir
5
+ module Helpers
6
+ module_function
7
+
8
+ def expand_in_tempdir path
9
+ absolute = File.absolute_path(path, PATH)
10
+ raise PathError, "#{path} is not relative to tempdir" unless absolute.start_with?(PATH)
11
+ absolute
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Tempdir
5
+ class List < Tool
6
+ description "List all files in the temp directory"
7
+
8
+ def execute
9
+ aigent.log.puts "[Tempdir::List]"
10
+ {
11
+ success: true,
12
+ absolute_paths: Dir.glob(File.join(PATH, '**', '*'))
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Tempdir
5
+ class Read < Tool
6
+ include Helpers
7
+
8
+ description "Read a file in the temp directory"
9
+
10
+ parameters do
11
+ string :filename, description: "The file to read"
12
+ end
13
+
14
+ def execute(filename:)
15
+ aigent.log.puts "[Tempdir::Read] #{filename}"
16
+ absolute_path = expand_in_tempdir(filename)
17
+ # @todo Hardcoded max size
18
+ if File.size(absolute_path) > 500_000
19
+ { success: false, error: "File is too large to read (max 500000 bytes)"}
20
+ else
21
+ {
22
+ success: true,
23
+ absolute_path: absolute_path,
24
+ content: File.read(File.join(PATH, filename))
25
+ }
26
+ end
27
+ rescue StandardError => e
28
+ { success: false, error: "[#{e.class}] #{e.message}"}
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Tempdir
5
+ class Size < Tool
6
+ include Helpers
7
+
8
+ description "Get the size of a file in the temp directory"
9
+
10
+ parameters do
11
+ string :filename, description: "The file to measure"
12
+ end
13
+
14
+ def execute(filename:)
15
+ aigent.log.puts "[Tempdir::Read] #{filename}"
16
+ absolute_path = expand_in_tempdir(filename)
17
+ {
18
+ success: true,
19
+ absolute_path: absolute_path,
20
+ size: File.size(absolute_path)
21
+ }
22
+ rescue StandardError => e
23
+ { success: false, error: "[#{e.class}] #{e.message}"}
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Tempdir
5
+ class Write < Tool
6
+ include Helpers
7
+
8
+ description "Write a file in the temp directory"
9
+
10
+ parameters do
11
+ string :filename, description: "The filename to write"
12
+ string :content, description: "The file's content"
13
+ end
14
+
15
+ def execute(filename:, content:)
16
+ aigent.log.puts "[Tempdir::Write] #{filename}"
17
+ absolute_path = expand_in_tempdir(filename)
18
+ File.write absolute_path, content
19
+ {
20
+ success: true,
21
+ absolute_path: absolute_path
22
+ }
23
+ rescue StandardError => e
24
+ { success: false, error: "[#{e.class}] #{e.message}"}
25
+ end
26
+ end
27
+ end
28
+ end