githuh 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.
@@ -34,19 +34,18 @@ module Githuh
34
34
  per_page: DEFAULT_PAGE_SIZE,
35
35
  verbose: false,
36
36
  info: true)
37
-
38
37
  self.context = Githuh
39
38
  self.verbose = verbose
40
39
  self.info = info
41
40
  self.token = api_token || determine_github_token
42
- self.per_page = per_page.to_i || DEFAULT_PAGE_SIZE
41
+ self.per_page = per_page.to_i
43
42
 
44
- if info
45
- begin
46
- print_userinfo
47
- rescue StandardError
48
- nil
49
- end
43
+ return unless info
44
+
45
+ begin
46
+ print_userinfo
47
+ rescue StandardError
48
+ nil
50
49
  end
51
50
  end
52
51
 
@@ -56,12 +55,12 @@ module Githuh
56
55
 
57
56
  protected
58
57
 
59
- def puts(*args)
60
- stdout.puts(*args)
58
+ def puts(*)
59
+ stdout.puts(*)
61
60
  end
62
61
 
63
- def warn(*args)
64
- stderr.puts(*args)
62
+ def warn(*)
63
+ stderr.puts(*)
65
64
  end
66
65
 
67
66
  def user_info
@@ -93,7 +92,7 @@ module Githuh
93
92
  end
94
93
 
95
94
  def determine_github_token
96
- @github_token ||= (ENV['GITHUB_TOKEN'] || `git config --global --get user.token`.chomp)
95
+ @github_token ||= ENV['GITHUB_TOKEN'] || `git config --global --get user.token`.chomp
97
96
 
98
97
  return @github_token unless @github_token.empty?
99
98
 
@@ -108,15 +107,18 @@ module Githuh
108
107
  def print_userinfo
109
108
  duration = DateTime.now - DateTime.parse(user_info[:created_at].to_s)
110
109
  years = (duration / 365).to_i
111
- months = ((duration - years * 365) / 30).to_i
112
- days = (duration - years * 365 - months * 30).to_i
110
+ months = ((duration - (years * 365)) / 30).to_i
111
+ days = (duration - (years * 365) - (months * 30)).to_i
113
112
 
114
113
  lines = []
115
- lines << sprintf(" Github API Token: %s", h("#{token[0..9]}#{'.' * 20}#{token[-11..-1]}"))
114
+ lines << sprintf(" Github API Token: %s", h("#{token[0..9]}#{'.' * 20}#{token[-11..]}"))
116
115
  lines << sprintf(" Current User: %s", h(user_info.login))
117
116
  lines << sprintf(" Public Repos: %s", h(user_info.public_repos.to_s))
118
117
  lines << sprintf(" Followers: %s", h(user_info.followers.to_s))
119
- lines << sprintf(" Member For: %s", h(sprintf("%d years, %d months, %d days", years, months, days)))
118
+ lines << sprintf(
119
+ " Member For: %s",
120
+ h("%<years>d years, %<months>d months, %<days>d days" % { years:, months:, days: })
121
+ )
120
122
 
