aigency 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c413cf37072efae1aa6df896884d291ee9bac175a1154e579d85833692f55959
4
- data.tar.gz: feaacbce278ae242f36bd8f2b3024d014bb200dad97679f6fb023986def493ec
3
+ metadata.gz: 7b9b5b24d49d87f69a17cec9420b4815db4646b37d8759951e4ced8431765800
4
+ data.tar.gz: 04ecc1ddb1433cfcd2c18b7c3af420df7dd52a417ef6ab90a2a3de35adb2b99b
5
5
  SHA512:
6
- metadata.gz: '09ba427def6dcce5abcb433c76d44276c58af597ad15f703e1cfb3503e8a527f2944951314c6a2a53e3b91cc54005d54c996e659288d4726e71d0565a33e7cef'
7
- data.tar.gz: e4ef1fea768cd49fad82edf2ea4086b792118c545c7d12ea8772e0ba7c694793fa45c3024552ca35b9677d15a744a133a6f94bcf5f98a952008a6e5fefd9834b
6
+ metadata.gz: 6ab68e574725ebb40bad60ff6e807e480733c91f406d75c8e45b6678e16952e0cd67dde124df13ef9036ef82437bcdae97a4182169cbc615b30116f29c89667c
7
+ data.tar.gz: 6a4e0866fc61258b3fee11e76baeadc72ade5d5d66b87f4d8d41ad2127ac27a50a5f7ef394d0d940e03d7cc389e567f4d7e814ebb1792ef56c125904a75c4dcc
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ ## 0.2.0 - October 2, 2026
2
+ - Log token usage
3
+ - Stale code
4
+ - Fix kwarg
5
+ - Log costs
6
+ - DirectoryReader includes hidden files
7
+ - Prohibited path spec
8
+ - Workspace::Helpers
9
+
10
+ ## 0.1.0 - September 12, 2026
11
+ - First release
@@ -38,26 +38,30 @@ module Aigency
38
38
  # Initialize a RubyLLM::Chat.
39
39
  #
40
40
  # @param extra [Array<String>] Additional instructions
41
+ # @param tools [Array<Class<Tool>>] Available tools (default is all registered tools)
41
42
  # @return [RubyLLM::Chat]
42
- def brief(*extra)
43
+ def brief(*extra, tools: Tools.registry)
43
44
  chat = rubyllm_context.chat(provider: :openai)
44
- Tools.registry.each { |klass| chat.with_tool(klass.new(self)) }
45
+ tools.each { |klass| chat.with_tool(klass.new(self)) }
45
46
  chat.with_instructions(build_instructions(extra), append: true)
46
- # @todo This should be configurable
47
47
  chat.with_params(max_tokens: max_tokens)
48
48
  end
49
49
 
50
- def load filename, *extra, out: $stdout
51
- stream File.read(filename), *extra, out: out
50
+ def load filename, *extra, tools: Tools.registry, out: $stdout
51
+ stream File.read(filename), *extra, tools: tools, out: out
52
52
  end
53
53
 
54
- def stream text, *extra, with: [], out: $stdout
54
+ def stream text, *extra, with: [], tools: Tools.registry, out: $stdout
55
55
  original_log = @log
56
56
  @log = out
57
- brief(*extra).ask text, with: with do |chunk|
57
+ response = brief(*extra, tools: tools).ask text, with: with do |chunk|
58
58
  @log.print chunk.thinking&.text
59
59
  @log.print chunk.content
60
- end.tap { @log = original_log }
60
+ end
61
+ @log.puts "Input tokens: #{response.tokens.input}"
62
+ @log.puts "Output tokens: #{response.tokens.output}"
63
+ @log = original_log
64
+ response
61
65
  end
62
66
 
63
67
  # @param name [String]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aigency
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -3,19 +3,18 @@
3
3
  module Aigency
4
4
  module Workspace
5
5
  class DirectoryDeleter < Tool
6
- desc "Deletes a directory from your private workspace"
6
+ include Helpers
7
7
 
8
- def execute(dirname:)
9
- puts "Deleting directory #{dirname}"
8
+ desc "Delete a directory and all of its contents from your private workspace"
10
9
 
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)
10
+ def execute(directory_name:)
11
+ aigent.log.puts "[Workspace::DirectoryDeleter] #{directory_name}"
16
12
 
13
+ resolved = expand_to_workspace(directory_name)
17
14
  FileUtils.rm_rf(resolved)
18
- { dirname: dirname, success: true }
15
+ { dirname: directory_name, success: true }
16
+ rescue StandardError => e
17
+ { success: false, error: "[#{e.class}] #{e.message}"}
19
18
  end
20
19
  end
21
20
  end
@@ -3,21 +3,18 @@
3
3
  module Aigency
4
4
  module Workspace
5
5
  class DirectoryReader < Tool
6
- desc "Gets the contents of a directory from your private workspace"
6
+ include Helpers
7
+
8
+ desc "Get the contents of a directory from your private workspace"
7
9
 
