x_aeon_agents 1.0.28

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +136 -0
  3. data/CHANGELOG.md +398 -0
  4. data/README.md +579 -0
  5. data/TODO.md +48 -0
  6. data/bin/xaa +5 -0
  7. data/lib/x_aeon_agents/agent_defaults.rb +60 -0
  8. data/lib/x_aeon_agents/agent_options.rb +58 -0
  9. data/lib/x_aeon_agents/agents/coder_agent.rb +82 -0
  10. data/lib/x_aeon_agents/agents/committer_agent.rb +71 -0
  11. data/lib/x_aeon_agents/agents/developer_agent.rb +235 -0
  12. data/lib/x_aeon_agents/agents/diff_interpreter_agent.rb +80 -0
  13. data/lib/x_aeon_agents/agents/documenter_agent.rb +44 -0
  14. data/lib/x_aeon_agents/agents/executor_agent.rb +9 -0
  15. data/lib/x_aeon_agents/agents/feedback_analyst_agent.rb +62 -0
  16. data/lib/x_aeon_agents/agents/gh_comments.gql +64 -0
  17. data/lib/x_aeon_agents/agents/git_diff_interpreter_agent.rb +57 -0
  18. data/lib/x_aeon_agents/agents/issue_implementer_agent.rb +88 -0
  19. data/lib/x_aeon_agents/agents/one_line_code_diff_summarizer_agent.rb +57 -0
  20. data/lib/x_aeon_agents/agents/plan_generator_agent.rb +51 -0
  21. data/lib/x_aeon_agents/agents/planner_agent.rb +89 -0
  22. data/lib/x_aeon_agents/agents/pull_request_creator_agent.rb +144 -0
  23. data/lib/x_aeon_agents/agents/readme/about_analyzer_agent.rb +55 -0
  24. data/lib/x_aeon_agents/agents/readme/contributing_agent.rb +47 -0
  25. data/lib/x_aeon_agents/agents/readme/development_agent.rb +54 -0
  26. data/lib/x_aeon_agents/agents/readme/documentation_agent.rb +43 -0
  27. data/lib/x_aeon_agents/agents/readme/features_agent.rb +45 -0
  28. data/lib/x_aeon_agents/agents/readme/how_it_works_agent.rb +47 -0
  29. data/lib/x_aeon_agents/agents/readme/license_agent.rb +43 -0
  30. data/lib/x_aeon_agents/agents/readme/public_api_agent.rb +44 -0
  31. data/lib/x_aeon_agents/agents/readme/quick_start_agent.rb +50 -0
  32. data/lib/x_aeon_agents/agents/readme/requirements_agent.rb +43 -0
  33. data/lib/x_aeon_agents/agents/readme_generator_agent.rb +438 -0
  34. data/lib/x_aeon_agents/agents/review_resolver_agent.rb +236 -0
  35. data/lib/x_aeon_agents/agents/review_responder_agent.rb +64 -0
  36. data/lib/x_aeon_agents/agents/skill_generator_agent.rb +95 -0
  37. data/lib/x_aeon_agents/agents/skill_installer_agent.rb +103 -0
  38. data/lib/x_aeon_agents/agents/task_starter_agent.rb +66 -0
  39. data/lib/x_aeon_agents/agents/tester_agent.rb +44 -0
  40. data/lib/x_aeon_agents/cli.rb +384 -0
  41. data/lib/x_aeon_agents/config.rb +110 -0
  42. data/lib/x_aeon_agents/gen_helpers.rb +270 -0
  43. data/lib/x_aeon_agents/helpers.rb +211 -0
  44. data/lib/x_aeon_agents/logger.rb +21 -0
  45. data/lib/x_aeon_agents/providers/cline.rb +78 -0
  46. data/lib/x_aeon_agents/version.rb +6 -0
  47. data/lib/x_aeon_agents.rb +38 -0
  48. metadata +296 -0