121
123
  self.box = TTY::Box.frame(*lines,
122
124
  padding: 0,
@@ -6,9 +6,9 @@ require "bundler/setup"
6
6
  require "dry/cli"
7
7
  require "json"
8
8
  require "tty/progressbar"
9
- require "csv"
10
9
  require "active_support/inflector"
11
10
  require "yaml"
11
+ require "csv"
12
12
 
13
13
  require_relative "../base"
14
14
 
@@ -19,7 +19,7 @@ module Githuh
19
19
  class Export < Base
20
20
  FORMATS = {
21
21
  json: "json",
22
- csv: "csv",
22
+ csv: "csv",
23
23
  }.freeze
24
24
 
25
25
  DEFAULT_FORMAT = :csv
@@ -27,16 +27,16 @@ module Githuh
27
27
 
28
28
  attr_accessor :filename, :file, :output, :repo, :issues, :format, :record_count, :mapping
29
29
 
30
- desc "Export Repo issues into a CSV or JSON format\n" \
31
- " Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
30
+ desc "Export Repo issues into a CSV or JSON format\n " \
31
+ "Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
32
32
 
33
33
  argument :repo, type: :string, required: true, desc: 'Name of the repo, eg "rails/rails"'
34
- option :file, required: false, desc: "Output file, overrides " + DEFAULT_OUTPUT_FORMAT
34
+ option :file, required: false, desc: "Output file, overrides #{DEFAULT_OUTPUT_FORMAT}"
35
35
  option :format, values: FORMATS.keys.map(&:to_s), default: DEFAULT_FORMAT.to_s, required: false, desc: "Output format"
36
36
  option :mapping, type: :string, require: false, desc: "YAML file with label to estimates mapping"
37
37
 
38
- def call(repo: nil, file: nil, format: nil, mapping: nil, **opts)
39
- super(**opts)
38
+ def call(repo: nil, file: nil, format: nil, mapping: nil, **)
39
+ super(**)
40
40
 
41
41
  self.record_count = 0
42
42
  self.repo = repo
@@ -46,9 +46,10 @@ module Githuh
46
46
 
47
47
  self.mapping = {}
48
48
  if mapping && ::File.exist?(mapping)
49
- self.mapping = ::YAML.safe_load(::File.read(mapping))['label-to-estimates'] || {}
49
+ self.mapping = ::YAML.safe_load_file(mapping)['label-to-estimates'] || {}
50
50
  end
51
51
 
52
+ Export.send(:remove_const, :LabelEstimates) if Export.const_defined?(:LabelEstimates)
52
53
  Export.const_set(:LabelEstimates, self.mapping)
53
54
 
54
55
  self.issues = []
@@ -139,14 +140,14 @@ module Githuh
139
140
  CSV_HEADER.each do |column|
140
141
  method = column.downcase.underscore.to_sym
141
142
  value = if CSV_MAP[column]
142
- CSV_MAP[column][client, issue]
143
- else
144
- begin
145
- issue.to_h[method]
146
- rescue StandardError
147
- nil
148
- end
149
- end
143
+ CSV_MAP[column][client, issue]
144
+ else
145
+ begin
146
+ issue.to_h[method]
147
+ rescue StandardError
148
+ nil
149
+ end
150
+ end
150
151
  value = value.strip if value.is_a?(String)
151
152
  row << value
152
153
  end
@@ -166,7 +167,7 @@ module Githuh
166
167
  def print_conclusion
167
168
  puts
168
169
  puts TTY::Box.info("Success: written a total of #{record_count} records to #{filename}",
169
- **{ width: ui_width, padding: 1 })
170
+ width: ui_width, padding: 1)
170
171
  puts
171
172
  end
172
173
 
@@ -175,7 +176,7 @@ module Githuh
175
176
  puts TTY::Box.info("Format : #{self.format}\n" \
176
177
  "File : #{filename}\n" \
177
178
  "Repo : #{repo}\n",
178
- **{ width: ui_width, padding: 1 })
179
+ width: ui_width, padding: 1)
179
180
  puts
180
181
  end
181
182
 
@@ -4,10 +4,12 @@
4
4
  # vim: ft=ruby
5
5
  require 'bundler/setup'
6
6
  require 'dry/cli'
7
+ require 'base64'
7
8
  require 'json'
8
9
  require 'tty/progressbar'
9
10
 
10
11
  require_relative '../base'
12
+ require_relative '../../../llm'
11
13
 
12
14
  module Githuh
13
15
  module CLI
@@ -23,18 +25,42 @@ module Githuh
23
25
  DEFAULT_OUTPUT_FORMAT = "<username>.repositories.<format>"
24
26
  FORK_OPTIONS = %w(exclude include only).freeze
25
27
 
26
- attr_accessor :filename, :file, :output, :repos, :format, :forks, :private, :record_count
28
+ attr_accessor :filename, :file, :output, :repos, :format,
29
+ :forks, :private, :record_count, :llm_adapter
27
30
 
28
- desc "List owned repositories and render the output in markdown or JSON\n" \
29
- " Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
31
+ desc "List owned repositories and render the output in markdown or JSON\n " \
32
+ "Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
30
33
 
