Module: XAeonAgents::Helpers

Extended by:
Logger
Defined in:
lib/x_aeon_agents/helpers.rb

Overview

Various helpers and utilities that are used internally

Defined Under Namespace

Classes: UnexpectedExitStatusError

Class Method Summary collapse

Methods included from Logger

log_debug

Class Method Details

.artifact_files_diffs(base = 'HEAD') ⇒ Object

Get a current files diffs

Parameters:

  • base (String, Symbol) (defaults to: 'HEAD')

    Git base (sha, objectish...) with which we diff, or :cached to only get diff of the staging area.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/x_aeon_agents/helpers.rb', line 98

def artifact_files_diffs(base = 'HEAD')
  if base == :cached
    <<~EO_ARTIFACT
      ### git diff --cached

      ```
      #{git_diff_cached}
      ```
    EO_ARTIFACT
  else
    <<~EO_ARTIFACT
      ### New untracked files

      #{git.status.untracked.keys.map do |file|
        <<~EO_UNTRACKED_FILE
          #### #{file}
          ```
          #{File.read(file)}
          ```
        EO_UNTRACKED_FILE
      end.join("\n")}

      ### git diff

      ```
      #{git.diff(base)}
      ```
    EO_ARTIFACT
  end
end

.gem_nameString?

Get the Ruby gem name from the gemspec file, if any. Returns nil if no gemspec exists.

Returns:

  • (String, nil)

    The gem name, or nil



157
158
159
160
161
162
# File 'lib/x_aeon_agents/helpers.rb', line 157

def gem_name
  @gem_name ||= begin
    gemspec_files = Dir['*.gemspec']
    Gem::Specification.load(gemspec_files.first).name unless gemspec_files.empty?
  end
end

.gitGit::Base

Get a Git instance on the current directory. Keep a cache of it.

Returns:

  • (Git::Base)

    The git instance



83
84
85
# File 'lib/x_aeon_agents/helpers.rb', line 83

def git
  @git ||= Git.open(Dir.pwd)
end

.git_diff_cachedString

Return a list of patch description of diffs in the git staging area.

Returns:

  • (String)

    Patches in the staging area



90
91
92
93
# File 'lib/x_aeon_agents/helpers.rb', line 90

def git_diff_cached
  # TODO: Use ruby-git when the --cached feature will be implemented
  `git diff --cached`.strip
end

.githubOctokit::Client

Get a Github Octokit API instance. Keep a cache of it.

Returns:

  • (Octokit::Client)

    The Octokit client



133
134
135
# File 'lib/x_aeon_agents/helpers.rb', line 133

def github
  @github ||= Octokit::Client.new(access_token: Config.github_token)
end

.github_remoteGit::Remote?

Get the Github remote from the Git remotes. Keep a cache of it.

Returns:

  • (Git::Remote, nil)

    The Github remote instance, or nil if none



141
142
143
# File 'lib/x_aeon_agents/helpers.rb', line 141

def github_remote
  @github_remote ||= git.remotes.find { |remote| remote.url.match(%r{github\.com[:/].+\.git}) }
end

.github_repoString?

Get the current repository name from the Git remote URL. Keep a cache of it.

Returns:

  • (String, nil)

    The Github repository name in the format "owner/repo", or nil if none



149
150
151
# File 'lib/x_aeon_agents/helpers.rb', line 149

def github_repo
  @github_repo ||= github_remote && github_remote.url.match(%r{github\.com[:/](.+)\.git})[1]
end

.keys_from_launcherHash{Symbol => SecretString}

Retrieve API keys needed for the agents from the X-Aeon launcher

Returns:

  • (Hash{Symbol => SecretString})

    The keys retrieved



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/x_aeon_agents/helpers.rb', line 21

