aigency 0.2.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +12 -13
  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 +117 -65
  7. data/lib/aigency/delegator.rb +11 -0
  8. data/lib/aigency/plugin/shell.rb +11 -0
  9. data/lib/aigency/plugin.rb +39 -0
  10. data/lib/aigency/rake.rb +13 -0
  11. data/lib/aigency/sandbox/exec.rb +21 -0
  12. data/lib/aigency/sandbox.rb +22 -0
  13. data/lib/aigency/shell.rb +5 -5
  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 +11 -0
  24. data/lib/aigency/workspace/cmd.rb +59 -0
  25. data/lib/aigency/workspace/copy_tempdir_file.rb +28 -0
  26. data/lib/aigency/workspace/cp.rb +11 -0
  27. data/lib/aigency/workspace/diff.rb +11 -0
  28. data/lib/aigency/workspace/grep.rb +15 -0
  29. data/lib/aigency/workspace/head.rb +11 -0
  30. data/lib/aigency/workspace/helpers.rb +40 -3
  31. data/lib/aigency/workspace/ls.rb +11 -0
  32. data/lib/aigency/workspace/mkdir.rb +11 -0
  33. data/lib/aigency/workspace/mv.rb +11 -0
  34. data/lib/aigency/workspace/rm.rb +11 -0
  35. data/lib/aigency/workspace/tail.rb +11 -0
  36. data/lib/aigency/workspace/wc.rb +13 -0
  37. data/lib/aigency/workspace/write.rb +36 -0
  38. data/lib/aigency/workspace.rb +20 -7
  39. data/lib/aigency.rb +16 -13
  40. data/scaffold/{Rakefile → aigent/aigent.rake} +2 -11
  41. data/scaffold/enlist/.env.example +4 -0
  42. data/scaffold/{.gitignore → enlist/.gitignore} +1 -0
  43. data/scaffold/{Gemfile → enlist/Gemfile} +1 -1
  44. data/scaffold/enlist/README.md +90 -0
  45. data/scaffold/enlist/Rakefile +9 -0
  46. data/scaffold/enlist/bin/aigent +11 -0
  47. data/scaffold/{config → enlist/config}/schedule.rb +5 -0
  48. data/scaffold/sandbox/Dockerfile +37 -0
  49. data/scaffold/sandbox/docker-compose.yml +37 -0
  50. data/scaffold/sandbox/squid.conf +25 -0
  51. data/scaffold/sandbox/ssh_config +2 -0
  52. metadata +114 -16
  53. data/lib/aigency/tools.rb +0 -55
  54. data/lib/aigency/workspace/directory_deleter.rb +0 -23
  55. data/lib/aigency/workspace/directory_reader.rb +0 -33
  56. data/lib/aigency/workspace/file_deleter.rb +0 -22
  57. data/lib/aigency/workspace/file_reader.rb +0 -27
  58. data/lib/aigency/workspace/file_writer.rb +0 -26
  59. data/scaffold/README.md +0 -74
  60. data/scaffold/config/aigent.yml +0 -6
  61. /data/scaffold/{IDENTITY.md → enlist/IDENTITY.md} +0 -0
@@ -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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ module Aigency
7
+ module Tempdir
8
+ require 'aigency/tempdir/helpers'
9
+ require 'aigency/tempdir/list'
10
+ require 'aigency/tempdir/read'
11
+ require 'aigency/tempdir/write'
12
+ require 'aigency/tempdir/size'
13
+
14
+ PATH = Dir.mktmpdir
15
+ TOOLS = [List, Read, Write, Size]
16
+
17
+ at_exit { FileUtils.rm_rf PATH }
18
+ end
19
+ end
data/lib/aigency/tool.rb CHANGED
@@ -5,5 +5,10 @@ module Aigency
5
5
  #
6
6
  class Tool < RubyLLM::Tool
7
7
  include Usability
8
+
9
+ # class << self
10
+ # alias desc description
11
+ # alias params parameters
12
+ # end
8
13
  end