31
- option :file, required: false, desc: 'Output file, overrides ' + DEFAULT_OUTPUT_FORMAT
32
- option :format, values: FORMATS.keys, default: DEFAULT_FORMAT.to_s, required: false, desc: 'Output format'
33
- option :forks, type: :string, values: FORK_OPTIONS, default: FORK_OPTIONS.first, required: false, desc: 'Include or exclude forks'
34
- option :private, type: :boolean, default: nil, required: false, desc: 'If specified, returns only private repos for true, public for false'
34
+ option :file,
35
+ required: false,
36
+ desc: "Output file, overrides #{DEFAULT_OUTPUT_FORMAT}"
35
37
 
36
- def call(file: nil, format: nil, forks: nil, private: nil, **opts)
37
- super(**opts)
38
+ option :format,
39
+ values: FORMATS.keys,
40
+ default: DEFAULT_FORMAT.to_s,
41
+ required: false,
42
+ desc: 'Output format'
43
+
44
+ option :forks, type: :string,
45
+ values: FORK_OPTIONS,
46
+ default: FORK_OPTIONS.first,
47
+ required: false,
48
+ desc: 'Include or exclude forks'
49
+
50
+ option :private,
51
+ type: :boolean,
52
+ default: nil,
53
+ required: false,
54
+ desc: 'If specified, returns only private repos for true, public for false'
55
+
56
+ option :llm,
57
+ type: :boolean,
58
+ default: false,
59
+ required: false,
60
+ desc: 'Use LLM (ANTHROPIC_API_KEY or OPENAI_API_KEY) to summarize README'
61
+
62
+ def call(file: nil, format: nil, forks: nil, private: nil, llm: false, **)
63
+ super(**)
38
64
 
39
65
  self.record_count = 0
40
66
  self.forks = forks
@@ -42,6 +68,7 @@ module Githuh
42
68
  self.repos = []
43
69
  self.output = StringIO.new
44
70
  self.format = (format || DEFAULT_FORMAT).to_sym
71
+ self.llm_adapter = build_llm_adapter if llm
45
72
 
46
73
  self.filename = file || "#{user_info.login}.repositories.#{FORMATS[self.format]}"
47
74
  self.file = File.open(filename, 'w')
@@ -100,55 +127,137 @@ module Githuh
100
127
  def bar_size
101
128
  return 1 if client&.last_response.nil?
102
129
 
103
- client&.last_response.rels[:last].href.match(/page=(\d+).*$/)[1].to_i
130
+ client&.last_response&.rels&.[](:last)&.href&.match(/page=(\d+).*$/)&.[](1)&.to_i # rubocop:disable Style/SafeNavigationChainLength
104
131
  end
105
132
 
106
133
  def render_as_markdown(repositories)
107
134
  output.puts "### #{client.user.name}'s Repos\n"
135
+
136
+ llm_bar = build_llm_progress_bar(repositories.size) if llm_adapter
137
+
108
138
  repositories.each_with_index do |repo, index|
109
139
  output.puts repo_as_markdown(index, repo)
140
+ llm_bar&.advance
141
+ end
142
+
143
+ if llm_bar
144
+ llm_bar.finish
145
+ puts
110
146
  end
147
+
111
148
  output.string
112
149
  end
113
150
 
151
+ def build_llm_progress_bar(total)
152
+ return unless info || verbose
153
+
154
+ color = llm_adapter.class.const_defined?(:BAR_COLOR) ? llm_adapter.class::BAR_COLOR : :cyan
155
+ provider = llm_adapter.class.name.split('::').last
156
+
157
+ puts
158
+ puts " • Summarizing #{total} READMEs with #{provider}…".send(color)
159
+ TTY::ProgressBar.new("[:bar]",
160
+ title: 'LLM Summaries',
161
+ total: total,
162
+ width: ui_width - 2,
163
+ head: '',
164
+ complete: '▉'.send(color))
165
+ end
166
+
114
167
  def render_as_json(repositories)
115
168
  JSON.pretty_generate(repositories.map(&:to_hash))
116
169
  end
117
170
 
118
171
  def repo_as_markdown(index, repo)
172
+ description = describe(repo)
173
+
119
174
  <<~REPO
120
175
 
