agentic 0.1.0 → 0.2.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.agentic.yml +2 -0
  3. data/.architecture/decisions/ArchitecturalFeatureBuilder.md +136 -0
  4. data/.architecture/decisions/ArchitectureConsiderations.md +200 -0
  5. data/.architecture/decisions/adr_001_observer_pattern_implementation.md +196 -0
  6. data/.architecture/decisions/adr_002_plan_orchestrator.md +320 -0
  7. data/.architecture/decisions/adr_003_plan_orchestrator_interface.md +179 -0
  8. data/.architecture/decisions/adrs/ADR-001-dependency-management.md +147 -0
  9. data/.architecture/decisions/adrs/ADR-002-system-boundaries.md +162 -0
  10. data/.architecture/decisions/adrs/ADR-003-content-safety.md +158 -0
  11. data/.architecture/decisions/adrs/ADR-004-agent-permissions.md +161 -0
  12. data/.architecture/decisions/adrs/ADR-005-adaptation-engine.md +127 -0
  13. data/.architecture/decisions/adrs/ADR-006-extension-system.md +273 -0
  14. data/.architecture/decisions/adrs/ADR-007-learning-system.md +156 -0
  15. data/.architecture/decisions/adrs/ADR-008-prompt-generation.md +325 -0
  16. data/.architecture/decisions/adrs/ADR-009-task-failure-handling.md +353 -0
  17. data/.architecture/decisions/adrs/ADR-010-task-input-handling.md +251 -0
  18. data/.architecture/decisions/adrs/ADR-011-task-observable-pattern.md +391 -0
  19. data/.architecture/decisions/adrs/ADR-012-task-output-handling.md +205 -0
  20. data/.architecture/decisions/adrs/ADR-013-architecture-alignment.md +211 -0
  21. data/.architecture/decisions/adrs/ADR-014-agent-capability-registry.md +80 -0
  22. data/.architecture/decisions/adrs/ADR-015-persistent-agent-store.md +100 -0
  23. data/.architecture/decisions/adrs/ADR-016-agent-assembly-engine.md +117 -0
  24. data/.architecture/decisions/adrs/ADR-017-streaming-observability.md +171 -0
  25. data/.architecture/decisions/capability_tools_distinction.md +150 -0
  26. data/.architecture/decisions/cli_command_structure.md +61 -0
  27. data/.architecture/implementation/agent_self_assembly_implementation.md +267 -0
  28. data/.architecture/implementation/agent_self_assembly_summary.md +138 -0
  29. data/.architecture/members.yml +187 -0
  30. data/.architecture/planning/self_implementation_exercise.md +295 -0
  31. data/.architecture/planning/session_compaction_rule.md +43 -0
  32. data/.architecture/planning/streaming_observability_feature.md +223 -0
  33. data/.architecture/principles.md +151 -0
  34. data/.architecture/recalibration/0-2-0.md +92 -0
  35. data/.architecture/recalibration/agent_self_assembly.md +238 -0
  36. data/.architecture/recalibration/cli_command_structure.md +91 -0
  37. data/.architecture/recalibration/implementation_roadmap_0-2-0.md +301 -0
  38. data/.architecture/recalibration/progress_tracking_0-2-0.md +114 -0
  39. data/.architecture/recalibration_process.md +127 -0
  40. data/.architecture/reviews/0-2-0.md +181 -0
  41. data/.architecture/reviews/cli_command_duplication.md +98 -0
  42. data/.architecture/templates/adr.md +105 -0
  43. data/.architecture/templates/implementation_roadmap.md +125 -0
  44. data/.architecture/templates/progress_tracking.md +89 -0
  45. data/.architecture/templates/recalibration_plan.md +70 -0
  46. data/.architecture/templates/version_comparison.md +124 -0
  47. data/.claude/settings.local.json +13 -0
  48. data/.claude-sessions/001-task-class-architecture-implementation.md +129 -0
  49. data/.claude-sessions/002-plan-orchestrator-interface-review.md +105 -0
  50. data/.claude-sessions/architecture-governance-implementation.md +37 -0
  51. data/.claude-sessions/architecture-review-session.md +27 -0
  52. data/ArchitecturalFeatureBuilder.md +136 -0
  53. data/ArchitectureConsiderations.md +229 -0
  54. data/CHANGELOG.md +57 -2
  55. data/CLAUDE.md +111 -0
  56. data/CONTRIBUTING.md +286 -0
  57. data/MAINTAINING.md +301 -0
  58. data/README.md +582 -28
  59. data/docs/agent_capabilities_api.md +259 -0
  60. data/docs/artifact_extension_points.md +757 -0
  61. data/docs/artifact_generation_architecture.md +323 -0
  62. data/docs/artifact_implementation_plan.md +596 -0
  63. data/docs/artifact_integration_points.md +345 -0
  64. data/docs/artifact_verification_strategies.md +581 -0
  65. data/docs/streaming_observability_architecture.md +510 -0
  66. data/exe/agentic +6 -1
  67. data/lefthook.yml +5 -0
  68. data/lib/agentic/adaptation_engine.rb +124 -0
  69. data/lib/agentic/agent.rb +181 -4
  70. data/lib/agentic/agent_assembly_engine.rb +442 -0
  71. data/lib/agentic/agent_capability_registry.rb +260 -0
  72. data/lib/agentic/agent_config.rb +63 -0
  73. data/lib/agentic/agent_specification.rb +46 -0
  74. data/lib/agentic/capabilities/examples.rb +530 -0
  75. data/lib/agentic/capabilities.rb +14 -0
  76. data/lib/agentic/capability_provider.rb +146 -0
  77. data/lib/agentic/capability_specification.rb +118 -0
  78. data/lib/agentic/cli/agent.rb +31 -0
  79. data/lib/agentic/cli/capabilities.rb +191 -0
  80. data/lib/agentic/cli/config.rb +134 -0
  81. data/lib/agentic/cli/execution_observer.rb +796 -0
  82. data/lib/agentic/cli.rb +1068 -0
  83. data/lib/agentic/default_agent_provider.rb +35 -0
  84. data/lib/agentic/errors/llm_error.rb +184 -0
  85. data/lib/agentic/execution_plan.rb +53 -0
  86. data/lib/agentic/execution_result.rb +91 -0
  87. data/lib/agentic/expected_answer_format.rb +46 -0
  88. data/lib/agentic/extension/domain_adapter.rb +109 -0
  89. data/lib/agentic/extension/plugin_manager.rb +163 -0
  90. data/lib/agentic/extension/protocol_handler.rb +116 -0
  91. data/lib/agentic/extension.rb +45 -0
  92. data/lib/agentic/factory_methods.rb +9 -1
  93. data/lib/agentic/generation_stats.rb +61 -0
  94. data/lib/agentic/learning/README.md +84 -0
  95. data/lib/agentic/learning/capability_optimizer.rb +613 -0
  96. data/lib/agentic/learning/execution_history_store.rb +251 -0
  97. data/lib/agentic/learning/pattern_recognizer.rb +500 -0
  98. data/lib/agentic/learning/strategy_optimizer.rb +706 -0
  99. data/lib/agentic/learning.rb +131 -0
  100. data/lib/agentic/llm_assisted_composition_strategy.rb +188 -0
  101. data/lib/agentic/llm_client.rb +215 -15
  102. data/lib/agentic/llm_config.rb +65 -1
  103. data/lib/agentic/llm_response.rb +163 -0
  104. data/lib/agentic/logger.rb +1 -1
  105. data/lib/agentic/observable.rb +51 -0
  106. data/lib/agentic/persistent_agent_store.rb +385 -0
  107. data/lib/agentic/plan_execution_result.rb +129 -0
  108. data/lib/agentic/plan_orchestrator.rb +464 -0
  109. data/lib/agentic/plan_orchestrator_config.rb +57 -0
  110. data/lib/agentic/retry_config.rb +63 -0
  111. data/lib/agentic/retry_handler.rb +125 -0
  112. data/lib/agentic/structured_outputs.rb +1 -1
  113. data/lib/agentic/task.rb +193 -0
  114. data/lib/agentic/task_definition.rb +39 -0
  115. data/lib/agentic/task_execution_result.rb +92 -0
  116. data/lib/agentic/task_failure.rb +66 -0
  117. data/lib/agentic/task_output_schemas.rb +112 -0
  118. data/lib/agentic/task_planner.rb +54 -19
  119. data/lib/agentic/task_result.rb +48 -0
  120. data/lib/agentic/ui.rb +244 -0
  121. data/lib/agentic/verification/critic_framework.rb +116 -0
  122. data/lib/agentic/verification/llm_verification_strategy.rb +60 -0
  123. data/lib/agentic/verification/schema_verification_strategy.rb +47 -0
  124. data/lib/agentic/verification/verification_hub.rb +62 -0
  125. data/lib/agentic/verification/verification_result.rb +50 -0
  126. data/lib/agentic/verification/verification_strategy.rb +26 -0
  127. data/lib/agentic/version.rb +1 -1
  128. data/lib/agentic.rb +74 -2
  129. data/plugins/README.md +41 -0
  130. metadata +245 -6
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ # Defines the specification for an agent capability
5
+ # @attr_reader [String] name The name of the capability
6
+ # @attr_reader [String] description Description of the capability
7
+ # @attr_reader [String] version The version of the capability
8
+ # @attr_reader [Hash] inputs The required inputs for the capability
9
+ # @attr_reader [Hash] outputs The expected outputs from the capability
10
+ # @attr_reader [Array<Hash>] dependencies The dependencies of the capability
11
+ class CapabilitySpecification
12
+ attr_reader :name, :description, :version, :inputs, :outputs, :dependencies
13
+
14
+ # Initialize a new capability specification
15
+ # @param name [String] The name of the capability
16
+ # @param description [String] Description of the capability
17
+ # @param version [String] The version of the capability
18
+ # @param inputs [Hash] The required inputs for the capability
19
+ # @param outputs [Hash] The expected outputs from the capability
20
+ # @param dependencies [Array<Hash>] The dependencies of the capability
21
+ def initialize(name:, description:, version:, inputs: {}, outputs: {}, dependencies: [])
22
+ @name = name
23
+ @description = description
24
+ @version = version
25
+ @inputs = inputs
26
+ @outputs = outputs
27
+ @dependencies = dependencies
28
+ end
29
+
30
+ # Check if this capability is compatible with another capability
31
+ # @param other [CapabilitySpecification] The other capability
32
+ # @return [Boolean] True if compatible
33
+ def compatible_with?(other)
34
+ return false unless other.is_a?(CapabilitySpecification)
35
+ return false unless name == other.name
36
+
37
+ # Compare versions using semantic versioning rules
38
+ # For now, just check for exact match or higher minor version
39
+ return true if version == other.version
40
+
41
+ begin
42
+ my_parts = version.split(".").map(&:to_i)
43
+ other_parts = other.version.split(".").map(&:to_i)
44
+
45
+ # Major version must match
46
+ return false unless my_parts[0] == other_parts[0]
47
+
48
+ # Our minor version should be >= other's minor version
49
+ my_parts[1] >= other_parts[1]
50
+ rescue
51
+ # If version parsing fails, require exact match
52
+ false
53
+ end
54
+ end
55
+
56
+ # Convert to a hash representation
57
+ # @return [Hash] The hash representation
58
+ def to_h
59
+ {
60
+ name: @name,
61
+ description: @description,
62
+ version: @version,
63
+ inputs: @inputs,
64
+ outputs: @outputs,
65
+ dependencies: @dependencies
66
+ }
67
+ end
68
+
69
+ # Create from a hash representation
70
+ # @param hash [Hash] The hash representation
71
+ # @return [CapabilitySpecification] The capability specification
72
+ def self.from_h(hash)
73
+ new(
74
+ name: hash[:name] || hash["name"],
75
+ description: hash[:description] || hash["description"],
76
+ version: hash[:version] || hash["version"],
77
+ inputs: hash[:inputs] || hash["inputs"] || {},
78
+ outputs: hash[:outputs] || hash["outputs"] || {},
79
+ dependencies: hash[:dependencies] || hash["dependencies"] || []
80
+ )
81
+ end
82
+
83
+ # Get the capability requirements as a human-readable string
84
+ # @return [String] The capability requirements
85
+ def requirements_description
86
+ result = "Capability: #{name} (v#{version})\n"
87
+ result += "Description: #{description}\n"
88
+
89
+ unless inputs.empty?
90
+ result += "\nInputs:\n"
91
+ inputs.each do |name, spec|
92
+ result += " #{name}: #{spec[:type] || "any"}"
93
+ result += " (required)" if spec[:required]
94
+ result += " - #{spec[:description]}" if spec[:description]
95
+ result += "\n"
96
+ end
97
+ end
98
+
99
+ unless outputs.empty?
100
+ result += "\nOutputs:\n"
101
+ outputs.each do |name, spec|
102
+ result += " #{name}: #{spec[:type] || "any"}"
103
+ result += " - #{spec[:description]}" if spec[:description]
104
+ result += "\n"
105
+ end
106
+ end
107
+
108
+ unless dependencies.empty?
109
+ result += "\nDependencies:\n"
110
+ dependencies.each do |dep|
111
+ result += " #{dep[:name]} (v#{dep[:version] || "any"})\n"
112
+ end
113
+ end
114
+
115
+ result
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ class CLI < Thor
5
+ # CLI commands for managing agents
6
+ class Agent < Thor
7
+ desc "list", "List available agents"
8
+ def list
9
+ puts "Available agents:"
10
+ # In a future implementation, this would list agents from a registry
11
+ puts " - No custom agents registered yet"
12
+ end
13
+
14
+ desc "create NAME", "Create a new agent"
15
+ option :role, type: :string, required: true, desc: "Role of the agent"
16
+ option :instructions, type: :string, required: true, desc: "Instructions for the agent"
17
+ def create(name)
18
+ puts "Creating agent: #{name}"
19
+ # In a future implementation, this would create and register an agent
20
+ puts "Agent created successfully."
21
+ end
22
+
23
+ desc "delete NAME", "Delete an agent"
24
+ def delete(name)
25
+ puts "Deleting agent: #{name}"
26
+ # In a future implementation, this would delete an agent from a registry
27
+ puts "Agent deleted successfully."
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agentic
4
+ class CLI < Thor
5
+ # Command-line interface for managing capabilities
6
+ class Capabilities < Thor
7
+ desc "list", "List available capabilities"
8
+ option :detailed, type: :boolean, aliases: "-d",
9
+ desc: "Show detailed information"
10
+ def list
11
+ # Initialize agent assembly system
12
+ Agentic.initialize_agent_assembly
13
+ registry = Agentic.agent_capability_registry
14
+
15
+ capabilities = registry.list(include_providers: options[:detailed])
16
+
17
+ if capabilities.empty?
18
+ puts UI.box(
19
+ "Available Capabilities",
20
+ "No capabilities registered yet.\n\n" \
21
+ "You can register capabilities programmatically via Agentic.register_capability.",
22
+ padding: [1, 2, 1, 2],
23
+ style: {border: {fg: :blue}}
24
+ )
25
+ return
26
+ end
27
+
28
+ output = ""
29
+ capabilities.each do |name, info|
30
+ output += "#{UI.colorize(name, :blue)}:\n"
31
+ output += " Available versions: #{info[:versions].join(", ")}\n"
32
+ output += " Latest version: #{UI.colorize(info[:latest], :green)}\n"
33
+
34
+ if options[:detailed] && info[:providers]
35
+ # Get a capability to show more details
36
+ capability = registry.get(name, info[:latest])
37
+ if capability
38
+ output += " Description: #{capability.description}\n"
39
+
40
+ unless capability.inputs.empty?
41
+ output += " Inputs:\n"
42
+ capability.inputs.each do |input_name, input_spec|
43
+ required = input_spec[:required] ? " (required)" : ""
44
+ output += " - #{input_name}#{required}: #{input_spec[:type] || "any"}\n"
45
+ output += " #{input_spec[:description]}\n" if input_spec[:description]
46
+ end
47
+ end
48
+
49
+ unless capability.outputs.empty?
50
+ output += " Outputs:\n"
51
+ capability.outputs.each do |output_name, output_spec|
52
+ output += " - #{output_name}: #{output_spec[:type] || "any"}\n"
53
+ output += " #{output_spec[:description]}\n" if output_spec[:description]
54
+ end
55
+ end
56
+
57
+ unless capability.dependencies.empty?
58
+ output += " Dependencies:\n"
59
+ capability.dependencies.each do |dep|
60
+ output += " - #{dep[:name]} (#{dep[:version] || "any version"})\n"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ output += "\n"
67
+ end
68
+
69
+ puts UI.box(
70
+ "Available Capabilities",
71
+ output,
72
+ padding: [1, 2, 1, 2],
73
+ style: {border: {fg: :blue}}
74
+ )
75
+ end
76
+
77
+ desc "show NAME", "Show details of a specific capability"
78
+ option :version, type: :string, aliases: "-v",
79
+ desc: "Capability version (defaults to latest)"
80
+ def show(name)
81
+ # Initialize agent assembly system
82
+ Agentic.initialize_agent_assembly
83
+ registry = Agentic.agent_capability_registry
84
+
85
+ capability = registry.get(name, options[:version])
86
+
87
+ unless capability
88
+ available = registry.list.keys.join(", ")
89
+ available_text = available.empty? ? "No capabilities registered yet." : "Available: #{available}"
90
+
91
+ puts UI.box(
92
+ "Error",
93
+ "Capability '#{UI.colorize(name, :yellow)}' not found.\n\n#{available_text}",
94
+ padding: [1, 2, 1, 2],
95
+ style: {border: {fg: :red}}
96
+ )
97
+ exit 1
98
+ end
99
+
100
+ output = ""
101
+ output += "Name: #{UI.colorize(capability.name, :blue)}\n"
102
+ output += "Version: #{UI.colorize(capability.version, :green)}\n"
103
+ output += "Description: #{capability.description}\n\n"
104
+
105
+ unless capability.inputs.empty?
106
+ output += "Inputs:\n"
107
+ capability.inputs.each do |input_name, input_spec|
108
+ required = input_spec[:required] ? " (required)" : ""
109
+ output += " - #{UI.colorize(input_name, :yellow)}#{required}: #{input_spec[:type] || "any"}\n"
110
+ output += " #{input_spec[:description]}\n" if input_spec[:description]
111
+ end
112
+ output += "\n"
113
+ end
114
+
115
+ unless capability.outputs.empty?
116
+ output += "Outputs:\n"
117
+ capability.outputs.each do |output_name, output_spec|
118
+ output += " - #{UI.colorize(output_name, :yellow)}: #{output_spec[:type] || "any"}\n"
119
+ output += " #{output_spec[:description]}\n" if output_spec[:description]
120
+ end
121
+ output += "\n"
122
+ end
123
+
124
+ unless capability.dependencies.empty?
125
+ output += "Dependencies:\n"
126
+ capability.dependencies.each do |dep|
127
+ output += " - #{UI.colorize(dep[:name], :magenta)} (#{dep[:version] || "any version"})\n"
128
+ end
129
+ end
130
+
131
+ puts UI.box(
132
+ "Capability Details",
133
+ output,
134
+ padding: [1, 2, 1, 2],
135
+ style: {border: {fg: :blue}}
136
+ )
137
+ end
138
+
139
+ desc "search QUERY", "Search for capabilities"
140
+ def search(query)
141
+ # Initialize agent assembly system
142
+ Agentic.initialize_agent_assembly
143
+ registry = Agentic.agent_capability_registry
144
+
145
+ # Search by name and description
146
+ capabilities = registry.list
147
+ results = {}
148
+
149
+ capabilities.each do |name, info|
150
+ # Check if query matches capability name
151
+ if name.downcase.include?(query.downcase)
152
+ results[name] = info
153
+ next
154
+ end
155
+
156
+ # Check if query matches capability description
157
+ capability = registry.get(name, info[:latest])
158
+ if capability && capability.description.downcase.include?(query.downcase)
159
+ results[name] = info
160
+ end
161
+ end
162
+
163
+ if results.empty?
164
+ puts UI.box(
165
+ "Search Results",
166
+ "No capabilities found matching '#{UI.colorize(query, :yellow)}'.",
167
+ padding: [1, 2, 1, 2],
168
+ style: {border: {fg: :blue}}
169
+ )
170
+ return
171
+ end
172
+
173
+ output = ""
174
+ results.each do |name, info|
175
+ capability = registry.get(name, info[:latest])
176
+
177
+ output += "#{UI.colorize(name, :blue)}:\n"
178
+ output += " Latest version: #{UI.colorize(info[:latest], :green)}\n"
179
+ output += " Description: #{capability.description}\n\n"
180
+ end
181
+
182
+ puts UI.box(
183
+ "Search Results",
184
+ output,
185
+ padding: [1, 2, 1, 2],
186
+ style: {border: {fg: :blue}}
187
+ )
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module Agentic
7
+ class CLI < Thor
8
+ # CLI commands for managing configuration
9
+ class Config < Thor
10
+ CONFIG_FILE_NAME = ".agentic.yml"
11
+ USER_CONFIG_PATH = File.join(Dir.home, CONFIG_FILE_NAME)
12
+ PROJECT_CONFIG_PATH = File.join(Dir.pwd, CONFIG_FILE_NAME)
13
+
14
+ desc "list", "List configuration settings"
15
+ def list
16
+ user_config = load_config(USER_CONFIG_PATH)
17
+ project_config = load_config(PROJECT_CONFIG_PATH)
18
+
19
+ puts "User configuration (#{USER_CONFIG_PATH}):"
20
+ print_config(user_config)
21
+
22
+ puts "\nProject configuration (#{PROJECT_CONFIG_PATH}):"
23
+ print_config(project_config)
24
+
25
+ puts "\nActive configuration:"
26
+ print_config(active_config)
27
+
28
+ puts "\nEnvironment variables:"
29
+ puts " OPENAI_ACCESS_TOKEN: #{ENV["OPENAI_ACCESS_TOKEN"] ? "[SET]" : "[NOT SET]"}"
30
+ end
31
+
32
+ desc "get KEY", "Get a configuration setting"
33
+ def get(key)
34
+ config = active_config
35
+ value = config[key]
36
+
37
+ if value
38
+ puts value
39
+ else
40
+ puts "Key '#{key}' not found in configuration"
41
+ exit 1
42
+ end
43
+ end
44
+
45
+ desc "set KEY=VALUE", "Set a configuration setting"
46
+ option :global, type: :boolean, aliases: "-g",
47
+ desc: "Set in global user config instead of project config"
48
+ def set(key_value)
49
+ key, value = key_value.split("=", 2)
50
+
51
+ unless value
52
+ puts "Error: Invalid format. Use KEY=VALUE"
53
+ exit 1
54
+ end
55
+
56
+ path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH
57
+ config = load_config(path) || {}
58
+
59
+ # Convert string values to appropriate types
60
+ value = case value.downcase
61
+ when "true" then true
62
+ when "false" then false
63
+ when /^\d+$/ then value.to_i
64
+ when /^\d+\.\d+$/ then value.to_f
65
+ else value
66
+ end
67
+
68
+ config[key] = value
69
+ save_config(path, config)
70
+
71
+ puts "Configuration updated successfully."
72
+ end
73
+
74
+ desc "init", "Initialize configuration"
75
+ option :global, type: :boolean, aliases: "-g",
76
+ desc: "Initialize global user config instead of project config"
77
+ def init
78
+ path = options[:global] ? USER_CONFIG_PATH : PROJECT_CONFIG_PATH
79
+
80
+ if File.exist?(path)
81
+ puts "Configuration already exists at #{path}"
82
+ return
83
+ end
84
+
85
+ config = {
86
+ "model" => "gpt-4o-mini"
87
+ # Add other default configuration options here
88
+ }
89
+
90
+ save_config(path, config)
91
+ puts "Configuration initialized at #{path}"
92
+ end
93
+
94
+ private
95
+
96
+ def load_config(path)
97
+ return unless File.exist?(path)
98
+
99
+ begin
100
+ YAML.load_file(path)
101
+ rescue => e
102
+ puts "Error loading configuration from #{path}: #{e.message}"
103
+ nil
104
+ end
105
+ end
106
+
107
+ def save_config(path, config)
108
+ # Create directory if it doesn't exist
109
+ dir = File.dirname(path)
110
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
111
+
112
+ File.write(path, YAML.dump(config))
113
+ end
114
+
115
+ def active_config
116
+ # Combine user and project configs, with project taking precedence
117
+ user_config = load_config(USER_CONFIG_PATH) || {}
118
+ project_config = load_config(PROJECT_CONFIG_PATH) || {}
119
+
120
+ user_config.merge(project_config)
121
+ end
122
+
123
+ def print_config(config)
124
+ if config.nil? || config.empty?
125
+ puts " [empty]"
126
+ else
127
+ config.each do |key, value|
128
+ puts " #{key}: #{value}"
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end