9
14
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Aigency
4
4
  module Usability
5
+ # @return [Aigent]
5
6
  attr_reader :aigent
6
7
 
7
8
  def initialize(aigent)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aigency
4
- VERSION = "0.2.0"
4
+ VERSION = '0.4.0'
5
5
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Cat < Cmd
6
+ description 'Run the `cat` shell command in your workspace'
7
+
8
+ def command = 'cat'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Aigency
6
+ module Workspace
7
+ class Cmd < 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.parameters 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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Aigency
6
+ module Workspace
7
+ class CopyTempdirFile < Tool
8
+ include Tempdir::Helpers
9
+ include Workspace::Helpers
10
+
11
+ description 'Copy a file from tempdir to workspace'
12
+
13
+ parameters do
14
+ string :tempdir_source, description: "A source filename in tempdir", required: true
15
+ string :workspace_dest, description: "A destination filename in workspace", required: true
16
+ end
17
+
18
+ def execute(tempdir_source:, workspace_dest:)
19
+ aigent.log.puts "[Workspace::CopyTempdirFile] #{tempdir_source.inspect} #{workspace_dest.inspect}"
20
+ abs_tempdir = expand_in_tempdir(tempdir_source)
21
+ abs_workspace = expand_in_workspace(workspace_dest)
22
+ FileUtils.copy_file abs_tempdir, abs_workspace
23
+ rescue StandardError => e
24
+ { success: false, error: "[#{e.class}] #{e.message}"}
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Cp < Cmd
6
+ description 'Run the `cp` shell command in your workspace'
7
+
8
+ def command = 'cp'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Diff < Cmd
6
+ description 'Run the `diff` shell command in your workspace'
7
+
8
+ def command = 'diff'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Grep < Cmd
6
+ description '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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Head < Cmd
6
+ description 'Run the `head` shell command in your workspace'
7
+
8
+ def command = 'head'
9
+ end
10
+ end
11
+ end
@@ -3,21 +3,58 @@
3
3
  module Aigency
4
4
  module Workspace
5
5
  module Helpers
6
+ module_function
7
+
6
8
  # @!method aigent
7
9
  # @abstract
8
10
  # @return [Aigency::Aigent]
9
11
 
10
- # Expand a path relative to the aigent's workspace directory.
12
+ # Expand a path relative to the aigent's workspace.
11
13
  #
12
14
  # @raise [PathError] if the expanded path is not inside the workspace
13
15
  # @param path [String]
14
16
  # @return [String]
15
- def expand_to_workspace(path)
17
+ def expand_in_workspace(path)
16
18
  workspace = aigent.data('workspace')
17
19
  resolved = File.expand_path(path, workspace)
18
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
19
50
 
20
- raise PathError, "Permission denied: `#{path}` is not relative to your root workspace"
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
21
58
  end
22
59
  end
23
60
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Ls < Cmd
6
+ description 'Run the `ls` shell command in your workspace'
7
+
8
+ def command = 'ls'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Mkdir < Cmd
6
+ description 'Run the `mkdir` shell command in your workspace'
7
+
8
+ def command = 'mkdir'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Mv < Cmd
6
+ description 'Run the `mv` shell command in your workspace'
7
+
8
+ def command = 'mv'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Rm < Cmd
6
+ description 'Run the `rm` shell command in your workspace'
7
+
8
+ def command = 'rm'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Tail < Cmd
6
+ description 'Run the `tail` shell command in your workspace'
7
+
8
+ def command = 'tail'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ class Wc < Cmd
6
+ description 'Run the `wc` shell command in your workspace'
7
+
8
+ def command = 'wc'
9
+
10
+ def prohibited_arguments = %w[--files0-from]
11
+ end
12
+ end
13
+ 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
+ description 'Write or append to a file in the workspace'
14
+
15
+ parameters 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
@@ -3,13 +3,26 @@
3
3
  module Aigency
4
4
  module Workspace
5
5
  require 'aigency/workspace/helpers'