121
176
  ### #{index + 1}. [#{repo.name}](#{repo.url}) (#{repo.stargazers_count} ★)
122
177
 
123
- #{repo.language ? "**#{repo.language}**. " : ''}
124
- #{repo.license ? "Distributed under the **#{repo.license.name}** license." : ''}
178
+ #{"**#{repo.language}**. " if repo.language}
179
+ #{"Distributed under the **#{repo.license.name}** license." if repo.license}
125
180
 
126
- #{repo.description}
181
+ #{description}
127
182
 
128
183
  REPO
129
184
  end
130
185
 
186
+ def describe(repo)
187
+ return repo.description unless llm_adapter
188
+
189
+ readme = fetch_readme(repo)
190
+ return repo.description if readme.nil? || readme.empty?
191
+
192
+ llm_adapter.summarize(readme)
193
+ rescue StandardError => e
194
+ warn "LLM summary failed for #{repo_full_name(repo)}: #{e.message}" if verbose
195
+ repo.description
196
+ end
197
+
198
+ def fetch_readme(repo)
199
+ readme = client.readme(repo_full_name(repo))
200
+ return nil unless readme
201
+
202
+ encoded = readme.respond_to?(:content) ? readme.content : readme[:content]
203
+ return nil if encoded.nil? || encoded.to_s.empty?
204
+
205
+ Base64.decode64(encoded).force_encoding('UTF-8')
206
+ rescue StandardError => e
207
+ warn "README fetch failed for #{repo_full_name(repo)}: #{e.message}" if verbose
208
+ nil
209
+ end
210
+
211
+ def repo_full_name(repo)
212
+ repo.respond_to?(:full_name) && repo.full_name ? repo.full_name : repo.name
213
+ end
214
+
215
+ def build_llm_adapter
216
+ adapter = Githuh::LLM.build
217
+ raise Githuh::LLM::Error, '--llm was specified but neither ANTHROPIC_API_KEY nor OPENAI_API_KEY is set' unless adapter
218
+
219
+ announce_llm(adapter) if info
220
+ adapter
221
+ end
222
+
223
+ def announce_llm(adapter)
224
+ provider = adapter.class.name.split('::').last
225
+ model = adapter.class.const_defined?(:MODEL) ? adapter.class::MODEL : 'n/a'
226
+
227
+ puts
228
+ puts TTY::Box.info(
229
+ "LLM summaries: ENABLED\n" \
230
+ "Provider : #{provider}\n" \
231
+ "Model : #{model}\n" \
232
+ "\n" \
233
+ "For each repository, the README will be fetched and summarized\n" \
234
+ "into a 5-6 sentence description before writing to the output file.",
235
+ width: ui_width, padding: 1
236
+ )
237
+ puts
238
+ end
239
+
131
240
  private
132
241
 
133
242
  def filter_result!(result)
134
243
  result.reject! do |r|
135
244
  fork_reject = case forks
136
- when 'exclude'
137
- r.fork
138
- when 'only'
139
- !r.fork
140
- when 'include'
141
- false
142
- end
245
+ when 'exclude'
246
+ r.fork
247
+ when 'only'
248
+ !r.fork
249
+ when 'include'
250
+ false
251
+ end
143
252
 
144
253
  private_reject = case private
145
- when true
146
- !r.private
147
- when false
148
- r.private
149
- when nil
150
- false
151
- end
254
+ when true
255
+ !r.private
256
+ when false
257
+ r.private
258
+ when nil
259
+ false
260
+ end
152
261
 
153
262
  fork_reject || private_reject
154
263
  end
@@ -15,8 +15,8 @@ module Githuh
15
15
  class Info < Base
16
16
  desc "Print user information"
17
17
 
18
- def call(**opts)
19
- super(**opts)
18
+ def call(**)
19
+ super
20
20
  ap client.user.to_hash
21
21
  end
22
22
  end
@@ -13,12 +13,10 @@ module Githuh
13
13
  class Launcher
14
14
  attr_accessor :argv, :stdin, :stdout, :stderr, :kernel, :command
15
15
 
