locallingo 0.2.2 → 0.3.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: 931aa3bf94ee48a2021c8ec6f5de047e593f2f94bc5ba01e20b48bd1b2e96d3c
4
- data.tar.gz: fcbab1174c9dbefb317fdae26a0c0a763f4a3b8026b19f8fe8f08a54d222f8fb
3
+ metadata.gz: ff4c0e72d7b3eb6d699b7cff03d8e2b8a61efb8a1c94c16cc906f2843b4dc53c
4
+ data.tar.gz: c4419498f0f7de3b334efab4c89471eb3a6a27da543992b5171aa3b67760c3d6
5
5
  SHA512:
6
- metadata.gz: 9bc87c498dcc47fbda209f58005ce265f09ef5432107bbced4f9185e32ae51873b6aae014f7f11f650cd4905328169cb32e8f477c9ab1d310f2b4692da350ce8
7
- data.tar.gz: 65f2c9cbf024baa4cb89202da61fff2d4feb0de544c2ea85371525714e83a322ac93f065ae08239e67f562d0158b0ddf01acd199e9d9060614f3c02afa8f37a5
6
+ metadata.gz: 7820e2f62b6784ae546cd7521303e434a0319099f1ac69fa7471aff9cb786ff3b844f1200ae711eab36df7e058b31742f03de231c5bd95d460889c1027209628
7
+ data.tar.gz: b727c0b6f89916f328f3c2b00831263d779b7a7139f478571d7b2e9015326ef8841873da7e72ddd6f963e7f0009dafe9cc4630c1d6883d091bb165672d70fc67
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  ## [Unreleased]
8
8
 
9
9
  ### Added
10
+ - `Locallingo.configure { |c| c.anthropic_api_key = ... }` — gem-level provider
11
+ credentials as Strings or lazy callables, for apps whose keys don't live in
12
+ ENV (Rails credentials, app config objects, vaults). Precedence:
13
+ `Locallingo.configure` → host `RubyLLM.configure` → ENV.
14
+ - `.locallingo.rb` setup file: the CLI loads it from the project root before
15
+ dispatch, so standalone `lingo` runs can configure credentials without
16
+ booting Rails.
10
17
  - Initial extraction from the `bin/translate` toolchain into a standalone gem.
11
18
  - `lingo` CLI with subcommands: `status`, `translate`, `validate`, `quality`,
12
19
  `fix-quality`, `accept-edits`, `hash`, `sync`. Legacy `--flag` forms still work
data/README.md CHANGED
@@ -32,8 +32,21 @@ group :development do
32
32
  end
33
33
  ```
34
34
 
35
- RubyLLM reads provider credentials from ENV (e.g. `OPENAI_API_KEY`,
36
- `ANTHROPIC_API_KEY`). Locallingo never stores keys.
35
+ Provider credentials come from ENV (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`)
36
+ or from Ruby, for apps whose keys live elsewhere (Rails credentials, an app
37
+ config object, a vault):
38
+
39
+ ```ruby
40
+ Locallingo.configure do |config|
41
+ config.anthropic_api_key = "sk-ant-..." # a String…
42
+ config.openai_api_key = -> { AppConf.openai_key } # …or a lazy callable
43
+ end
44
+ ```
45
+
46
+ Put that in a `.locallingo.rb` file next to `.locallingo.yml` and the `lingo`
47
+ CLI loads it on start — no Rails boot required. A key set via
48
+ `Locallingo.configure` wins over one set through `RubyLLM.configure`, which
49
+ wins over ENV. Locallingo never stores keys itself.
37
50
 
38
51
  ## Configuration
39
52
 
@@ -16,6 +16,11 @@ module Locallingo
16
16
  class CLI
17
17
  CLI_NAME = "lingo"
18
18
 
19
+ # Optional Ruby setup file loaded before dispatch — the hook for apps to
20
+ # configure credentials (via Locallingo.configure or RubyLLM.configure)
21
+ # without booting Rails.
22
+ SETUP_FILENAME = ".locallingo.rb"
23
+
19
24
  # subcommand => the legacy flag it replaces