def keys_from_launcher
  @keys_from_launcher ||= begin
    keys = {
      cline_api_key: 'Cline API key',
      github_token: 'Github API token',
      openrouter_api_key: 'OpenRouter API key'
    }
    launcher_keys = {}
    Bundler.with_unbundled_env { `launcher safe -- #{keys.values.map { |launcher_key| "\"#{launcher_key}\"" }.join(' ')}` }.each_line do |line|
      next unless line =~ /^\[PASSWORD\] \[([^\]]+)\]: (.+)$/

      launcher_keys[Regexp.last_match(1)] = SecretString.new(Regexp.last_match(2))
    end
    keys.to_h { |key, launcher_key| [key, launcher_keys[launcher_key]] }
  end
end

.review_content(reviews_dir: "#{Config.data_dir}/reviews", name: 'content.txt', description: 'Content to be reviewed', editable: true, promptable: false, content: '') ⇒ Array<String>

Allow user to review and edit content before using it

Parameters:

  • reviews_dir (String) (defaults to: "#{Config.data_dir}/reviews")

    Directory that can be used to store files to be reviewed

  • name (String) (defaults to: 'content.txt')

    Name used for the temporary file

  • description (String) (defaults to: 'Content to be reviewed')

    Description shown to the user

  • editable (Boolean) (defaults to: true)

    Indicates if user can edit the content

  • promptable (Boolean) (defaults to: false)

    Indicates if user can issue a prompt as an answer

  • content (String) (defaults to: '')

    Initial content to present

Returns:

  • (Array<String>)

    2 values are returned:

    • [String] Content after user review (same as content if editable is false)
    • [String] User prompt


175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/x_aeon_agents/helpers.rb', line 175

def review_content(
  reviews_dir: "#{Config.data_dir}/reviews",
  name: 'content.txt',
  description: 'Content to be reviewed',
  editable: true,
  promptable: false,
  content: ''
)
  content_file = "#{reviews_dir}/#{Time.now.utc.strftime('%F-%H-%M-%S')}-#{name}"
  FileUtils.mkdir_p File.dirname(content_file)
  File.write(content_file, content)
  begin
    Launchy.open(content_file)
    puts
    puts <<~EO_STDOUT
      Review the following content: #{description}.
      #{
        (
          (editable ? ['Modify the file and save it to take your changes into consideration'] : []) + [
            'Hit Enter to continue',
            'Hit Ctrl-C to cancel and interrupt'
          ] + (promptable ? ['Any other input will be used to prompt again the generation of this content'] : [])
        ).map { |option| "* #{option}" }.join("\n")
      }
    EO_STDOUT
    user_prompt = $stdin.gets
    [
      editable ? File.read(content_file).strip : content,
      user_prompt.strip
    ]
  ensure
    FileUtils.rm_f content_file unless Config.debug
  end
end

.run_cmd(cmd, expected_exit_status: 0) ⇒ Hash{Symbol => Object}

Execute a command while capturing its output in real time

Parameters:

  • cmd (String)

    Command to be run

  • expected_exit_status (Integer, nil) (defaults to: 0)

    Expected exit status, or nil for no expectation

Returns:

  • (Hash{Symbol => Object})

    Command final output

    • stdout [String] Full stdout
    • stderr [String] Full stderr
    • exit_status [Integer] Exit status


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/x_aeon_agents/helpers.rb', line 46

def run_cmd(cmd, expected_exit_status: 0)
  stdout_lines = []
  stderr_lines = []
  exit_status = nil
  Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
    stdin.close
    [
      # Parse stdout
      Thread.new do
        stdout.each_line do |line|
          stdout_lines << line
        end
      end,
      # Parse stderr
      Thread.new do
        stderr.each_line do |line|
          stderr_lines << line
        end
      end
    ].each(&:join)
    exit_status = wait_thr.value.exitstatus
    log_debug "Command `#{cmd}` exited with status: #{exit_status}"
    if !expected_exit_status.nil? && exit_status != expected_exit_status
      raise UnexpectedExitStatusError, "Command `#{cmd}` exited with status #{exit_status} (expected #{expected_exit_status})"
    end
  end
  {
    stdout: stdout_lines.join,
    stderr: stderr_lines.join,
    exit_status:
  }
end