16
- def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = nil)
17
- if ::Githuh.launcher
18
- raise(ArgumentError, "Another instance of CLI Launcher was detected, aborting.")
19
- else
20
- Githuh.launcher = self
21
- end
16
+ def initialize(argv, stdin = $stdin, stdout = $stdout, stderr = $stderr, kernel = nil)
17
+ raise(ArgumentError, "Another instance of CLI Launcher was detected, aborting.") if ::Githuh.launcher
18
+
19
+ Githuh.launcher = self
22
20
 
23
21
  self.argv = argv
24
22
  self.stdin = stdin
@@ -28,7 +26,7 @@ module Githuh
28
26
  end
29
27
 
30
28
  def execute!
31
- if argv.empty? || !(%w(--help -h) & argv).empty?
29
+ if argv.empty? || !!%w(--help -h).intersect?(argv)
32
30
  stdout.puts BANNER
33
31
  Githuh.configure_kernel_behavior! help: true
34
32
  else
@@ -39,17 +37,15 @@ module Githuh
39
37
  self.command = ::Dry::CLI.new(::Githuh::CLI::Commands)
40
38
  command.call(arguments: argv, out: stdout, err: stderr)
41
39
  rescue StandardError => e
42
- lines = [e.message.gsub(/\n/, ', ')]
43
- if e.backtrace && !(ARGV & %w[-v --verbose]).empty?
40
+ lines = [e.message.gsub("\n", ', ')]
41
+ if e.backtrace && !!ARGV.intersect?(%w[-v --verbose])
44
42
  lines << ''
45
43
  lines.concat(e.backtrace)
46
44
  end
47
45
 
48
46
  box = TTY::Box.frame(*lines,
49
- **BOX_OPTIONS.merge(
50
- width: TTY::Screen.width,
51
- title: { top_center: "┤ #{e.class.name} ├" },
52
- ))
47
+ **BOX_OPTIONS, width: TTY::Screen.width,
48
+ title: { top_center: "┤ #{e.class.name} ├" })
53
49
  stderr.puts
54
50
  stderr.print box
55
51
  ensure
@@ -62,7 +58,7 @@ module Githuh
62
58
  end
63
59
  end
64
60
 
65
- BANNER = <<~BANNER
61
+ BANNER = <<~BANNER.freeze
66
62
 
67
63
  #{'Githuh CLI'.bold.yellow} #{::Githuh::VERSION.bold.green} — API client for Github.com.