6
- require 'aigency/workspace/directory_deleter'
7
- require 'aigency/workspace/directory_reader'
8
- require 'aigency/workspace/file_reader'
9
- require 'aigency/workspace/file_writer'
6
+ require 'aigency/workspace/cmd'
7
+ require 'aigency/workspace/cat'
8
+ require 'aigency/workspace/cp'
9
+ require 'aigency/workspace/diff'
10
+ require 'aigency/workspace/grep'
11
+ require 'aigency/workspace/ls'
12
+ require 'aigency/workspace/mkdir'
13
+ require 'aigency/workspace/mv'
14
+ require 'aigency/workspace/rm'
15
+ require 'aigency/workspace/wc'
16
+ require 'aigency/workspace/write'
17
+ require 'aigency/workspace/copy_tempdir_file'
10
18
 
11
- class PathError < RuntimeError; end
19
+ Plugin.register Plugin.new(
20
+ 'workspace',
21
+ directories: ['data/workspace'],
22
+ tools: [Cat, Cp, Diff, Grep, Ls, Mkdir, Mv, Rm, Wc, Write, CopyTempdirFile],
23
+ config: <<~FILE
24
+ prompt: "The workspace tools provide access to a readable and writeable directory."
25
+ FILE
26
+ )
12
27
  end
13
-
14
- Tools.mkdir_p 'data/workspace'
15
28
  end
data/lib/aigency.rb CHANGED
@@ -2,20 +2,23 @@
2
2
 
3
3
  require 'ruby_llm'
4
4
 
5
- # Core
6
- require 'aigency/version'
7
- require 'aigency/usability'
8
- require 'aigency/tool'
9
- require 'aigency/tools'
10
- require 'aigency/aigent'
11
- require 'aigency/shell'
12
-
13
- # Extensions
14
- require 'aigency/workspace'
15
-
16
5
  module Aigency
6
+ class PathError < ArgumentError; end
7
+
17
8
  GEM_PATH = File.absolute_path(File.join(__dir__, '..'))
18
9
 
19
- class Error < StandardError; end
20
- # Your code goes here...
10
+ # Core
11
+ require 'aigency/version'
12
+ require 'aigency/usability'
13
+ require 'aigency/tool'
14
+ require 'aigency/delegator'
15
+ require 'aigency/plugin'
16
+ require 'aigency/aigent'
17
+ require 'aigency/shell'
18
+ require 'aigency/rake'
19
+
20
+ # Plugins and Tools
21
+ require 'aigency/sandbox'
22
+ require 'aigency/tempdir'
23
+ require 'aigency/workspace'
21
24
  end
@@ -1,7 +1,4 @@
1
- require "bundler/setup"
2
- require 'yaml'
3
- require 'dotenv/load'
4
- require 'aigency'
1
+ # frozen_string_literal: true
5
2
 
6
3
  desc "Run a prompt from a file"
7
4
  task :load, [:filename] do |_, args|
@@ -17,13 +14,7 @@ task :load, [:filename] do |_, args|
17
14
  end
18
15
  end
19
16
 
20
- desc "Talk to the aigent"
21
- task :say, [:text] do |_, args|
22
- aigent = Aigency::Aigent.new('.')
23
- aigent.stream args[:text]
24
- end
25
-
26
- desc "Set up aigent configurations"
17
+ desc "Set up aigent plugins"
27
18
  task :setup do
28
19
  aigent = Aigency::Aigent.new('.')
29
20
  aigent.setup
@@ -0,0 +1,4 @@
1
+ # Environment variables to load into aigent config files
2
+
3
+ OPENAI_API_BASE=https://api.openai.com/v1
4
+ OPENAI_API_KEY=change_me
@@ -1 +1,2 @@
1
1
  data
2
+ .env
@@ -7,4 +7,4 @@ gem "rake", "~> 13.0"
7
7
  gem "whenever", "~> 1.1"
8
8
 
9
9
  gem "ruby_llm"
10
- gem "aigency", path: "../aigency"
10
+ gem "aigency"