20
25
  COMMANDS = {
21
26
  "status" => "--status",
@@ -52,6 +57,7 @@ module Locallingo
52
57
  def run
53
58
  command = resolve_command
54
59
  options = parse_options!
60
+ load_setup_file
55
61
  config = Locallingo.configuration(root_path: Dir.pwd, package: options[:package])
56
62
  dispatch(command, config, options)
57
63
  rescue Locallingo::Error => e
@@ -61,6 +67,13 @@ module Locallingo
61
67
 
62
68
  private
63
69
 
70
+ # An absent file is fine; errors in the file propagate loudly. `load` (not
71
+ # require) so repeated in-process invocations re-execute it.
72
+ def load_setup_file
73
+ path = File.join(Dir.pwd, SETUP_FILENAME)
74
+ load path if File.file?(path)
75
+ end
76
+
64
77
  # Determine the subcommand, translating a leading legacy flag (with a
65
78
  # deprecation notice) and defaulting to `status`.
66
79
  def resolve_command
@@ -31,14 +31,14 @@ module Locallingo
31
31
  @provider = provider.to_sym
32
32
  end
33
33
 
34
- # True when credentials for the configured provider are present in ENV.
35
- # Unknown providers are assumed configured (RubyLLM may source the key
36
- # elsewhere) rather than blocking.
34
+ # True when a key for the configured provider is found in any source
35
+ # (Locallingo settings, host RubyLLM config, ENV). Unknown providers are
36
+ # assumed configured (RubyLLM may source the key elsewhere) rather than
37
+ # blocking.
37
38
  def credentials?
38
- env = CREDENTIAL_ENV[provider]
39
- return true unless env
39
+ return true unless CREDENTIAL_ENV.key?(provider)
40
40
 
41
- !ENV.fetch(env, "").to_s.strip.empty?
41
+ !resolved_api_key.nil?
42
42
  end
43
43
 
44
44
  # Raise a precise error when the provider has no credentials.
@@ -46,8 +46,10 @@ module Locallingo
46
46
  return if credentials?
47
47
 
48
48
  raise MissingCredentialsError,
49
- "No credentials for provider #{provider.inspect} " \
50
- "(expected #{CREDENTIAL_ENV[provider]} in ENV)"
49
+ "No credentials for provider #{provider.inspect}. " \
50
+ "Set #{CREDENTIAL_ENV[provider]} in ENV, call " \
51
+ "Locallingo.configure { |c| c.#{provider}_api_key = ... }, or add a " \
52
+ ".locallingo.rb setup file at the project root (loaded by the CLI)."
51
53
  end
52
54
 
53
55
  # Send +instructions+ (system prompt) + +payload+ (user message, JSON) to
@@ -70,20 +72,55 @@ module Locallingo
70
72
 
71
73
  # RubyLLM does not read provider API keys from ENV on its own, so a
72
74
  # standalone CLI run (no Rails initializer to call RubyLLM.configure)
73
- # would raise "Missing configuration for <provider>". Fill the
74
- # provider's key from ENV unless the host app already configured
75
- # one, which always wins.
75
+ # would raise "Missing configuration for <provider>". Push the resolved
76
+ # key into RubyLLM's config: an explicit `Locallingo.configure` key wins
77
+ # (re-resolved every chat so callables stay live), then a key the host
78
+ # app already set, then the ENV fallback.
76
79
  def configure_credentials!
77
- env = CREDENTIAL_ENV[provider]
78
- return unless env
80
+ return unless CREDENTIAL_ENV.key?(provider)
79
81
 
80
- setting = "#{provider}_api_key"
81
82
  config = ::RubyLLM.config
83
+ setting = key_setting
82
84
  return unless config.respond_to?(setting) && config.respond_to?("#{setting}=")
83
- return unless config.public_send(setting).to_s.strip.empty?
84
85
 
85
- key = ENV.fetch(env, "")
86
- config.public_send("#{setting}=", key) unless key.strip.empty?
86
+ key = settings_api_key
87
+ return if key.nil? && !presence(config.public_send(setting)).nil?
88
+
89
+ key ||= env_api_key
90
+ config.public_send("#{setting}=", key) unless key.nil?
91
+ end
92
+
93
+ # Key resolution across sources, in precedence order. Used by
94
+ # #credentials? as a fail-fast check before any network call.
95
+ def resolved_api_key
96
+ settings_api_key || host_configured_api_key || env_api_key
97
+ end
98
+
99
+ def settings_api_key
100
+ Locallingo.settings.api_key_for(provider)
101
+ end
102
+
103
+ # A key the host app set via RubyLLM.configure — only inspected when
104
+ # ruby_llm is already loaded (we never require it just to peek).
105
+ def host_configured_api_key
106
+ return nil unless defined?(::RubyLLM)
107
+
108
+ config = ::RubyLLM.config
109
+ return nil unless config.respond_to?(key_setting)
110
+
111
+ presence(config.public_send(key_setting))
112
+ end
113
+
114
+ def env_api_key
115
+ env = CREDENTIAL_ENV[provider]
116
+ env ? presence(ENV.fetch(env, "")) : nil
117
+ end
118
+
119
+ def key_setting = "#{provider}_api_key"
120
+
121
+ def presence(value)
122
+ key = value.to_s.strip
123
+ key.empty? ? nil : key
87
124
  end
88
125
  end
89
126
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locallingo
4
+ # Code-level gem settings, configured via `Locallingo.configure`. Distinct
5
+ # from Locallingo::Configuration, which loads the `.locallingo.yml` project
6
+ # file — Settings holds what must never live in YAML: provider credentials.
7
+ #
8
+ # Locallingo.configure do |config|
9
+ # config.anthropic_api_key = ENV.fetch("MY_KEY") # a String…
10
+ # config.openai_api_key = -> { Vault.read("openai_key") } # …or a callable
11
+ # end
12
+ #
13
+ # Callables are resolved lazily on every use (never memoized), so keys can
14
+ # come from sources that aren't ready at configure time or that rotate.
15
+ class Settings
16
+ PROVIDERS = %i[openai anthropic gemini deepseek openrouter].freeze
17
+
18
+ attr_accessor(*PROVIDERS.map { |name| :"#{name}_api_key" })
19
+
20
+ # The usable key for +provider+: callables are called, results stripped,
21
+ # and blank or unknown-provider values come back as nil.
22
+ def api_key_for(provider)
23
+ return nil unless PROVIDERS.include?(provider.to_sym)
24
+
25
+ value = public_send("#{provider}_api_key")
26
+ value = value.call if value.respond_to?(:call)
27
+ key = value.to_s.strip
28
+ key.empty? ? nil : key
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locallingo
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/locallingo.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "locallingo/version"
4
+ require_relative "locallingo/settings"
4
5
  require_relative "locallingo/configuration"
5
6
  require_relative "locallingo/json_extraction"
6
7
  require_relative "locallingo/key_flattener"
@@ -35,4 +36,21 @@ module Locallingo
35
36
  def self.configuration(root_path: Dir.pwd, package: nil)
36
37
  Configuration.load(root_path:, package:)
37
38
  end
39
+
40
+ # Code-level settings (provider credentials) — see Locallingo::Settings.
41
+ def self.settings
42
+ @settings ||= Settings.new
43
+ end
44
+
45
+ # Configure the gem from Ruby — the credentials path for apps whose keys
46
+ # don't live in ENV (call it from a Rails initializer or a `.locallingo.rb`
47
+ # setup file next to `.locallingo.yml`; the CLI loads the latter on start).
48
+ def self.configure
49
+ yield settings
50
+ settings
51
+ end
52
+
53
+ def self.reset_settings!
54
+ @settings = nil
55
+ end
38
56
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: locallingo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -81,6 +81,7 @@ files:
81
81
  - lib/locallingo/quality_checker.rb
82
82
  - lib/locallingo/reporter.rb
83
83
  - lib/locallingo/rubocop.rb
84
+ - lib/locallingo/settings.rb
84
85
  - lib/locallingo/state_store.rb
85
86
  - lib/locallingo/validators/duplicate_values.rb
86
87
  - lib/locallingo/validators/manual_edits.rb