68
64
  #{'© 2020 Konstantin Gredeskoul, All rights reserved. MIT License.'.cyan}
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Githuh
4
+ module EnvLoader
5
+ # Load a simple KEY=VALUE .env file from the current working directory
6
+ # and/or HOME, without clobbering already-set ENV entries.
7
+ def self.load!
8
+ [File.join(Dir.pwd, '.env'), File.join(Dir.home, '.env')].uniq.each do |path|
9
+ next unless File.file?(path) && File.readable?(path)
10
+
11
+ File.foreach(path) do |raw|
12
+ line = raw.strip
13
+ next if line.empty? || line.start_with?('#')
14
+
15
+ key, value = line.split('=', 2)
16
+ next if key.nil? || value.nil?
17
+
18
+ key = key.strip
19
+ value = strip_quotes(value.strip)
20
+ ENV[key] ||= value
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.strip_quotes(value)
26
+ if (value.start_with?('"') && value.end_with?('"')) ||
27
+ (value.start_with?("'") && value.end_with?("'"))
28
+ value[1..-2]
29
+ else
30
+ value
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Githuh
6
+ module LLM
7
+ class Anthropic < Base
8
+ ENDPOINT = URI('https://api.anthropic.com/v1/messages').freeze
9
+ MODEL = 'claude-haiku-4-5-20251001'
10
+ MAX_TOKENS = 800
11
+ BAR_COLOR = :yellow
12
+
13
+ def summarize(readme)
14
+ headers = {
15
+ 'x-api-key' => api_key,
16
+ 'anthropic-version' => '2023-06-01'
17
+ }
18
+ body = {
19
+ model: MODEL,
20
+ max_tokens: MAX_TOKENS,
21
+ messages: [{ role: 'user', content: prompt_for(readme) }]
22
+ }
23
+
24
+ payload = parse!(post_json(ENDPOINT, headers, body))
25
+ payload.dig('content', 0, 'text').to_s.strip
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module Githuh
8
+ module LLM
9
+ class Base
10
+ # Hard cap to keep prompts cheap and within model context.
11
+ README_CHAR_LIMIT = 12_000
12
+ REQUEST_TIMEOUT = 30
13
+
14
+ PROMPT = <<~PROMPT
15
+ You are summarizing a GitHub repository for a public directory page.
16
+ Below is the README. Write a user-friendly description of 5–6 sentences.
17
+ Focus on: what the project *is*, what problem it *solves*, who would
18
+ use it, and any notable technical approach or feature. Keep it flowing
19
+ prose — no bullet points, no headings, no quotes, no markdown syntax.
20
+ Do NOT include installation instructions, badges, license mentions,
21
+ or author credits. Return ONLY the description prose — no preamble.
22
+
23
+ README:
24
+ ---
25
+ %<readme>s
26
+ ---
27
+ PROMPT
28
+
29
+ attr_reader :api_key, :name
30
+
31
+ def initialize(api_key:)
32
+ @api_key = api_key
33
+ @name = self.class.name.split('::').last.downcase
34
+ end
35
+
36
+ # @param readme [String] raw README markdown
37
+ # @return [String] a 2-4 sentence description
38
+ def summarize(readme)
39
+ raise NotImplementedError
40
+ end
41
+
42
+ protected
43
+
44
+ def prompt_for(readme)
45
+ format(PROMPT, readme: readme.to_s[0, README_CHAR_LIMIT])
46
+ end
47
+
48
+ def post_json(uri, headers, body)
49
+ req = Net::HTTP::Post.new(uri)
50
+ headers.each { |k, v| req[k] = v }
51
+ req['Content-Type'] = 'application/json'
52
+ req.body = JSON.dump(body)
53
+
54
+ Net::HTTP.start(uri.hostname, uri.port,
55
+ use_ssl: uri.scheme == 'https',
56
+ read_timeout: REQUEST_TIMEOUT,
57
+ open_timeout: REQUEST_TIMEOUT) do |http|
58
+ http.request(req)
59
+ end
60
+ end
61
+
62
+ def parse!(response)
63
+ raise Error, "#{name} API #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
64
+
65
+ JSON.parse(response.body)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Githuh
6
+ module LLM
7
+ class OpenAI < Base
8
+ ENDPOINT = URI('https://api.openai.com/v1/chat/completions').freeze
9
+ MODEL = 'gpt-4o-mini'
10
+ MAX_TOKENS = 800
11
+ BAR_COLOR = :green
12
+
13
+ def summarize(readme)
14
+ headers = { 'Authorization' => "Bearer #{api_key}" }
15
+ body = {
16
+ model: MODEL,
17
+ max_tokens: MAX_TOKENS,
18
+ messages: [{ role: 'user', content: prompt_for(readme) }]
19
+ }
20
+
21
+ payload = parse!(post_json(ENDPOINT, headers, body))
22
+ payload.dig('choices', 0, 'message', 'content').to_s.strip
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/githuh/llm.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Githuh
4
+ module LLM
5
+ class Error < StandardError; end
6
+
7
+ # Returns an adapter instance based on available env vars.
8
+ # Priority: ANTHROPIC_API_KEY > OPENAI_API_KEY.
9
+ # Returns nil if no key is set (caller decides how to handle).
10
+ def self.build
11
+ if (key = env('ANTHROPIC_API_KEY'))
12
+ Anthropic.new(api_key: key)
13
+ elsif (key = env('OPENAI_API_KEY'))
14
+ OpenAI.new(api_key: key)
15
+ end
16
+ end
17
+
18
+ def self.available?
19
+ !build.nil?
20
+ end
21
+
22
+ def self.env(name)
23
+ value = ENV.fetch(name, nil)
24
+ value && !value.strip.empty? ? value.strip : nil
25
+ end
26
+ end
27
+ end
28
+
29
+ require_relative 'llm/base'
30
+ require_relative 'llm/anthropic'
31
+ require_relative 'llm/openai'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Githuh
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end