8
10
  def execute(directory_name:)
9
11
  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
-
12
+ resolved = expand_to_workspace(directory_name)
16
13
  if File.directory?(resolved)
17
14
  {
18
15
  directory: directory_name,
19
16
  success: true,
20
- entries: Dir[File.join(resolved, '*')].map { |dir| dir[resolved.length+1..]}
17
+ entries: Dir.entries(resolved)[2..]
21
18
  }
22
19
  else
23
20
  {
@@ -26,6 +23,8 @@ module Aigency
26
23
  error: "Not a directory"
27
24
  }
28
25
  end
26
+ rescue StandardError => e
27
+ { success: false, error: "[#{e.class}] #{e.message}" }
29
28
  end
30
29
  end
31
30
  end
@@ -3,19 +3,17 @@
3
3
  module Aigency
4
4
  module Workspace
5
5
  class FileDeleter < Tool
6
- desc "Deletes a file from your private workspace"
6
+ include Helpers
7
+
8
+ desc "Delete a file from your private workspace"
7
9
 
8
10
  def execute(filename:)
9
11
  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
-
12
+ resolved = expand_to_workspace(filename)
17
13
  File.delete resolved
18
14
  { filename: filename, success: true }
15
+ rescue StandardError => e
16
+ { success: false, error: "[#{e.class}] #{e.message}" }
19
17
  end
20
18
  end
21
19
  end
@@ -3,28 +3,22 @@
3
3
  module Aigency
4
4
  module Workspace
5
5
  class FileReader < Tool
6
- desc "Reads a file from your private workspace"
6
+ include Helpers
7
+
8
+ desc "Read a file from your private workspace"
7
9
 
8
10
  def execute(filename:)
9
11
  aigent.log.puts "[Workspace::FileReader] Reading #{filename}"
10
12
 
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)
13
+ resolved = expand_to_workspace(filename)
14
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
15
+ {
16
+ success: true,
17
+ filename: filename,
18
+ content: File.read(resolved)
19
+ }
20
+ rescue StandardError => e
21
+ { success: false, error: "[#{e.class}] #{e.message}"}
28
22
  end
29
23
  end
30
24
  end
@@ -5,18 +5,19 @@ require 'fileutils'
5
5
  module Aigency
6
6
  module Workspace
7
7
  class FileWriter < Tool
8
- desc "Writes a file to your private workspace"
8
+ include Helpers
9
+
10
+ desc "Write a file to your private workspace"
9
11
 
10
12
  def execute(filename:, content:)
11
13
  aigent.log.puts "[Workspace::FileWriter] Writing #{filename}"
12
14
 
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
-
15
+ resolved = expand_to_workspace(filename)
17
16
  FileUtils.mkdir_p File.dirname(resolved)
18
17
  File.write resolved, content
19
18
  return { filename: filename, success: true }
19
+ rescue StandardError => e
20
+ { success: false, error: "[#{e.class}] #{e.message}" }
20
21
  end
21
22
  end
22
23
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aigency
4
+ module Workspace
5
+ module Helpers
6
+ # @!method aigent
7
+ # @abstract
8
+ # @return [Aigency::Aigent]
9
+
10
+ # Expand a path relative to the aigent's workspace directory.
11
+ #
12
+ # @raise [PathError] if the expanded path is not inside the workspace
13
+ # @param path [String]
14
+ # @return [String]
15
+ def expand_to_workspace(path)
16
+ workspace = aigent.data('workspace')
17
+ resolved = File.expand_path(path, workspace)
18
+ return resolved if resolved == workspace || resolved.start_with?("#{workspace}/")
19
+
20
+ raise PathError, "Permission denied: `#{path}` is not relative to your root workspace"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,9 +2,13 @@
2
2
 
3
3
  module Aigency
4
4
  module Workspace
5
+ require 'aigency/workspace/helpers'
6
+ require 'aigency/workspace/directory_deleter'
5
7
  require 'aigency/workspace/directory_reader'
6
8
  require 'aigency/workspace/file_reader'
7
9
  require 'aigency/workspace/file_writer'
10
+
11
+ class PathError < RuntimeError; end
8
12
  end
9
13
 
10
14
  Tools.mkdir_p 'data/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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fred Snyder
@@ -58,6 +58,7 @@ executables:
58
58
  extensions: []
59
59
  extra_rdoc_files: []
60
60
  files:
61
+ - CHANGELOG.md
61
62
  - README.md
62
63
  - Rakefile
63
64
  - exe/aigency
@@ -74,6 +75,7 @@ files:
74
75
  - lib/aigency/workspace/file_deleter.rb
75
76
  - lib/aigency/workspace/file_reader.rb
76
77
  - lib/aigency/workspace/file_writer.rb
78
+ - lib/aigency/workspace/helpers.rb
77
79
  - scaffold/.gitignore
78
80
  - scaffold/Gemfile
79
81
  - scaffold/IDENTITY.md