@@ -0,0 +1,270 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ module XAeonAgents
5
+ # Helper methods for generating skill content
6
+ module GenHelpers
7
+ # @!group Public API
8
+
9
+ # Define a skill metadata.
10
+ # This should always be the first call in a skill ERB file.
11
+ # It also returns the corresponding YAML frontmatter.
12
+ # The name is automatically derived from skill_name.
13
+ #
14
+ # @param description [String] Description of the skill
15
+ # @param dependencies [Array<String>] List of skills dependencies
16
+ # @param plan [Boolean] Is this skill applicable to plan mode?
17
+ # @param metadata [Hash] Optional metadata key-value pairs
18
+ # @return [String] The complete YAML frontmatter block (including --- delimiters)
19
+ def skill(description:, dependencies: [], plan: false, metadata: {})
20
+ @plan = plan
21
+ frontmatter = {
22
+ 'name' => name,
23
+ 'description' => "#{description}#{' Use this skill also in Plan mode.' if plan}"
24
+ }
25
+ metadata['agent'] = 'Plan' if plan
26
+ metadata['dependencies'] = dependencies unless dependencies.empty?
27
+ frontmatter['metadata'] = metadata.transform_keys(&:to_s) unless metadata.empty?
28
+ "#{YAML.dump(frontmatter, line_width: -1).chomp}\n---"
29
+ end
30
+
31
+ # Define or get the skill goal to be used in ERB templates
32
+ #
33
+ # @param goal_desc [String, nil] The skill goal, or nil to retrieve the previously set skill goal
34
+ # @return [String] The skill goal
35
+ def goal(goal_desc = nil)
36
+ @skill_goal = goal_desc unless goal_desc.nil?
37
+ @skill_goal
38
+ end
39
+
40
+ # Get the skill goal as a sentence
41
+ # Prerequisite: skill_goal should be set before.
42
+ #
43
+ # @return [String] The skill goal as useable inside a sentence
44
+ def goal_sentence
45
+ "#{@skill_goal[0].downcase}#{@skill_goal[1..]}"
46
+ end
47
+
48
+ # Return the prompt to announce that the agent is working on a skill.
49
+ # Prerequisite: skill_goal should be set before.
50
+ def announce
51
+ "Always tell the user \"SKILL: I am #{goal_sentence}\" to inform the user that you are running this skill."
52
+ end
53
+
54
+ # Return a default temporary folder that agents can use in a project.
55
+ # It's better to force it to the agents, as some models will try weird CLI commands to create temporary files otherwise.
56
+ #
57
+ # @return [String] Temporary folder path
58
+ def tmp_path
59
+ "#{Config.data_dir}/tmp"
60
+ end
61
+
62
+ # Generate a rule documentation block with examples and rationale.
63
+ #
64
+ # @param title [String] The rule title
65
+ # @param description [String, nil] The additional description
66
+ # @param type [Symbol] The code block language type (e.g., :bash, :ruby)
67
+ # @param bad [String, nil] The incorrect example
68
+ # @param good [String, nil] The correct example
69
+ # @param rationale [String, nil] The explanation for why this rule exists
70
+ # @return [String] The formatted markdown documentation for the rule
71
+ def rule(title, description: nil, type: :ruby, bad: nil, good: nil, rationale: nil)
72
+ markdown_sections = [
73
+ <<~EO_MARKDOWN
74
+ ### Rule: #{title}#{"\n\n#{description.strip}" unless description.nil?}
75
+ EO_MARKDOWN
76
+ ]
77
+ unless bad.nil?
78
+ markdown_sections << <<~EO_MARKDOWN
79
+ #### Example: Incorrect
80
+
81
+ ```#{type}
82
+ #{bad.strip}
83
+ ```
84
+ EO_MARKDOWN
85
+ end
86
+ unless good.nil?
87
+ markdown_sections << <<~EO_MARKDOWN
88
+ #### Example: Correct
89
+
90
+ ```#{type}
91
+ #{good.strip}
92
+ ```
93
+ EO_MARKDOWN
94
+ end
95
+ unless rationale.nil?
96
+ markdown_sections << <<~EO_MARKDOWN
97
+ #### Rationale
98
+
99
+ #{rationale}
100
+ EO_MARKDOWN
101
+ end
102
+ markdown_sections.join("\n").strip
103
+ end
104
+
105
+ # Define an ordered todo list for a skill.
106
+ # Captures the ERB block content, parses its ## sections, numbers them starting at 2
107
+ # (section 1 "Inform the USER" is auto-generated), and wraps everything with the
108
+ # standard skill header, checklist initialization, and final verification sections.
109
+ #
110
+ # @param erb_block [#call] ERB block containing the markdown sections
111
+ # @return [String] The formatted todo list
112
+ def ordered_todo_list(&erb_block)
113
+ transform_erb_block(erb_block) do |erb_content|
114
+ # Split into sections by ## headings
115
+ # Number sections starting from 2 and strip trailing whitespace
116
+ step_number = 2
117
+ numbered_sections = erb_content
118
+ .strip
119
+ .split(/^(?=### )/)
120
+ .reject { |s| s.strip.empty? }
121
+ .map do |section|
122
+ numbered = section.sub(/^### /, "### #{step_number}. ").rstrip
123
+ step_number += 1
124
+ numbered
125
+ end
126
+
127
+ # Compose the full output and append directly to ERB buffer
128
+ # (we use <% %> not <%= %> since standard ERB doesn't support <%= method do %>)
129
+ <<~EO_MARKDOWN
130
+ ## Sequential steps to be followed when using this skill
131
+
132
+ When #{goal_sentence}, follow those steps.
133
+
134
+ #{init_skill_checklist(name).rstrip}
135
+
136
+ ### 1. Inform the user
137
+
138
+ - #{announce}
139
+
140
+ #{numbered_sections.join("\n\n")}
141
+
142
+ #{validate_skill_checklist(name).rstrip}
143
+ EO_MARKDOWN
144
+ end
145
+ end
146
+
147
+ # Return the skill config hash from its .skill_config.yml file, if it exists.
148
+ #
149
+ # @param skill_name [String] The skill name (matching a directory under skills.src/)
150
+ # @return [Hash] The YAML config hash, or an empty Hash if no config file exists
151
+ def self.config(skill_name)
152
+ skill_config_file = "skills.src/#{skill_name}/.skill_config.yml"
153
+ File.exist?(skill_config_file) ? YAML.load_file(skill_config_file) : {}
154
+ end
155
+
156
+ # Return the skill being generated
157
+ #
158
+ # @return [String] Skill name being generated
159
+ def name
160
+ current_erb_file.match(%r{/skills\.src/([^/]+)/})[1]
161
+ end
162
+
163
+ # Generate the "When to use it" section for a skill.
164
+ # This helper automatically includes standard items and custom usage instructions.
165
+ #
166
+ # @param erb_block [#call] ERB block containing custom usage instructions
167
+ # @return [String] The formatted "When to use it" section
168
+ def when_to_use(&erb_block)
169
+ transform_erb_block(erb_block) do |erb_content|
170
+ blocks = []
171
+ blocks << '- This skill can be used when in Plan mode.' if @plan
172
+ blocks << "- Always use it every time another skill specifically mentions `skill: #{name}`."
173
+ blocks << erb_content
174
+ <<~EO_MARKDOWN
175
+ ## When to use it
176
+
177
+ #{blocks.join("\n").rstrip}
178
+ EO_MARKDOWN
179
+ end
180
+ end
181
+
182
+ # Small class that can serve as a container for ERB evaluation with our DSL
183
+ class ErbEvaluator
184
+ include GenHelpers
185
+
186
+ # Constructor
187
+ #
188
+ # @param erb_file [String] File containing the ERB template
189
+ def initialize(erb_file)
190
+ @erb = ERB.new(File.read(erb_file), trim_mode: '-')
191
+ # Use filename for better error reporting
192
+ @erb.filename = erb_file
193
+ end
194
+
195
+ # Evaluate the ERB template
196
+ #
197
+ # @return [String] The evaluated ERB result
198
+ def result
199
+ @erb.result(binding)
200
+ end
201
+ end
202
+
203
+ private
204
+
205
+ # Return the execution checklist initialization section
206
+ #
207
+ # @param checklist_name [String] Name to be given to this checklist
208
+ # @return [String] The execution checklist section
209
+ def init_skill_checklist(checklist_name)
210
+ <<~EO_MARKDOWN
211
+ ### Create the #{checklist_name} Execution Checklist (MANDATORY)
212
+
213
+ - Before executing anything, create a checklist named #{checklist_name} Execution Checklist with all steps of these instructions.
214
+ - The #{checklist_name} Execution Checklist must include all numbered steps explicitly.
215
+ - After completing each step of these instructions, mark the item in the #{checklist_name} Execution Checklist as completed.
216
+ - Do not skip any item.
217
+ - If an item cannot be executed, explicitly explain why.
218
+ - Never mark the task as completed while any item from the #{checklist_name} Execution Checklist remains open.
219
+ EO_MARKDOWN
220
+ end
221
+
222
+ # Return the final verification section
223
+ #
224
+ # @param checklist_name [String] Name to be given to this checklist
225
+ # @return [String] The final verification section
226
+ def validate_skill_checklist(checklist_name)
227
+ <<~EO_MARKDOWN
228
+ ### Final Verification (MANDATORY)
229
+
230
+ Before declaring the task complete:
231
+
232
+ - Re-list all numbered steps from the #{checklist_name} Execution Checklist.
233
+ - Confirm each one was executed.
234
+ - If any step was not executed, execute it now.
235
+ EO_MARKDOWN
236
+ end
237
+
238
+ # Return the ERB file being generated
239
+ #
240
+ # @return [String] ERB file being generated
241
+ def current_erb_file
242
+ file_found = caller.find { |stack_trace| stack_trace =~ %r{(/skills\.src/.+\.erb)} }
243
+ raise "Unable to find ERB file among stack:\n#{caller.join("\n")}" if file_found.nil?
244
+
245
+ Regexp.last_match[1]
246
+ end
247
+
248
+ # Capture the ERB content inside a code block, and return a user-transformed version of it.
249
+ # Handle indentation properly by removing the indentation caused by the ERB call itself.
250
+ #
251
+ # @param erb_block [#call] The block containing ERB content
252
+ # @yield [#call(erb_content) -> String] The code that should transform the content
253
+ # @yieldparam erb_content [String] The ERB content
254
+ # @yieldreturn [String] The transformed content
255
+ def transform_erb_block(erb_block)
256
+ # Capture the ERB block content using buffer manipulation
257
+ erb_buffer = eval('_erbout', erb_block.binding, __FILE__, __LINE__)
258
+ saved_content = erb_buffer.dup
259
+ erb_buffer.clear
260
+ erb_block.call
261
+ captured = erb_buffer.dup
262
+ erb_buffer.replace(saved_content)
263
+
264
+ # Dedent the captured content: remove common leading whitespace
265
+ lines = captured.lines
266
+ min_indent = lines.reject { |l| l.strip.empty? }.map { |l| l.match(/^(\s*)/)[1].length }.min || 0
267
+ erb_buffer << yield(lines.map { |l| l.strip.empty? ? "\n" : l[min_indent..] }.join)
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,211 @@
1
+ require 'fileutils'
2
+ require 'git'
3
+ require 'launchy'
4
+ require 'octokit'
5
+ require 'open3'
6
+ require 'secret_string'
7
+
8
+ module XAeonAgents
9
+ # Various helpers and utilities that are used internally
10
+ module Helpers
11
+ # Exception class used to identify commands not returning the expected exit status
12
+ class UnexpectedExitStatusError < StandardError
13
+ end
14
+
15
+ class << self
16
+ include Logger
17
+
18
+ # Retrieve API keys needed for the agents from the X-Aeon launcher
19
+ #
20
+ # @return [Hash{Symbol => SecretString}] The keys retrieved
21
+ def keys_from_launcher
22
+ @keys_from_launcher ||= begin
23
+ keys = {
24
+ cline_api_key: 'Cline API key',
25
+ github_token: 'Github API token',
26
+ openrouter_api_key: 'OpenRouter API key'
27
+ }
28
+ launcher_keys = {}
29
+ Bundler.with_unbundled_env { `launcher safe -- #{keys.values.map { |launcher_key| "\"#{launcher_key}\"" }.join(' ')}` }.each_line do |line|
30
+ next unless line =~ /^\[PASSWORD\] \[([^\]]+)\]: (.+)$/
31
+
32
+ launcher_keys[Regexp.last_match(1)] = SecretString.new(Regexp.last_match(2))
33
+ end
34
+ keys.to_h { |key, launcher_key| [key, launcher_keys[launcher_key]] }
35
+ end
36
+ end
37
+
38
+ # Execute a command while capturing its output in real time
39
+ #
40
+ # @param cmd [String] Command to be run
41
+ # @param expected_exit_status [Integer, nil] Expected exit status, or nil for no expectation
42
+ # @return [Hash{Symbol => Object}] Command final output
43
+ # - stdout [String] Full stdout
44
+ # - stderr [String] Full stderr
45
+ # - exit_status [Integer] Exit status
46
+ def run_cmd(cmd, expected_exit_status: 0)
47
+ stdout_lines = []
48
+ stderr_lines = []
49
+ exit_status = nil
50
+ Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
51
+ stdin.close
52
+ [
53
+ # Parse stdout
54
+ Thread.new do
55
+ stdout.each_line do |line|
56
+ stdout_lines << line
57
+ end
58
+ end,
59
+ # Parse stderr
60
+ Thread.new do
61
+ stderr.each_line do |line|
62
+ stderr_lines << line
63
+ end
64
+ end
65
+ ].each(&:join)
66
+ exit_status = wait_thr.value.exitstatus
67
+ log_debug "Command `#{cmd}` exited with status: #{exit_status}"
68
+ if !expected_exit_status.nil? && exit_status != expected_exit_status
69
+ raise UnexpectedExitStatusError, "Command `#{cmd}` exited with status #{exit_status} (expected #{expected_exit_status})"
70
+ end
71
+ end
72
+ {
73
+ stdout: stdout_lines.join,
74
+ stderr: stderr_lines.join,
75
+ exit_status:
76
+ }
77
+ end
78
+
79
+ # Get a Git instance on the current directory.
80
+ # Keep a cache of it.
81
+ #
82
+ # @return [Git::Base] The git instance
83
+ def git
84
+ @git ||= Git.open(Dir.pwd)
85
+ end
86
+
87
+ # Return a list of patch description of diffs in the git staging area.
88
+ #
89
+ # @return [String] Patches in the staging area
90
+ def git_diff_cached
91
+ # TODO: Use ruby-git when the --cached feature will be implemented
92
+ `git diff --cached`.strip
93
+ end
94
+
95
+ # Get a current files diffs
96
+ #
97
+ # @param base [String, Symbol] Git base (sha, objectish...) with which we diff, or :cached to only get diff of the staging area.
98
+ def artifact_files_diffs(base = 'HEAD')
99
+ if base == :cached
100
+ <<~EO_ARTIFACT
101
+ ### git diff --cached
102
+
103
+ ```
104
+ #{git_diff_cached}
105
+ ```
106
+ EO_ARTIFACT
107
+ else
108
+ <<~EO_ARTIFACT
109
+ ### New untracked files
110
+
111
+ #{git.status.untracked.keys.map do |file|
112
+ <<~EO_UNTRACKED_FILE
113
+ #### #{file}
114
+ ```
115
+ #{File.read(file)}
116
+ ```
117
+ EO_UNTRACKED_FILE
118
+ end.join("\n")}
119
+
120
+ ### git diff
121
+
122
+ ```
123
+ #{git.diff(base)}
124
+ ```
125
+ EO_ARTIFACT
126
+ end
127
+ end
128
+
129
+ # Get a Github Octokit API instance.
130
+ # Keep a cache of it.
131
+ #
132
+ # @return [Octokit::Client] The Octokit client
133
+ def github
134
+ @github ||= Octokit::Client.new(access_token: Config.github_token)
135
+ end
136
+
137
+ # Get the Github remote from the Git remotes.
138
+ # Keep a cache of it.
139
+ #
140
+ # @return [Git::Remote, nil] The Github remote instance, or nil if none
141
+ def github_remote
142
+ @github_remote ||= git.remotes.find { |remote| remote.url.match(%r{github\.com[:/].+\.git}) }
143
+ end
144
+
145
+ # Get the current repository name from the Git remote URL.
146
+ # Keep a cache of it.
147
+ #
148
+ # @return [String, nil] The Github repository name in the format "owner/repo", or nil if none
149
+ def github_repo
150
+ @github_repo ||= github_remote && github_remote.url.match(%r{github\.com[:/](.+)\.git})[1]
151
+ end
152
+
153
+ # Get the Ruby gem name from the gemspec file, if any.
154
+ # Returns nil if no gemspec exists.
155
+ #
156
+ # @return [String, nil] The gem name, or nil
157
+ def gem_name
158
+ @gem_name ||= begin
159
+ gemspec_files = Dir['*.gemspec']
160
+ Gem::Specification.load(gemspec_files.first).name unless gemspec_files.empty?
161
+ end
162
+ end
163
+
164
+ # Allow user to review and edit content before using it
165
+ #
166
+ # @param reviews_dir [String] Directory that can be used to store files to be reviewed
167
+ # @param name [String] Name used for the temporary file
168
+ # @param description [String] Description shown to the user
169
+ # @param editable [Boolean] Indicates if user can edit the content
170
+ # @param promptable [Boolean] Indicates if user can issue a prompt as an answer
171
+ # @param content [String] Initial content to present
172
+ # @return [Array<String>] 2 values are returned:
173
+ # - [String] Content after user review (same as content if editable is false)
174
+ # - [String] User prompt
175
+ def review_content(
176
+ reviews_dir: "#{Config.data_dir}/reviews",
177
+ name: 'content.txt',
178
+ description: 'Content to be reviewed',
179
+ editable: true,
180
+ promptable: false,
181
+ content: ''
182
+ )
183
+ content_file = "#{reviews_dir}/#{Time.now.utc.strftime('%F-%H-%M-%S')}-#{name}"
184
+ FileUtils.mkdir_p File.dirname(content_file)
185
+ File.write(content_file, content)
186
+ begin
187
+ Launchy.open(content_file)
188
+ puts
189
+ puts <<~EO_STDOUT
190
+ Review the following content: #{description}.
191
+ #{
192
+ (
193
+ (editable ? ['Modify the file and save it to take your changes into consideration'] : []) + [
194
+ 'Hit Enter to continue',
195
+ 'Hit Ctrl-C to cancel and interrupt'
196
+ ] + (promptable ? ['Any other input will be used to prompt again the generation of this content'] : [])
197
+ ).map { |option| "* #{option}" }.join("\n")
198
+ }
199
+ EO_STDOUT
200
+ user_prompt = $stdin.gets
201
+ [
202
+ editable ? File.read(content_file).strip : content,
203
+ user_prompt.strip
204
+ ]
205
+ ensure
206
+ FileUtils.rm_f content_file unless Config.debug
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,21 @@
1
+ module XAeonAgents
2
+ # Mixin adding logging capabilities
3
+ module Logger
4
+ class << self
5
+ # Global debug switch.
6
+ attr_accessor :debug
7
+ end
8
+
9
+ # Log a message if debug was activated
10
+ #
11
+ # @param msg [String, nil] Message to be displayed, or nil if the message is given lazily through a code block
12
+ # @yield [#call -> String] Optional code returning a [String] for lazy evaluation
13
+ # @yieldreturn [String] The message to be displayed
14
+ def log_debug(msg = nil)
15
+ return unless Logger.debug
16
+
17
+ msg = yield if block_given?
18
+ puts "[DEBUG] - #{msg}"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,78 @@
1
+ require 'ruby_llm/message'
2
+ require 'ruby_llm/thinking'
3
+ require 'ruby_llm/providers/openai'
4
+ require 'ruby_llm/providers/openrouter/chat'
5
+ require 'ruby_llm/providers/openrouter/models'
6
+ require 'ruby_llm/providers/openrouter/streaming'
7
+ require 'ruby_llm/providers/openrouter/images'
8
+
9
+ module XAeonAgents
10
+ # Collection of providers for ai-agents
11
+ module Providers
12
+ # Cline API integration.
13
+ class Cline < RubyLLM::Providers::OpenAI
14
+ # @return [String] The base API URL
15
+ def api_base
16
+ @config.cline_api_base || 'https://api.cline.bot/api/v1'
17
+ end
18
+
19
+ # @return [Hash{String => String}] HTTP headers to add to the queries
20
+ def headers
21
+ {
22
+ 'Authorization' => "Bearer #{@config.cline_api_key}"
23
+ }
24
+ end
25
+
26
+ # Parses the completion response from the Cline API into a [RubyLLM::Message].
27
+ #
28
+ # Handles empty responses, API errors, message extraction, content/thinking parsing,
29
+ # token usage tracking (including cached and reasoning tokens), and tool calls.
30
+ #
31
+ # @param response [Faraday::Response] The raw HTTP response from the Cline API.
32
+ # @return [RubyLLM::Message, nil] The parsed message, or nil if the response body is empty
33
+ # or no message data is present.
34
+ def parse_completion_response(response)
35
+ data = response.body
36
+ return if data.empty?
37
+
38
+ raise Error.new(response, data['error']) if data['error']
39
+
40
+ message_data = data.dig('data', 'choices', 0, 'message')
41
+ return unless message_data
42
+
43
+ usage = data.dig('data', 'usage') || {}
44
+ cached_tokens = usage.dig('prompt_tokens_details', 'cached_tokens')
45
+ thinking_tokens = usage.dig('completion_tokens_details', 'reasoning_tokens')
46
+ content, thinking_from_blocks = extract_content_and_thinking(message_data['content'])
47
+ thinking_text = thinking_from_blocks || extract_thinking_text(message_data)
48
+ thinking_signature = extract_thinking_signature(message_data)
49
+
50
+ RubyLLM::Message.new(
51
+ role: :assistant,
52
+ content: content,
53
+ thinking: RubyLLM::Thinking.build(text: thinking_text, signature: thinking_signature),
54
+ tool_calls: parse_tool_calls(message_data['tool_calls']),
55
+ input_tokens: usage['prompt_tokens'],
56
+ output_tokens: usage['completion_tokens'],
57
+ cached_tokens: cached_tokens,
58
+ cache_creation_tokens: 0,
59
+ thinking_tokens: thinking_tokens,
60
+ model_id: data.dig('data', 'model'),
61
+ raw: response
62
+ )
63
+ end
64
+
65
+ class << self
66
+ # @return [Array<Symbol>] The required configuration keys for the Cline provider.
67
+ def configuration_requirements
68
+ %i[cline_api_base cline_api_key]
69
+ end
70
+
71
+ # @return [Array<Symbol>] The available configuration keys for the Cline provider.
72
+ def configuration_options
73
+ %i[cline_api_base cline_api_key]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,6 @@
1
+ module XAeonAgents
2
+ # @!group Global API
3
+
4
+ # Gem version
5
+ VERSION = '1.0.28'
6
+ end
@@ -0,0 +1,38 @@
1
+ require 'zeitwerk'
2
+
3
+ Zeitwerk::Loader.for_gem.setup
4
+
5
+ # As most agents come from composable_agents, require it here just once.
6
+ require 'composable_agents'
7
+
8
+ # Provide skills, tools, agents and workflows that allow AI to be used in X-Aeon projects.
9
+ module XAeonAgents
10
+ # Return the current AI agent name
11
+ #
12
+ # @return [String] The current AI agent name
13
+ def self.agent_name
14
+ # TODO: Make this adaptable to different agents using plugins
15
+ require 'sqlite3'
16
+ require 'json'
17
+
18
+ # Find VSCode globalStorage database
19
+ state_db = "#{ENV['VSCODE_PORTABLE'] ? "#{ENV['VSCODE_PORTABLE']}/user-data" : "#{ENV.fetch('APPDATA', nil)}/Code"}/User/globalStorage/state.vscdb"
20
+ raise "Cannot find #{state_db}" unless File.exist?(state_db)
21
+
22
+ # Open SQLite database and query for our extension's key
23
+ db = SQLite3::Database.new(state_db)
24
+ db.results_as_hash = true
25
+ row = db.get_first_row('SELECT value FROM ItemTable WHERE key = ?', 'saoudrizwan.claude-dev')
26
+ db.close
27
+ raise 'Key \'saoudrizwan.claude-dev\' not found in database.' unless row
28
+
29
+ "Cline (#{JSON.parse(row['value'])['actModeOpenRouterModelId']})"
30
+ end
31
+
32
+ # Return the agent signature that is added in any description authored by the AI agent
33
+ #
34
+ # @return [String] AI agent signature
35
+ def self.agent_signature
36
+ "\n\nCo-authored by: #{agent_name}"
37
+ end
38
+ end