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.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ module Adapters
5
+ # Adapter for Codex agent configuration.
6
+ #
7
+ # Codex stores configuration in TOML files:
8
+ #
9
+ # - Global: ~/.codex/config.toml (user-level config)
10
+ # - Project: .codex/config.toml (project-level config, trusted only)
11
+ # - System: /etc/codex/config.toml (system-level config)
12
+ #
13
+ # Codex has a unique "trust" model - project config is only used
14
+ # for trusted projects. Untrusted projects will have a warning
15
+ # and no project layer.
16
+ #
17
+ # Environment variable CODEX_HOME can override the home directory
18
+ # where the global config is located.
19
+ #
20
+ # @example Trusted project
21
+ # adapter = Codex.new
22
+ # layers = adapter.layers(dir: "/work/my_app", env: ENV, trusted: true)
23
+ # layers.count #=> includes project layer
24
+ #
25
+ # @example Untrusted project
26
+ # layers = adapter.layers(dir: "/untrusted", env: ENV, trusted: false)
27
+ # adapter.warnings(...) #=> ["Project config ignored for untrusted project"]
28
+ class Codex
29
+ # Get all config layers for Codex.
30
+ #
31
+ # Returns an array of Location objects for global, system (if exists),
32
+ # and project (if trusted) configs. Project config is excluded for
33
+ # untrusted projects.
34
+ #
35
+ # @param dir [String] project directory path
36
+ # @param env [Hash] environment variables hash (CODEX_HOME is used)
37
+ # @param trusted [Boolean] whether the project is trusted
38
+ # @return [Array<Location>] config layers in precedence order
39
+ def layers(dir:, env:, trusted:)
40
+ build_layers(dir, env, trusted)
41
+ end
42
+
43
+ # Get environment variable overrides for Codex.
44
+ #
45
+ # Checks for CODEX_HOME environment variable. When set, it changes
46
+ # the base directory for the global config (from ~ to the specified path).
47
+ #
48
+ # @param dir [String] project directory path (unused)
49
+ # @param env [Hash] environment variables hash
50
+ # @param trusted [Boolean] whether the project is trusted (unused)
51
+ # @return [Array<EnvOverride>] detected environment overrides
52
+ def env_overrides(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
53
+ build_env_overrides(env)
54
+ end
55
+
56
+ # Get warnings for Codex configuration.
57
+ #
58
+ # Returns a warning if the project is untrusted but has a config file.
59
+ # This helps users understand why their project config is being ignored.
60
+ #
61
+ # @param dir [String] project directory path
62
+ # @param env [Hash] environment variables hash (unused)
63
+ # @param trusted [Boolean] whether the project is trusted
64
+ # @return [Array<String>] warning messages
65
+ def warnings(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
66
+ warnings = []
67
+ if !trusted && File.exist?(File.join(dir, ".codex", "config.toml"))
68
+ warnings << "Project config ignored for untrusted project"
69
+ end
70
+ warnings
71
+ end
72
+
73
+ private
74
+
75
+ # Build the list of config layers.
76
+ #
77
+ # Creates Location objects for global, system (if exists), and
78
+ # project (if trusted) configs. CODEX_HOME can override the
79
+ # home directory for global config location.
80
+ #
81
+ # @param dir [String] project directory path
82
+ # @param env [Hash] environment variables hash
83
+ # @param trusted [Boolean] whether the project is trusted
84
+ # @return [Array<Location>] config layers with active flag set
85
+ def build_layers(dir, env, trusted)
86
+ home = env["CODEX_HOME"] || Dir.home
87
+ global_path = File.join(home, ".codex", "config.toml")
88
+ system_path = "/etc/codex/config.toml"
89
+ project_path = File.join(dir, ".codex", "config.toml")
90
+
91
+ layers = []
92
+ layers << build_layer(:global, "user", global_path)
93
+ layers << build_layer(:system, "system", system_path) if File.exist?(system_path)
94
+
95
+ layers << build_layer(:project, "file", project_path) if trusted
96
+
97
+ mark_active(layers)
98
+ end
99
+
100
+ # Build environment variable overrides.
101
+ #
102
+ # Checks if CODEX_HOME is set and creates an EnvOverride for it.
103
+ # The override's path is the config.toml file within the specified
104
+ # home directory's .codex subdirectory.
105
+ #
106
+ # @param env [Hash] environment variables hash
107
+ # @return [Array<EnvOverride>] detected overrides
108
+ def build_env_overrides(env) # rubocop:disable Metrics/MethodLength -- constructs EnvOverride with all required attributes
109
+ overrides = []
110
+
111
+ if (value = env["CODEX_HOME"])
112
+ path = File.join(value, ".codex", "config.toml")
113
+ overrides << EnvOverride.new(
114
+ agent: :codex,
115
+ name: "CODEX_HOME",
116
+ value: value,
117
+ path: path,
118
+ exists: File.exist?(path),
119
+ active: File.exist?(path),
120
+ note: "Home directory override"
121
+ )
122
+ end
123
+
124
+ overrides
125
+ end
126
+
127
+ # Build a Location object for a config layer.
128
+ #
129
+ # @param scope [Symbol] the scope (:global, :project, :system)
130
+ # @param source [String] where this config comes from
131
+ # @param path [String] the file path
132
+ # @return [Location] a Location object (not yet marked active)
133
+ def build_layer(scope, source, path)
134
+ Location.new(
135
+ agent: :codex,
136
+ scope: scope,
137
+ source: source,
138
+ path: path,
139
+ exists: File.exist?(path),
140
+ active: false,
141
+ note: nil
142
+ )
143
+ end
144
+
145
+ # Mark the first existing layer as active.
146
+ #
147
+ # @param layers [Array<Location>] config layers
148
+ # @return [Array<Location>] layers with active flag set
149
+ def mark_active(layers)
150
+ found = layers.find(&:exists?)
151
+ layers.map do |layer|
152
+ layer.with(active: layer == found)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ module Adapters
5
+ # Adapter for OpenCode agent configuration.
6
+ #
7
+ # OpenCode stores configuration in JSON files:
8
+ #
9
+ # - Global: ~/.config/opencode/opencode.json (user-level config)
10
+ # - Project: opencode.json (discovered by walking up from project directory)
11
+ #
12
+ # OpenCode supports multiple environment variable overrides:
13
+ # - OPENCODE_CONFIG: Direct path to config file
14
+ # - OPENCODE_CONFIG_DIR: Directory containing opencode.json
15
+ # - OPENCODE_CONFIG_CONTENT: Inline JSON config content
16
+ #
17
+ # The project config uses discovery - it walks up from the provided
18
+ # directory looking for opencode.json, similar to how git finds .git.
19
+ #
20
+ # @example
21
+ # adapter = Opencode.new
22
+ # layers = adapter.layers(dir: "/work/my_app", env: ENV, trusted: true)
23
+ # env_overrides = adapter.env_overrides(dir: "/work/my_app", env: ENV, trusted: true)
24
+ class Opencode
25
+ # Get all config layers for OpenCode.
26
+ #
27
+ # Returns an array of Location objects for global and project
28
+ # configs. Project config is discovered by walking up the directory
29
+ # tree from the provided dir.
30
+ #
31
+ # @param dir [String] project directory path (starting point for discovery)
32
+ # @param env [Hash] environment variables hash (unused for layers)
33
+ # @param trusted [Boolean] whether the project is trusted (unused)
34
+ # @return [Array<Location>] config layers in precedence order
35
+ def layers(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
36
+ build_layers(dir)
37
+ end
38
+
39
+ # Get environment variable overrides for OpenCode.
40
+ #
41
+ # Checks for three environment variables:
42
+ # - OPENCODE_CONFIG: Path to config file (active if file exists)
43
+ # - OPENCODE_CONFIG_DIR: Directory containing opencode.json
44
+ # - OPENCODE_CONFIG_CONTENT: Inline JSON (always active if set)
45
+ #
46
+ # @param dir [String] project directory path (unused)
47
+ # @param env [Hash] environment variables hash
48
+ # @param trusted [Boolean] whether the project is trusted (unused)
49
+ # @return [Array<EnvOverride>] detected environment overrides
50
+ def env_overrides(dir:, env:, trusted:) # rubocop:disable Lint/UnusedMethodArgument
51
+ build_env_overrides(env)
52
+ end
53
+
54
+ # Get warnings for OpenCode configuration.
55
+ #
56
+ # OpenCode doesn't produce any warnings currently.
57
+ #
58
+ # @return [Array<String>] empty array
59
+ def warnings(*) = []
60
+
61
+ private
62
+
63
+ # Build the list of config layers.
64
+ #
65
+ # Creates Location objects for global and project configs.
66
+ # Project config is discovered by walking up the directory tree.
67
+ #
68
+ # @param dir [String] project directory path
69
+ # @return [Array<Location>] config layers with active flag set
70
+ def build_layers(dir)
71
+ home = Dir.home
72
+ global_path = File.join(home, ".config", "opencode", "opencode.json")
73
+ project_path = discover_project_config(dir)
74
+
75
+ layers = []
76
+ layers << build_layer(:global, "user", global_path)
77
+ layers << build_layer(:project, "file", project_path) if project_path
78
+
79
+ mark_active(layers)
80
+ end
81
+
82
+ # Discover project config by walking up the directory tree.
83
+ #
84
+ # OpenCode project config is named opencode.json (no leading dot).
85
+ # This method walks up from the provided directory looking for it,
86
+ # similar to how git finds .git directories.
87
+ #
88
+ # @param dir [String] starting directory
89
+ # @return [String] path to opencode.json (existing or default)
90
+ def discover_project_config(dir)
91
+ current = File.expand_path(dir)
92
+ root = File.expand_path("/")
93
+
94
+ while current != root
95
+ path = File.join(current, "opencode.json")
96
+ return path if File.exist?(path)
97
+
98
+ current = File.dirname(current)
99
+ end
100
+
101
+ File.join(dir, "opencode.json")
102
+ end
103
+
104
+ # Build environment variable overrides.
105
+ #
106
+ # Checks all three OpenCode environment variables and creates
107
+ # EnvOverride objects for any that are set. OPENCODE_CONFIG_CONTENT
108
+ # is always active if set (it's inline content, not a file path).
109
+ #
110
+ # @param env [Hash] environment variables hash
111
+ # @return [Array<EnvOverride>] detected overrides
112
+ def build_env_overrides(env) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength -- handles three different env vars with full attribute construction
113
+ overrides = []
114
+
115
+ if (value = env["OPENCODE_CONFIG"])
116
+ overrides << EnvOverride.new(
117
+ agent: :opencode,
118
+ name: "OPENCODE_CONFIG",
119
+ value: value,
120
+ path: value,
121
+ exists: File.exist?(value),
122
+ active: File.exist?(value),
123
+ note: "Config file override"
124
+ )
125
+ end
126
+
127
+ if (value = env["OPENCODE_CONFIG_DIR"])
128
+ path = File.join(value, "opencode.json")
129
+ overrides << EnvOverride.new(
130
+ agent: :opencode,
131
+ name: "OPENCODE_CONFIG_DIR",
132
+ value: value,
133
+ path: path,
134
+ exists: File.exist?(path),
135
+ active: File.exist?(path),
136
+ note: "Config directory override"
137
+ )
138
+ end
139
+
140
+ if (value = env["OPENCODE_CONFIG_CONTENT"])
141
+ overrides << EnvOverride.new(
142
+ agent: :opencode,
143
+ name: "OPENCODE_CONFIG_CONTENT",
144
+ value: value[0, 50] + (value.length > 50 ? "..." : ""),
145
+ path: "(inline)",
146
+ exists: true,
147
+ active: true,
148
+ note: "Inline config content"
149
+ )
150
+ end
151
+
152
+ overrides
153
+ end
154
+
155
+ # Build a Location object for a config layer.
156
+ #
157
+ # @param scope [Symbol] the scope (:global, :project)
158
+ # @param source [String] where this config comes from
159
+ # @param path [String] the file path
160
+ # @return [Location] a Location object (not yet marked active)
161
+ def build_layer(scope, source, path)
162
+ Location.new(
163
+ agent: :opencode,
164
+ scope: scope,
165
+ source: source,
166
+ path: path,
167
+ exists: File.exist?(path),
168
+ active: false,
169
+ note: nil
170
+ )
171
+ end
172
+
173
+ # Mark the first existing layer as active.
174
+ #
175
+ # @param layers [Array<Location>] config layers
176
+ # @return [Array<Location>] layers with active flag set
177
+ def mark_active(layers)
178
+ found = layers.find(&:exists?)
179
+ layers.map do |layer|
180
+ layer.with(active: layer == found)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ # The complete result of resolving an agent's config location.
5
+ #
6
+ # AgentConfigPath is the primary return value from {AgentSettings.resolve}
7
+ # and contains all information about where an agent's configuration is
8
+ # located, including:
9
+ #
10
+ # - The effective (active) config location
11
+ # - Global and project-specific locations
12
+ # - All config layers organized by precedence
13
+ # - Environment variable overrides detected
14
+ # - Any warnings (e.g., untrusted project)
15
+ #
16
+ # The effective location is determined by checking in order:
17
+ # 1. Active environment variable overrides (highest precedence)
18
+ # 2. First existing layer in precedence order
19
+ # 3. First layer if none exist
20
+ #
21
+ # @example
22
+ # result = AgentSettings.resolve(:claude, dir: Dir.pwd)
23
+ # result.effective.path #=> "/Users/me/.claude/settings.json"
24
+ # result.effective.exists? #=> true
25
+ # result.global #=> Location for global config
26
+ # result.project #=> Location for project config
27
+ # result.custom_config? #=> false (no env override active)
28
+ #
29
+ # @attr agent [Symbol] the agent this config path is for
30
+ # @attr effective [Location, nil] the currently active config location
31
+ # @attr global [Location, nil] the global (user) config location
32
+ # @attr project [Location, nil] the project-level config location
33
+ # @attr layers [Array<Location>] all config locations in precedence order
34
+ # @attr env_overrides [Array<EnvOverride>] detected environment variable overrides
35
+ # @attr warnings [Array<String>] any warnings about the configuration
36
+ AgentConfigPath = Data.define(:agent, :effective, :global, :project, :layers, :env_overrides, :warnings) do
37
+ # Check if a custom config is being used via environment variable.
38
+ #
39
+ # Returns true if any environment variable override is active,
40
+ # meaning the agent is using a config location different from
41
+ # the default file-based locations.
42
+ #
43
+ # @return [Boolean] true if an environment override is active
44
+ #
45
+ # @example
46
+ # env = { "OPENCODE_CONFIG" => "/custom/config.json" }
47
+ # result = AgentSettings.resolve(:opencode, dir: Dir.pwd, env: env)
48
+ # result.custom_config? #=> true (if /custom/config.json exists)
49
+ def custom_config? = env_overrides.any?(&:active?)
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ # Represents an environment variable that overrides config location.
5
+ #
6
+ # Agents can use environment variables to override their default config
7
+ # locations. EnvOverride captures information about these variables,
8
+ # including their name, value, the resulting config path, and whether
9
+ # the override is active (i.e., the target file exists).
10
+ #
11
+ # Supported environment variables by agent:
12
+ # - Claude: CLAUDE_CONFIG_DIR
13
+ # - OpenCode: OPENCODE_CONFIG, OPENCODE_CONFIG_DIR, OPENCODE_CONFIG_CONTENT
14
+ # - Codex: CODEX_HOME
15
+ #
16
+ # @example
17
+ # override = EnvOverride.new(
18
+ # agent: :claude,
19
+ # name: 'CLAUDE_CONFIG_DIR',
20
+ # value: '/custom/config',
21
+ # path: '/custom/config/settings.json',
22
+ # exists: true,
23
+ # active: true,
24
+ # note: 'Config directory override'
25
+ # )
26
+ # override.active? #=> true
27
+ #
28
+ # @attr agent [Symbol] the agent this override applies to
29
+ # @attr name [String] the environment variable name (e.g., 'CLAUDE_CONFIG_DIR')
30
+ # @attr value [String] the value of the environment variable
31
+ # @attr path [String] the resulting config file path
32
+ # @attr exists [Boolean] whether the config file exists at this path
33
+ # @attr active [Boolean] whether this override is in effect (file exists)
34
+ # @attr note [String] description of what this override does
35
+ EnvOverride = Data.define(:agent, :name, :value, :path, :exists, :active, :note) do
36
+ # @return [Boolean] whether the config file exists at this path
37
+ def exists? = exists
38
+
39
+ # @return [Boolean] whether this override is in effect
40
+ def active? = active
41
+
42
+ # Convert this override to a Location object.
43
+ #
44
+ # When an environment override is active, it becomes the effective
45
+ # config location. This method converts the override to a Location
46
+ # with scope :env.
47
+ #
48
+ # @return [Location] a Location representing this override
49
+ def to_location
50
+ Location.new(
51
+ agent: agent,
52
+ scope: :env,
53
+ source: name,
54
+ path: path,
55
+ exists: exists,
56
+ active: active,
57
+ note: note
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ # Represents a config file location for an agent.
5
+ #
6
+ # A Location describes where a configuration file is (or would be) stored,
7
+ # whether it exists, and whether it's the currently active configuration.
8
+ # Locations are organized by scope (global vs project) and source
9
+ # (user, system, file, etc.).
10
+ #
11
+ # @example
12
+ # location = Location.new(
13
+ # agent: :claude,
14
+ # scope: :global,
15
+ # source: 'user',
16
+ # path: '/Users/me/.claude/settings.json',
17
+ # exists: true,
18
+ # active: true,
19
+ # note: nil
20
+ # )
21
+ # location.exists? #=> true
22
+ # location.active? #=> true
23
+ #
24
+ # @attr agent [Symbol] the agent this location belongs to (:claude, :opencode, :codex)
25
+ # @attr scope [Symbol] the scope of this location (:global, :project, :managed, :system, :env)
26
+ # @attr source [String] where this location comes from ('user', 'system', 'file', 'local')
27
+ # @attr path [String] the absolute file path to the config file
28
+ # @attr exists [Boolean] whether the config file exists at this path
29
+ # @attr active [Boolean] whether this is the currently effective config location
30
+ # @attr note [String, nil] optional note about this location
31
+ Location = Data.define(:agent, :scope, :source, :path, :exists, :active, :note) do
32
+ # @return [Boolean] whether the config file exists at this path
33
+ def exists? = exists
34
+
35
+ # @return [Boolean] whether this is the currently effective config location
36
+ def active? = active
37
+ end
38
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ # Internal module for discovering agent config locations.
5
+ #
6
+ # LocationDiscovery contains the core logic for finding and resolving
7
+ # where agent configuration files are located. It uses adapters (one per
8
+ # agent) to handle agent-specific rules, then applies common precedence
9
+ # logic to determine the effective configuration.
10
+ #
11
+ # This module is used internally by the {AgentSettings} public API methods.
12
+ # You typically don't need to call it directly.
13
+ #
14
+ # @api private
15
+ module LocationDiscovery
16
+ module_function
17
+
18
+ # Find the global config location for an agent.
19
+ #
20
+ # Queries the appropriate adapter and extracts the global-scoped
21
+ # location from the layers.
22
+ #
23
+ # @param agent [Symbol] the agent identifier
24
+ # @param env [Hash] environment variables
25
+ # @param dir [String] project directory
26
+ # @param trusted [Boolean] whether project is trusted
27
+ # @return [Location, nil] the global location, or nil
28
+ def global(agent, env:, dir:, trusted:)
29
+ adapter = Registry.adapter(agent)
30
+ layers = adapter.layers(dir: dir, env: env, trusted: trusted)
31
+ layers.find { |l| l.scope == :global }
32
+ end
33
+
34
+ # Find the project config location for an agent.
35
+ #
36
+ # Queries the appropriate adapter and extracts the project-scoped
37
+ # location from the layers. Note that some agents (like Codex) may
38
+ # not return a project location for untrusted projects.
39
+ #
40
+ # @param agent [Symbol] the agent identifier
41
+ # @param dir [String] project directory
42
+ # @param env [Hash] environment variables
43
+ # @param trusted [Boolean] whether project is trusted
44
+ # @return [Location, nil] the project location, or nil
45
+ def project(agent, dir:, env:, trusted:)
46
+ adapter = Registry.adapter(agent)
47
+ layers = adapter.layers(dir: dir, env: env, trusted: trusted)
48
+ layers.find { |l| l.scope == :project }
49
+ end
50
+
51
+ # Resolve complete config path information for an agent.
52
+ #
53
+ # This is the main discovery method. It:
54
+ # 1. Gets the appropriate adapter for the agent
55
+ # 2. Retrieves all config layers from the adapter
56
+ # 3. Retrieves any environment variable overrides
57
+ # 4. Retrieves any warnings
58
+ # 5. Determines the effective location based on precedence
59
+ # 6. Builds and returns an AgentConfigPath with all information
60
+ #
61
+ # @param agent [Symbol] the agent identifier
62
+ # @param dir [String] project directory
63
+ # @param env [Hash] environment variables
64
+ # @param trusted [Boolean] whether project is trusted
65
+ # @return [AgentConfigPath] complete config path information
66
+ def resolve(agent, dir:, env:, trusted:) # rubocop:disable Metrics/MethodLength -- orchestrates adapter calls and builds complete AgentConfigPath
67
+ adapter = Registry.adapter(agent)
68
+ layers = adapter.layers(dir: dir, env: env, trusted: trusted)
69
+ env_overrides = adapter.env_overrides(dir: dir, env: env, trusted: trusted)
70
+ warnings = adapter.warnings(dir: dir, env: env, trusted: trusted)
71
+
72
+ effective = find_effective(layers, env_overrides)
73
+ global = layers.find { |l| l.scope == :global }
74
+ project = layers.find { |l| l.scope == :project }
75
+
76
+ AgentConfigPath.new(
77
+ agent: agent,
78
+ effective: effective,
79
+ global: global,
80
+ project: project,
81
+ layers: layers,
82
+ env_overrides: env_overrides,
83
+ warnings: warnings
84
+ )
85
+ end
86
+
87
+ # Resolve config paths for all supported agents.
88
+ #
89
+ # Iterates through all known agents and resolves each one,
90
+ # returning a hash mapping agent symbols to AgentConfigPath objects.
91
+ #
92
+ # @param dir [String] project directory
93
+ # @param env [Hash] environment variables
94
+ # @param trusted [Boolean] whether project is trusted
95
+ # @return [Hash{Symbol => AgentConfigPath}] map of agent to config path
96
+ def all(dir:, env:, trusted:)
97
+ Registry::AGENTS.to_h do |agent|
98
+ [agent, resolve(agent, dir: dir, env: env, trusted: trusted)]
99
+ end
100
+ end
101
+
102
+ # Determine the effective config location.
103
+ #
104
+ # The effective location is determined by precedence:
105
+ # 1. If an environment override is active, convert it to a Location
106
+ # 2. Otherwise, use the first existing layer
107
+ # 3. If no layers exist, use the first layer (even though it doesn't exist)
108
+ #
109
+ # @param layers [Array<Location>] config layers in precedence order
110
+ # @param env_overrides [Array<EnvOverride>] environment variable overrides
111
+ # @return [Location, nil] the effective location
112
+ def find_effective(layers, env_overrides)
113
+ active_override = env_overrides.find(&:active?)
114
+ return active_override.to_location if active_override
115
+
116
+ layers.find(&:active?) || layers.first
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ # Registry mapping agent symbols to their adapter classes.
5
+ #
6
+ # The Registry is responsible for knowing which adapters exist and
7
+ # providing the correct adapter instance for a given agent symbol.
8
+ # It also defines the list of supported agents.
9
+ #
10
+ # @api private
11
+ module Registry
12
+ # List of supported agent symbols.
13
+ AGENTS = %i[claude opencode codex].freeze
14
+
15
+ module_function
16
+
17
+ # Get the adapter instance for an agent.
18
+ #
19
+ # Returns a new instance of the appropriate adapter class for the
20
+ # given agent symbol. Uses pattern matching to select the adapter.
21
+ #
22
+ # @param agent [Symbol] the agent identifier (:claude, :opencode, :codex)
23
+ # @return [Object] an instance of the appropriate adapter class
24
+ # @raise [UnknownAgentError] if the agent symbol is not recognized
25
+ #
26
+ # @example
27
+ # adapter = Registry.adapter(:claude) #=> AgentSettings::Adapters::Claude instance
28
+ # adapter.layers(dir: Dir.pwd, env: {}, trusted: true)
29
+ def adapter(agent)
30
+ case agent
31
+ in :claude then Adapters::Claude.new
32
+ in :opencode then Adapters::Opencode.new
33
+ in :codex then Adapters::Codex.new
34
+ else
35
+ raise UnknownAgentError, "Unknown agent: #{agent}. Supported: #{AGENTS.join(", ")}"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentSettings
4
+ # Current gem version.
5
+ VERSION = "0.1.0"
6
+ end