agent_settings 0.1.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 +7 -0
- data/AGENTS.md +244 -0
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +95 -0
- data/README.md +177 -0
- data/lib/agent_settings/adapters/claude.rb +170 -0
- data/lib/agent_settings/adapters/codex.rb +157 -0
- data/lib/agent_settings/adapters/opencode.rb +185 -0
- data/lib/agent_settings/agent_config_path.rb +51 -0
- data/lib/agent_settings/env_override.rb +61 -0
- data/lib/agent_settings/location.rb +38 -0
- data/lib/agent_settings/location_discovery.rb +119 -0
- data/lib/agent_settings/registry.rb +39 -0
- data/lib/agent_settings/version.rb +6 -0
- data/lib/agent_settings.rb +152 -0
- data/llm.md +165 -0
- metadata +77 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 15e2352fd94c8dbf7fe85836e0c9b48a0dd8cc09e631674949ef301a28083dbc
|
|
4
|
+
data.tar.gz: 8f618aba7e01370d35ec3d5a3cb66c4f7f637788487cb656dea2449c3334b96a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14c65ca62831a314b49f92157cbe229c51da19d333274f16545a3b459450d7e789a4d21b7a9fd2a9a010d6029f8a27de08920a665bb16007d7b0aaad2641d645
|
|
7
|
+
data.tar.gz: a778b94ef5bcd36571c8e91e8cf7c18cfe2fe7272a4f9bd573624806a94c22d246cd2ced383304d3ca3d41f9657d8d2b724dc0ce539ec575c5c34d6cebd27cc6
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Agent Settings - Ruby Gem
|
|
2
|
+
|
|
3
|
+
Ruby gem that provides a clean interface to discover config locations for Claude Code, OpenCode, and Codex — including whether each path exists and which path is currently effective based on precedence and overrides.
|
|
4
|
+
|
|
5
|
+
Read @doc/ruby.md before writing any Ruby code.
|
|
6
|
+
|
|
7
|
+
## Build / Lint / Test Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install dependencies
|
|
11
|
+
bundle install
|
|
12
|
+
|
|
13
|
+
# Run all tests
|
|
14
|
+
bundle exec rake test
|
|
15
|
+
|
|
16
|
+
# Run a single test file
|
|
17
|
+
bundle exec ruby -Ilib:test test/agent_settings/resolver_test.rb
|
|
18
|
+
|
|
19
|
+
# Run a single test by name
|
|
20
|
+
bundle exec ruby -Ilib:test test/agent_settings/resolver_test.rb --name test_global_resolution
|
|
21
|
+
|
|
22
|
+
# Run tests with verbose output
|
|
23
|
+
bundle exec rake test TESTOPTS="--verbose"
|
|
24
|
+
|
|
25
|
+
# Lint with RuboCop (if configured)
|
|
26
|
+
bundle exec rubocop
|
|
27
|
+
|
|
28
|
+
# Lint and auto-correct
|
|
29
|
+
bundle exec rubocop -a
|
|
30
|
+
|
|
31
|
+
# Build the gem
|
|
32
|
+
bundle exec rake build
|
|
33
|
+
|
|
34
|
+
# Install the gem locally
|
|
35
|
+
bundle exec rake install
|
|
36
|
+
|
|
37
|
+
# Run the console for experimentation
|
|
38
|
+
bundle exec console
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Project Structure
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
lib/agent_settings.rb # Entry point with Zeitwerk loader
|
|
45
|
+
lib/agent_settings/version.rb # Version constant
|
|
46
|
+
lib/agent_settings/location.rb # Location value object
|
|
47
|
+
lib/agent_settings/env_override.rb # EnvOverride value object
|
|
48
|
+
lib/agent_settings/agent_config_path.rb # AgentConfigPath value object
|
|
49
|
+
lib/agent_settings/location_discovery.rb # Core discovery logic
|
|
50
|
+
lib/agent_settings/registry.rb # Agent-to-adapter mapping
|
|
51
|
+
lib/agent_settings/adapters/ # Per-agent adapters
|
|
52
|
+
claude.rb
|
|
53
|
+
opencode.rb
|
|
54
|
+
codex.rb
|
|
55
|
+
test/ # Minitest tests
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Public API
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
AgentSettings.global(agent, env: ENV, dir: Dir.pwd, trusted: true)
|
|
62
|
+
AgentSettings.project(agent, dir:, env: ENV, trusted: true)
|
|
63
|
+
AgentSettings.resolve(agent, dir:, env: ENV, trusted: true)
|
|
64
|
+
AgentSettings.all(dir:, env: ENV, trusted: true)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Supported agents: `:claude`, `:opencode`, `:codex`
|
|
68
|
+
|
|
69
|
+
## Code Style Guidelines
|
|
70
|
+
|
|
71
|
+
### Ruby 3.x Features
|
|
72
|
+
|
|
73
|
+
Use modern Ruby features throughout:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Data objects for immutable value objects
|
|
77
|
+
Location = Data.define(:agent, :scope, :path, :exists, :active) do
|
|
78
|
+
def exists? = exists
|
|
79
|
+
def active? = active
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pattern matching
|
|
83
|
+
case agent
|
|
84
|
+
in :claude then ClaudeAdapter.new
|
|
85
|
+
in :opencode then OpencodeAdapter.new
|
|
86
|
+
in :codex then CodexAdapter.new
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Endless methods for simple one-liners
|
|
90
|
+
def global = layers.find { |l| l.scope == :global }
|
|
91
|
+
def custom_config? = variables.any?(&:active?)
|
|
92
|
+
|
|
93
|
+
# Keyword arguments throughout
|
|
94
|
+
def resolve(agent, dir:, env: ENV, trusted: true)
|
|
95
|
+
|
|
96
|
+
# Safe navigation operator
|
|
97
|
+
path = env["CLAUDE_CONFIG_DIR"]&.then { |dir| File.join(dir, "settings.json") }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Naming Conventions
|
|
101
|
+
|
|
102
|
+
- **Boolean methods** end with `?`: `exists?`, `active?`, `custom_config?`
|
|
103
|
+
- **Positive names**: use `active` not `not_deleted`
|
|
104
|
+
- **Role-based naming**: name variables after their role, not type
|
|
105
|
+
- **No pattern names in classes**: avoid `ResolverFactory`, `LocationDecorator`
|
|
106
|
+
- **Domain language**: reflect the business domain in names
|
|
107
|
+
|
|
108
|
+
### Method Design
|
|
109
|
+
|
|
110
|
+
- **Composed methods**: each method does one thing at one abstraction level
|
|
111
|
+
- **Intention-revealing selectors**: name after what, not how
|
|
112
|
+
- **Keyword arguments**: always prefer over positional arguments
|
|
113
|
+
- **Tiny public surface**: expose only what callers need
|
|
114
|
+
|
|
115
|
+
### Class Design
|
|
116
|
+
|
|
117
|
+
- **Single responsibility**: one sentence description, no "and/or"
|
|
118
|
+
- **Cohesion**: everything in a class shares one idea
|
|
119
|
+
- **Composition over inheritance**: prefer composition
|
|
120
|
+
- **Law of Demeter**: only talk to immediate neighbors
|
|
121
|
+
- **Tell, don't ask**: send messages, avoid train wrecks
|
|
122
|
+
|
|
123
|
+
### Value Objects
|
|
124
|
+
|
|
125
|
+
Use `Data.define` for immutable value objects:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
module AgentSettings
|
|
129
|
+
Location = Data.define(:agent, :scope, :source, :path, :exists, :active, :note) do
|
|
130
|
+
def exists? = exists
|
|
131
|
+
def active? = active
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
EnvOverride = Data.define(:agent, :name, :value, :path, :exists, :active, :note) do
|
|
135
|
+
def exists? = exists
|
|
136
|
+
def active? = active
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
AgentConfigPath = Data.define(:agent, :effective, :global, :project, :layers, :env_overrides, :warnings) do
|
|
140
|
+
def custom_config? = env_overrides.any?(&:active?)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Error Handling
|
|
146
|
+
|
|
147
|
+
Use custom error classes for domain errors:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
module AgentSettings
|
|
151
|
+
class Error < StandardError; end
|
|
152
|
+
class UnknownAgentError < Error; end
|
|
153
|
+
class InvalidConfigError < Error; end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Raise with clear messages
|
|
157
|
+
raise UnknownAgentError, "Unknown agent: #{agent}. Supported: #{Registry::AGENTS.join(', ')}"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Adapter Protocol
|
|
161
|
+
|
|
162
|
+
Each adapter must implement:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
class SomeAdapter
|
|
166
|
+
# Returns array of Location objects, ordered by precedence (highest first)
|
|
167
|
+
def layers(dir:, env:, trusted:) = [...]
|
|
168
|
+
|
|
169
|
+
# Returns array of EnvOverride objects for env overrides
|
|
170
|
+
def env_overrides(dir:, env:, trusted:) = [...]
|
|
171
|
+
|
|
172
|
+
# Returns array of warning strings
|
|
173
|
+
def warnings(dir:, env:, trusted:) = []
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Agent Config Rules
|
|
178
|
+
|
|
179
|
+
### Claude Code
|
|
180
|
+
- Global: `~/.claude/settings.json`
|
|
181
|
+
- Project: `.claude/settings.json`
|
|
182
|
+
- Local override: `.claude/settings.local.json`
|
|
183
|
+
- Managed: `/Library/Application Support/ClaudeCode/managed-settings.json` (macOS)
|
|
184
|
+
- Env override: `CLAUDE_CONFIG_DIR`
|
|
185
|
+
- Precedence: managed > CLI > local project > project > user
|
|
186
|
+
|
|
187
|
+
### OpenCode
|
|
188
|
+
- Global: `~/.config/opencode/opencode.json`
|
|
189
|
+
- Project: `opencode.json` (discovered upward from dir)
|
|
190
|
+
- Env overrides: `OPENCODE_CONFIG`, `OPENCODE_CONFIG_DIR`, `OPENCODE_CONFIG_CONTENT`
|
|
191
|
+
|
|
192
|
+
### Codex
|
|
193
|
+
- User: `~/.codex/config.toml`
|
|
194
|
+
- Project: `.codex/config.toml` (trusted projects only)
|
|
195
|
+
- System: `/etc/codex/config.toml`
|
|
196
|
+
- Env override: `CODEX_HOME`
|
|
197
|
+
- Trust caveat: project config ignored for untrusted projects
|
|
198
|
+
|
|
199
|
+
## Testing Strategy
|
|
200
|
+
|
|
201
|
+
- **Test public interfaces only**: test what callers see
|
|
202
|
+
- **Test private methods indirectly**: through public API
|
|
203
|
+
- **Cover these scenarios**:
|
|
204
|
+
- Global/project resolution per agent
|
|
205
|
+
- Override variable detection and activation
|
|
206
|
+
- Effective path precedence
|
|
207
|
+
- `exists?` true/false for paths
|
|
208
|
+
- Trusted vs untrusted Codex behavior
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
class LocationDiscoveryTest < Minitest::Test
|
|
212
|
+
def test_global_resolution_for_claude
|
|
213
|
+
result = AgentSettings.global(:claude)
|
|
214
|
+
assert_equal :global, result.scope
|
|
215
|
+
assert_predicate result, :exists?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def test_project_config_ignored_for_untrusted_codex
|
|
219
|
+
result = AgentSettings.resolve(:codex, dir: "/untrusted/repo", trusted: false)
|
|
220
|
+
assert_nil result.project
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Dependencies
|
|
226
|
+
|
|
227
|
+
- **Runtime**: `zeitwerk` (autoloading)
|
|
228
|
+
- **Development**: `minitest`, `rake`, `rubocop` (optional)
|
|
229
|
+
|
|
230
|
+
Use Zeitwerk at entry point:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# lib/agent_settings.rb
|
|
234
|
+
require "zeitwerk"
|
|
235
|
+
Zeitwerk::Loader.for_gem.setup
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Design Principles
|
|
239
|
+
|
|
240
|
+
1. **Messaging over objects**: focus on messages passing between objects
|
|
241
|
+
2. **Behavior over data**: depend on what objects do, not what they are
|
|
242
|
+
3. **Duck typing**: type is defined by behavior, not class
|
|
243
|
+
4. **Dependency injection**: pass collaborators, don't hard-code class names
|
|
244
|
+
5. **Late binding**: allow objects to hide internal state-process
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2025-02-24
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Discover config locations for Claude Code, OpenCode, and Codex
|
|
8
|
+
- Global and project config resolution
|
|
9
|
+
- Environment variable override detection
|
|
10
|
+
- Precedence-based active config determination
|
|
11
|
+
- Trusted/untrusted project handling for Codex
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# agent_settings
|
|
2
|
+
|
|
3
|
+
Ruby gem that provides a clean interface to discover config locations for Claude Code, OpenCode, and Codex — including whether each path exists and which path is currently effective based on precedence and overrides.
|
|
4
|
+
|
|
5
|
+
Read @doc/ruby.md before writing any Ruby code.
|
|
6
|
+
|
|
7
|
+
## Public API
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
AgentSettings.global(agent, env: ENV, dir: Dir.pwd, trusted: true)
|
|
11
|
+
AgentSettings.project(agent, dir:, env: ENV, trusted: true)
|
|
12
|
+
AgentSettings.resolve(agent, dir:, env: ENV, trusted: true)
|
|
13
|
+
AgentSettings.all(dir:, env: ENV, trusted: true)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## File Layout
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
lib/agent_settings.rb
|
|
20
|
+
lib/agent_settings/version.rb
|
|
21
|
+
lib/agent_settings/location.rb
|
|
22
|
+
lib/agent_settings/variable.rb
|
|
23
|
+
lib/agent_settings/resolution.rb
|
|
24
|
+
lib/agent_settings/resolver.rb
|
|
25
|
+
lib/agent_settings/registry.rb
|
|
26
|
+
lib/agent_settings/adapters/claude.rb
|
|
27
|
+
lib/agent_settings/adapters/opencode.rb
|
|
28
|
+
lib/agent_settings/adapters/codex.rb
|
|
29
|
+
test/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Core Value Objects
|
|
33
|
+
|
|
34
|
+
- `Location` — fields: `agent, scope, source, path, exists, active, note`; methods: `exists?`, `active?`
|
|
35
|
+
- `Variable` — fields: `agent, name, value, path, exists, active, note`; methods: `exists?`, `active?`
|
|
36
|
+
- `Resolution` — fields: `agent, effective, global, project, layers, variables, warnings`; method: `custom_config?`
|
|
37
|
+
|
|
38
|
+
## Core Classes
|
|
39
|
+
|
|
40
|
+
- `Resolver` — builds final `Resolution`, picks effective location from precedence layers
|
|
41
|
+
- `Registry` — maps agent symbols to adapters
|
|
42
|
+
- `Adapters::Claude`, `Adapters::Opencode`, `Adapters::Codex` — each exposes `layers(dir:, env:, trusted:)`, `variables(dir:, env:, trusted:)`, `warnings(dir:, env:, trusted:)`
|
|
43
|
+
|
|
44
|
+
## Agent Config Rules
|
|
45
|
+
|
|
46
|
+
### Claude Code
|
|
47
|
+
- Global: `~/.claude/settings.json`
|
|
48
|
+
- Project: `.claude/settings.json`
|
|
49
|
+
- Local project override: `.claude/settings.local.json`
|
|
50
|
+
- Managed/system: `/Library/Application Support/ClaudeCode/managed-settings.json` (macOS), `/etc/claude-code/managed-settings.json` (Linux)
|
|
51
|
+
- Env override: `CLAUDE_CONFIG_DIR`
|
|
52
|
+
- Precedence (high→low): managed > CLI > local project > project > user
|
|
53
|
+
|
|
54
|
+
### OpenCode
|
|
55
|
+
- Global: `~/.config/opencode/opencode.json`
|
|
56
|
+
- Project: `opencode.json` (discovered from dir toward project root)
|
|
57
|
+
- Env overrides: `OPENCODE_CONFIG`, `OPENCODE_CONFIG_DIR`, `OPENCODE_CONFIG_CONTENT`
|
|
58
|
+
|
|
59
|
+
### Codex
|
|
60
|
+
- User: `~/.codex/config.toml`
|
|
61
|
+
- Project: `.codex/config.toml` (trusted projects only)
|
|
62
|
+
- System: `/etc/codex/config.toml`
|
|
63
|
+
- Env base override: `CODEX_HOME`
|
|
64
|
+
|
|
65
|
+
## Ruby Conventions
|
|
66
|
+
|
|
67
|
+
Use Ruby 3.x features:
|
|
68
|
+
- `Data` objects for immutable value objects (`Location`, `Variable`, `Resolution`)
|
|
69
|
+
- Pattern matching (`case/in`, `=>`)
|
|
70
|
+
- Endless methods for simple one-liners
|
|
71
|
+
- Keyword arguments throughout
|
|
72
|
+
- Safe navigation operator (`&.`)
|
|
73
|
+
|
|
74
|
+
Design principles (Sandi Metz / OOD):
|
|
75
|
+
- Tiny public surface — expose only what callers need
|
|
76
|
+
- Intention-revealing names — reflect domain, not implementation
|
|
77
|
+
- Single responsibility — one sentence description per class, no "and/or"
|
|
78
|
+
- Tell, don't ask — send messages, avoid train wrecks
|
|
79
|
+
- Depend on behavior (duck types), not class names
|
|
80
|
+
- Composed methods — each method does one thing at one level of abstraction
|
|
81
|
+
|
|
82
|
+
Naming rules:
|
|
83
|
+
- Boolean methods end with `?` (e.g., `exists?`, `active?`, `custom_config?`)
|
|
84
|
+
- Positive names (`active` not `not_deleted`)
|
|
85
|
+
- Variables named for their role, not their type
|
|
86
|
+
- No design pattern names in class names
|
|
87
|
+
|
|
88
|
+
## Testing (Minitest)
|
|
89
|
+
|
|
90
|
+
- Test public interfaces only; test private methods indirectly
|
|
91
|
+
- Cover: global/project resolution per agent, override variable detection, effective path precedence, `exists?` true/false, trusted vs untrusted Codex behavior
|
|
92
|
+
|
|
93
|
+
## Dependencies
|
|
94
|
+
|
|
95
|
+
- `zeitwerk` runtime dependency; use `Zeitwerk::Loader.for_gem` in entrypoint
|
data/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# agent_settings
|
|
2
|
+
|
|
3
|
+
Discover config locations for Claude Code, OpenCode, and Codex
|
|
4
|
+
|
|
5
|
+
If you are an LLM/AI Agent read [./llm.md](llm.md) in this repository.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's **Gemfile**:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "agent_settings"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And run:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install agent_settings
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
Get the effective config for an agent:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "agent_settings"
|
|
33
|
+
|
|
34
|
+
result = AgentSettings.resolve(:claude, dir: Dir.pwd)
|
|
35
|
+
puts result.effective.path
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Get Global Config
|
|
41
|
+
|
|
42
|
+
Retrieve the global config location for an agent:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
location = AgentSettings.global(:claude)
|
|
46
|
+
location.path # => "/Users/me/.claude/settings.json"
|
|
47
|
+
location.exists? # => true
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Get Project Config
|
|
51
|
+
|
|
52
|
+
Retrieve the project-level config location:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
location = AgentSettings.project(:codex, dir: "/work/my_app")
|
|
56
|
+
location.path # => "/work/my_app/.codex/config.toml"
|
|
57
|
+
location.exists? # => false
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Resolve Effective Config
|
|
61
|
+
|
|
62
|
+
Get the active config with full details:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
result = AgentSettings.resolve(:opencode, dir: "/work/my_app")
|
|
66
|
+
|
|
67
|
+
result.effective.path # the active config path
|
|
68
|
+
result.global # global location
|
|
69
|
+
result.project # project location
|
|
70
|
+
result.layers # all config layers by precedence
|
|
71
|
+
result.env_overrides # environment variable overrides
|
|
72
|
+
result.warnings # any warnings
|
|
73
|
+
result.custom_config? # true if env override is active
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Resolve All Agents
|
|
77
|
+
|
|
78
|
+
Get config paths for all supported agents:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
results = AgentSettings.all(dir: "/work/my_app")
|
|
82
|
+
|
|
83
|
+
results.each do |agent, config_path|
|
|
84
|
+
puts "#{agent}: #{config_path.effective.path}"
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Detect Environment Overrides
|
|
89
|
+
|
|
90
|
+
Check if environment variables override config paths:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
result = AgentSettings.resolve(:opencode, dir: Dir.pwd, env: ENV)
|
|
94
|
+
|
|
95
|
+
result.env_overrides.each do |override|
|
|
96
|
+
puts "#{override.name}=#{override.value}"
|
|
97
|
+
puts " active: #{override.active?}"
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Handle Untrusted Projects
|
|
102
|
+
|
|
103
|
+
Codex ignores project config for untrusted projects:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# trusted (default) - includes project config
|
|
107
|
+
result = AgentSettings.resolve(:codex, dir: dir, trusted: true)
|
|
108
|
+
|
|
109
|
+
# untrusted - excludes project config
|
|
110
|
+
result = AgentSettings.resolve(:codex, dir: dir, trusted: false)
|
|
111
|
+
result.warnings # => ["Project config ignored for untrusted project"]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Supported Agents
|
|
115
|
+
|
|
116
|
+
| Agent | Global Config | Project Config |
|
|
117
|
+
|-------|---------------|----------------|
|
|
118
|
+
| Claude | `~/.claude/settings.json` | `.claude/settings.json` |
|
|
119
|
+
| OpenCode | `~/.config/opencode/opencode.json` | `opencode.json` |
|
|
120
|
+
| Codex | `~/.codex/config.toml` | `.codex/config.toml` |
|
|
121
|
+
|
|
122
|
+
## Options
|
|
123
|
+
|
|
124
|
+
All methods accept these keyword arguments:
|
|
125
|
+
|
|
126
|
+
| Option | Type | Default | Description |
|
|
127
|
+
|--------|------|---------|-------------|
|
|
128
|
+
| `agent` | Symbol | required | `:claude`, `:opencode`, or `:codex` |
|
|
129
|
+
| `dir` | String | `Dir.pwd` | Project directory path |
|
|
130
|
+
| `env` | Hash | `ENV` | Environment variables hash |
|
|
131
|
+
| `trusted` | Boolean | `true` | Trust project for Codex |
|
|
132
|
+
|
|
133
|
+
## Value Objects
|
|
134
|
+
|
|
135
|
+
### Location
|
|
136
|
+
|
|
137
|
+
Represents a config file location:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
location.agent # => :claude
|
|
141
|
+
location.scope # => :global or :project
|
|
142
|
+
location.path # => "/Users/me/.claude/settings.json"
|
|
143
|
+
location.exists? # => true
|
|
144
|
+
location.active? # => true
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### EnvOverride
|
|
148
|
+
|
|
149
|
+
Represents an environment variable override:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
override.name # => "CLAUDE_CONFIG_DIR"
|
|
153
|
+
override.value # => "/custom/path"
|
|
154
|
+
override.path # => "/custom/path/settings.json"
|
|
155
|
+
override.active? # => true
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### AgentConfigPath
|
|
159
|
+
|
|
160
|
+
The result of resolving an agent's config:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
config.effective # => Location (highest precedence)
|
|
164
|
+
config.global # => Location or nil
|
|
165
|
+
config.project # => Location or nil
|
|
166
|
+
config.layers # => Array of Location
|
|
167
|
+
config.env_overrides # => Array of EnvOverride
|
|
168
|
+
config.warnings # => Array of String
|
|
169
|
+
config.custom_config? # => true if env override active
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
The gem is available as open source under the terms of the Apache 2.0 License.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentSettings
|
|
4
|
+
module Adapters
|
|
5
|
+
# Adapter for Claude Code agent configuration.
|
|
6
|
+
#
|
|
7
|
+
# Claude Code stores configuration in JSON files at various locations:
|
|
8
|
+
#
|
|
9
|
+
# - Global: ~/.claude/settings.json (user-level config)
|
|
10
|
+
# - Project: .claude/settings.json (project-level config)
|
|
11
|
+
# - Local: .claude/settings.local.json (local overrides, not committed)
|
|
12
|
+
# - Managed: /Library/Application Support/ClaudeCode/managed-settings.json (macOS system-level)
|
|
13
|
+
#
|
|
14
|
+
# Precedence (highest to lowest): managed > local > project > global
|
|
15
|
+
#
|
|
16
|
+
# Environment variable CLAUDE_CONFIG_DIR can override the global config location.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# adapter = Claude.new
|
|
20
|
+
# layers = adapter.layers(dir: "/work/my_app", env: ENV, trusted: true)
|
|
21
|
+
# env_overrides = adapter.env_overrides(dir: "/work/my_app", env: ENV, trusted: true)
|
|
22
|
+
class Claude
|
|
23
|
+
# Get all config layers for Claude Code.
|
|
24
|
+
#
|
|
25
|
+
# Returns an array of Location objects representing all possible
|
|
26
|
+
# config file locations, ordered by precedence (highest first).
|
|
27
|
+
# Only one layer will be marked as active (the first existing one).
|
|
28
|
+
#
|
|
29
|
+
# @param dir [String] project directory path
|
|
30
|
+
# @param env [Hash] environment variables hash
|
|
31
|
+
# @param trusted [Boolean] whether the project is trusted (unused for Claude)
|
|
32
|
+
# @return [Array<Location>] config layers in precedence order
|
|
33
|
+
def layers(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
|
|
34
|
+
build_layers(dir, env)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get environment variable overrides for Claude Code.
|
|
38
|
+
#
|
|
39
|
+
# Checks for CLAUDE_CONFIG_DIR environment variable and returns
|
|
40
|
+
# an EnvOverride if it's set. The override is active if the
|
|
41
|
+
# resulting settings.json file exists.
|
|
42
|
+
#
|
|
43
|
+
# @param dir [String] project directory path (unused)
|
|
44
|
+
# @param env [Hash] environment variables hash
|
|
45
|
+
# @param trusted [Boolean] whether the project is trusted (unused)
|
|
46
|
+
# @return [Array<EnvOverride>] detected environment overrides
|
|
47
|
+
def env_overrides(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
|
|
48
|
+
build_env_overrides(env)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get warnings for Claude Code configuration.
|
|
52
|
+
#
|
|
53
|
+
# Claude Code doesn't produce any warnings currently.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<String>] empty array
|
|
56
|
+
def warnings(*) = []
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# Build the list of config layers.
|
|
61
|
+
#
|
|
62
|
+
# Creates Location objects for each possible config location,
|
|
63
|
+
# ordered by precedence. The managed config (system-level) has
|
|
64
|
+
# highest precedence, followed by local project overrides,
|
|
65
|
+
# then project config, then user global config.
|
|
66
|
+
#
|
|
67
|
+
# @param dir [String] project directory path
|
|
68
|
+
# @param env [Hash] environment variables hash
|
|
69
|
+
# @return [Array<Location>] config layers with active flag set
|
|
70
|
+
def build_layers(dir, env) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength -- builds all layer types in one coherent method
|
|
71
|
+
home = Dir.home
|
|
72
|
+
config_dir = env["CLAUDE_CONFIG_DIR"]
|
|
73
|
+
|
|
74
|
+
managed_path = managed_config_path
|
|
75
|
+
global_path = config_dir ? File.join(config_dir, "settings.json") : File.join(home, ".claude", "settings.json")
|
|
76
|
+
project_path = File.join(dir, ".claude", "settings.json")
|
|
77
|
+
local_path = File.join(dir, ".claude", "settings.local.json")
|
|
78
|
+
|
|
79
|
+
layers = []
|
|
80
|
+
|
|
81
|
+
layers << build_layer(:managed, "system", managed_path) if managed_path
|
|
82
|
+
layers << build_layer(:global, "user", global_path)
|
|
83
|
+
layers << build_layer(:project, "local", local_path)
|
|
84
|
+
layers << build_layer(:project, "file", project_path)
|
|
85
|
+
|
|
86
|
+
mark_active(layers)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build environment variable overrides.
|
|
90
|
+
#
|
|
91
|
+
# Checks if CLAUDE_CONFIG_DIR is set and creates an EnvOverride
|
|
92
|
+
# for it. The override's path is the settings.json file within
|
|
93
|
+
# the specified directory.
|
|
94
|
+
#
|
|
95
|
+
# @param env [Hash] environment variables hash
|
|
96
|
+
# @return [Array<EnvOverride>] detected overrides
|
|
97
|
+
def build_env_overrides(env) # rubocop:disable Metrics/MethodLength -- constructs EnvOverride with all required attributes
|
|
98
|
+
overrides = []
|
|
99
|
+
value = env["CLAUDE_CONFIG_DIR"]
|
|
100
|
+
|
|
101
|
+
if value
|
|
102
|
+
path = File.join(value, "settings.json")
|
|
103
|
+
overrides << EnvOverride.new(
|
|
104
|
+
agent: :claude,
|
|
105
|
+
name: "CLAUDE_CONFIG_DIR",
|
|
106
|
+
value: value,
|
|
107
|
+
path: path,
|
|
108
|
+
exists: File.exist?(path),
|
|
109
|
+
active: File.exist?(path),
|
|
110
|
+
note: "Config directory override"
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
overrides
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get the managed (system-level) config path.
|
|
118
|
+
#
|
|
119
|
+
# Managed config is typically set by system administrators and
|
|
120
|
+
# has the highest precedence. Location varies by OS:
|
|
121
|
+
# - macOS: /Library/Application Support/ClaudeCode/managed-settings.json
|
|
122
|
+
# - Linux: /etc/claude-code/managed-settings.json
|
|
123
|
+
#
|
|
124
|
+
# @return [String, nil] the managed config path if it exists, nil otherwise
|
|
125
|
+
def managed_config_path
|
|
126
|
+
case RbConfig::CONFIG["host_os"]
|
|
127
|
+
when /darwin/
|
|
128
|
+
path = "/Library/Application Support/ClaudeCode/managed-settings.json"
|
|
129
|
+
File.exist?(path) ? path : nil
|
|
130
|
+
when /linux/
|
|
131
|
+
path = "/etc/claude-code/managed-settings.json"
|
|
132
|
+
File.exist?(path) ? path : nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Build a Location object for a config layer.
|
|
137
|
+
#
|
|
138
|
+
# @param scope [Symbol] the scope (:global, :project, :managed)
|
|
139
|
+
# @param source [String] where this config comes from
|
|
140
|
+
# @param path [String] the file path
|
|
141
|
+
# @return [Location] a Location object (not yet marked active)
|
|
142
|
+
def build_layer(scope, source, path)
|
|
143
|
+
Location.new(
|
|
144
|
+
agent: :claude,
|
|
145
|
+
scope: scope,
|
|
146
|
+
source: source,
|
|
147
|
+
path: path,
|
|
148
|
+
exists: File.exist?(path),
|
|
149
|
+
active: false,
|
|
150
|
+
note: nil
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Mark the first existing layer as active.
|
|
155
|
+
#
|
|
156
|
+
# Iterates through layers and marks the first one that exists
|
|
157
|
+
# as active. This implements the precedence rule where the
|
|
158
|
+
# first existing config file wins.
|
|
159
|
+
#
|
|
160
|
+
# @param layers [Array<Location>] config layers
|
|
161
|
+
# @return [Array<Location>] layers with active flag set
|
|
162
|
+
def mark_active(layers)
|
|
163
|
+
found = layers.find(&:exists?)
|
|
164
|
+
layers.map do |layer|
|
|
165
|
+
layer.with(active: layer == found)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|