aidp 0.14.1 → 0.15.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.
data/lib/aidp/logger.rb CHANGED
@@ -16,7 +16,7 @@ module Aidp
16
16
  # Usage:
17
17
  # Aidp.setup_logger(project_dir, config)
18
18
  # Aidp.logger.info("component", "message", key: "value")
19
- class AidpLogger
19
+ class Logger
20
20
  LEVELS = {
21
21
  debug: ::Logger::DEBUG,
22
22
  info: ::Logger::INFO,
@@ -26,7 +26,6 @@ module Aidp
26
26
 
27
27
  LOG_DIR = ".aidp/logs"
28
28
  INFO_LOG = "#{LOG_DIR}/aidp.log"
29
- DEBUG_LOG = "#{LOG_DIR}/aidp_debug.log"
30
29
 
31
30
  DEFAULT_MAX_SIZE = 10 * 1024 * 1024 # 10MB
32
31
  DEFAULT_MAX_FILES = 5
@@ -42,8 +41,7 @@ module Aidp
42
41
  @max_files = config[:max_backups] || DEFAULT_MAX_FILES
43
42
 
44
43
  ensure_log_directory
45
- migrate_old_logs if should_migrate?
46
- setup_loggers
44
+ setup_logger
47
45
  end
48
46
 
49
47
  # Log info level message
@@ -74,23 +72,12 @@ module Aidp
74
72
  safe_message = redact(message)
75
73
  safe_metadata = redact_hash(metadata)
76
74
 
77
- # Log to appropriate file(s)
78
- if level == :debug
79
- write_to_debug(level, component, safe_message, safe_metadata)
80
- else
81
- write_to_info(level, component, safe_message, safe_metadata)
82
- end
83
-
84
- # Always log errors to both files
85
- if level == :error
86
- write_to_debug(level, component, safe_message, safe_metadata)
87
- end
75
+ write_entry(level, component, safe_message, safe_metadata)
88
76
  end
89
77
 
90
78
  # Close all loggers
91
79
  def close
92
- @info_logger&.close
93
- @debug_logger&.close
80
+ @logger&.close
94
81
  end
95
82
 
96
83
  private
@@ -111,12 +98,9 @@ module Aidp
111
98
  FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
112
99
  end
113
100
 
114
- def setup_loggers
101
+ def setup_logger
115
102
  info_path = File.join(@project_dir, INFO_LOG)
116
- debug_path = File.join(@project_dir, DEBUG_LOG)
117
-
118
- @info_logger = create_logger(info_path)
119
- @debug_logger = create_logger(debug_path)
103
+ @logger = create_logger(info_path)
120
104
  end
121
105
 
122
106
  def create_logger(path)
@@ -126,14 +110,9 @@ module Aidp
126
110
  logger
127
111
  end
128
112
 
129
- def write_to_info(level, component, message, metadata)
113
+ def write_entry(level, component, message, metadata)
130
114
  entry = format_entry(level, component, message, metadata)
131
- @info_logger.send(logger_method(level), entry)
132
- end
133
-
134
- def write_to_debug(level, component, message, metadata)
135
- entry = format_entry(level, component, message, metadata)
136
- @debug_logger.send(logger_method(level), entry)
115
+ @logger.send(logger_method(level), entry)
137
116
  end
138
117
 
139
118
  def logger_method(level)
@@ -205,58 +184,18 @@ module Aidp
205
184
  def redact_hash(hash)
206
185
  hash.transform_values { |v| v.is_a?(String) ? redact(v) : v }
207
186
  end
208
-
209
- # Migration from old debug_logs location
210
- OLD_DEBUG_DIR = ".aidp/debug_logs"
211
- OLD_DEBUG_LOG = "#{OLD_DEBUG_DIR}/aidp_debug.log"
212
-
213
- def should_migrate?
214
- old_path = File.join(@project_dir, OLD_DEBUG_LOG)
215
- new_path = File.join(@project_dir, DEBUG_LOG)
216
-
217
- # Migrate if old exists and new doesn't
218
- File.exist?(old_path) && !File.exist?(new_path)
219
- end
220
-
221
- def migrate_old_logs
222
- old_path = File.join(@project_dir, OLD_DEBUG_LOG)
223
- new_path = File.join(@project_dir, DEBUG_LOG)
224
-
225
- begin
226
- FileUtils.mv(old_path, new_path)
227
- log_migration_notice
228
- rescue => e
229
- # If migration fails, just continue (new logs will be created)
230
- warn "Failed to migrate old logs: #{e.message}"
231
- end
232
- end
233
-
234
- def log_migration_notice
235
- notice = format_text(
236
- :info,
237
- "migration",
238
- "Logs migrated from .aidp/debug_logs/ to .aidp/logs/",
239
- timestamp: Time.now.utc.iso8601
240
- )
241
-
242
- # Write directly to avoid recursion
243
- info_path = File.join(@project_dir, INFO_LOG)
244
- File.open(info_path, "a") do |f|
245
- f.puts notice
246
- end
247
- end
248
187
  end
249
188
 
250
189
  # Module-level logger accessor
251
190
  class << self
252
191
  # Set up global logger instance
253
192
  def setup_logger(project_dir = Dir.pwd, config = {})
254
- @logger = AidpLogger.new(project_dir, config)
193
+ @logger = Logger.new(project_dir, config)
255
194
  end
256
195
 
257
196
  # Get current logger instance (creates default if not set up)
258
197
  def logger
259
- @logger ||= AidpLogger.new
198
+ @logger ||= Logger.new
260
199
  end
261
200
 
262
201
  # Convenience logging methods
@@ -5,6 +5,8 @@ require "tty-spinner"
5
5
 
6
6
  module Aidp
7
7
  module Providers
8
+ class ProviderUnavailableError < StandardError; end
9
+
8
10
  class Base
9
11
  include Aidp::MessageDisplay
10
12
 
@@ -86,6 +86,16 @@ module Aidp
86
86
  # Log the results
87
87
  debug_command("copilot", args: args, input: prompt, output: result.out, error: result.err, exit_code: result.exit_status)
88
88
 
89
+ # Detect authorization/access errors
90
+ auth_error = result.err.to_s =~ /not authorized|requires an enterprise|access denied|permission denied|not enabled/i
91
+ if auth_error
92
+ spinner.error("✗")
93
+ mark_failed("copilot authorization error: #{result.err}")
94
+ @unavailable = true
95
+ debug_error(StandardError.new("copilot authorization error"), {exit_code: result.exit_status, stderr: result.err})
96
+ raise Aidp::Providers::ProviderUnavailableError.new("copilot authorization error: #{result.err}")
97
+ end
98
+
89
99
  if result.exit_status == 0
90
100
  spinner.success("✓")
91
101
  mark_completed
@@ -96,6 +106,8 @@ module Aidp
96
106
  debug_error(StandardError.new("copilot failed"), {exit_code: result.exit_status, stderr: result.err})
97
107
  raise "copilot failed with exit code #{result.exit_status}: #{result.err}"
98
108
  end
109
+ rescue Aidp::Providers::ProviderUnavailableError
110
+ raise
99
111
  rescue => e
100
112
  spinner&.error("✗")
101
113
  mark_failed("copilot execution failed: #{e.message}")
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ module Aidp
5
+ # Mixin providing a unified helper for logging rescued exceptions.
6
+ # Usage:
7
+ # include Aidp::RescueLogging
8
+ # rescue => e
9
+ # log_rescue(e, component: "storage", action: "store file", fallback: {success: false})
10
+ #
11
+ # Defaults:
12
+ # - level: :warn (so filtering WARN surfaces rescue sites)
13
+ # - includes error class, message
14
+ # - optional fallback and extra context hash merged in
15
+ module RescueLogging
16
+ def log_rescue(error, component:, action:, fallback: nil, level: :warn, **context)
17
+ data = {
18
+ error_class: error.class.name,
19
+ error_message: error.message,
20
+ action: action
21
+ }
22
+ data[:fallback] = fallback if fallback
23
+ data.merge!(context) unless context.empty?
24
+
25
+ # Prefer debug_mixin if present; otherwise use Aidp.logger directly
26
+ if respond_to?(:debug_log)
27
+ debug_log("⚠️ Rescue in #{component}: #{action}", level: level, data: data)
28
+ else
29
+ Aidp.logger.send(level, component, "Rescued exception during #{action}", **data)
30
+ end
31
+ rescue => logging_error
32
+ # Last resort: avoid raising from logging path - fall back to STDERR
33
+ warn "[AIDP Rescue Logging Error] Failed to log rescue for #{component}:#{action} - #{error.class}: #{error.message} (logging error: #{logging_error.message})"
34
+ end
35
+ end
36
+ end
@@ -143,46 +143,35 @@ module Aidp
143
143
  # Save primary provider
144
144
  set([:harness, :default_provider], provider_choice) unless provider_choice == "custom"
145
145
 
146
- # Prompt for fallback providers (excluding the primary)
146
+ ensure_provider_billing_config(provider_choice) unless provider_choice == "custom"
147
+
148
+ # Prompt for fallback providers (excluding the primary), pre-select existing
149
+ existing_fallbacks = Array(get([:harness, :fallback_providers])).map(&:to_s) - [provider_choice]
147
150
  fallback_choices = available_providers.reject { |_, name| name == provider_choice }
148
- fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):") do |menu|
151
+ fallback_selected = prompt.multi_select("Select fallback providers (used if primary fails):", default: existing_fallbacks) do |menu|
149
152
  fallback_choices.each do |display_name, provider_name|
150
153
  menu.choice display_name, provider_name
151
154
  end
152
155
  end
153
156
 
154
- # Remove any accidental duplication of primary provider & save
157
+ # If user selected none but we had existing fallbacks, confirm removal
158
+ if fallback_selected.empty? && existing_fallbacks.any?
159
+ keep = prompt.no?("No fallbacks selected. Remove existing fallbacks (#{existing_fallbacks.join(", ")})?", default: false)
160
+ fallback_selected = existing_fallbacks if keep
161
+ end
162
+
163
+ # Remove any accidental duplication of primary provider & save (preserve order)
155
164
  cleaned_fallbacks = fallback_selected.reject { |name| name == provider_choice }
156
165
  set([:harness, :fallback_providers], cleaned_fallbacks)
157
166
 
158
- # No LLM settings needed; provider agent handles LLM config
167
+ # Auto-create minimal provider configs for fallbacks if missing
168
+ cleaned_fallbacks.each { |fp| ensure_provider_billing_config(fp) }
159
169
 
160
- configure_mcp
161
- show_provider_secrets_help(provider_choice)
170
+ # Provide informational note (no secret handling stored)
171
+ show_provider_info_note(provider_choice) unless provider_choice == "custom"
162
172
  end
163
173
 
164
- def configure_mcp
165
- existing = get([:providers, :mcp]) || {}
166
- enabled = prompt.yes?("Enable MCP (Model Context Protocol) tools?", default: existing.fetch(:enabled, true))
167
- return delete_path([:providers, :mcp]) unless enabled
168
-
169
- # TODO: Add default back once TTY-Prompt default validation issue is resolved
170
- tools = prompt.multi_select("Select MCP tools:") do |menu|
171
- menu.choice "Git", "git"
172
- menu.choice "Shell", "shell"
173
- menu.choice "Filesystem", "fs"
174
- menu.choice "Browser", "browser"
175
- menu.choice "GitHub", "github"
176
- end
177
-
178
- custom = ask_list("Custom MCP servers (comma-separated)", existing.fetch(:custom_servers, []))
179
-
180
- set([:providers, :mcp], {
181
- enabled: true,
182
- tools: tools,
183
- custom_servers: custom
184
- }.compact)
185
- end
174
+ # Removed MCP configuration step (MCP now expected to be provider-specific if used)
186
175
 
187
176
  # -------------------------------------------
188
177
  # Work loop configuration
@@ -621,27 +610,34 @@ module Aidp
621
610
  :other
622
611
  end
623
612
 
624
- def default_model(provider)
625
- case provider
626
- when "anthropic" then "claude-3-5-sonnet-20241022"
627
- when "openai" then "gpt-4.1"
628
- when "google" then "gemini-1.5-pro"
629
- when "azure" then "gpt-4"
630
- else "claude-3-5-sonnet-20241022"
613
+ def show_provider_info_note(provider)
614
+ prompt.say("\n💡 Provider integration:")
615
+ prompt.say("AIDP does not store API keys or model lists. Configure the agent (#{provider}) externally.")
616
+ prompt.say("Only the billing model (subscription vs usage_based) is recorded for fallback decisions.")
617
+ end
618
+
619
+ # Ensure a minimal billing configuration exists for a selected provider (no secrets)
620
+ def ensure_provider_billing_config(provider_name)
621
+ return if provider_name.nil? || provider_name == "custom"
622
+ providers_section = get([:providers]) || {}
623
+ existing = providers_section[provider_name.to_sym]
624
+
625
+ if existing && existing[:type]
626
+ prompt.say(" • Provider '#{provider_name}' already configured (type: #{existing[:type]})")
627
+ return
631
628
  end
629
+
630
+ provider_type = ask_provider_billing_type(provider_name)
631
+ set([:providers, provider_name.to_sym], {type: provider_type})
632
+ prompt.say(" • Added provider '#{provider_name}' with billing type '#{provider_type}' (no secrets stored)")
632
633
  end
633
634
 
634
- def show_provider_secrets_help(provider)
635
- prompt.say("\n💡 Provider setup:")
636
- case provider
637
- when "anthropic"
638
- prompt.say("Export API key: export ANTHROPIC_API_KEY=sk-ant-...")
639
- when "openai", "azure"
640
- prompt.say("Export API key: export OPENAI_API_KEY=sk-...")
641
- when "google"
642
- prompt.say("Export API key: export GOOGLE_API_KEY=...")
643
- else
644
- prompt.say("Configure API credentials via environment variables.")
635
+ def ask_provider_billing_type(provider_name)
636
+ prompt.select("Billing model for #{provider_name}:") do |menu|
637
+ menu.choice "Subscription / flat-rate", "subscription"
638
+ # e.g. tools that expose an integrated model under a subscription cost
639
+ menu.choice "Usage-based / metered (API)", "usage_based"
640
+ menu.choice "Passthrough / local (no billing)", "passthrough"
645
641
  end
646
642
  end
647
643
 
@@ -2,11 +2,14 @@
2
2
 
3
3
  require "csv"
4
4
  require "fileutils"
5
+ require "aidp/rescue_logging"
5
6
 
6
7
  module Aidp
7
8
  module Storage
8
9
  # Simple CSV file storage for tabular data
9
10
  class CsvStorage
11
+ include Aidp::RescueLogging
12
+
10
13
  def initialize(base_dir = ".aidp")
11
14
  @base_dir = base_dir
12
15
  ensure_directory_exists
@@ -42,11 +45,13 @@ module Aidp
42
45
  success: true
43
46
  }
44
47
  rescue => error
45
- {
48
+ log_rescue(error,
49
+ component: "csv_storage",
50
+ action: "append",
51
+ fallback: {success: false},
46
52
  filename: filename,
47
- error: error.message,
48
- success: false
49
- }
53
+ path: file_path)
54
+ {filename: filename, error: error.message, success: false}
50
55
  end
51
56
 
52
57
  # Read all rows from CSV file
@@ -60,7 +65,12 @@ module Aidp
60
65
  end
61
66
  rows
62
67
  rescue => error
63
- puts "Error reading #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
68
+ log_rescue(error,
69
+ component: "csv_storage",
70
+ action: "read_all",
71
+ fallback: [],
72
+ filename: filename,
73
+ path: (defined?(file_path) ? file_path : nil))
64
74
  []
65
75
  end
66
76
 
@@ -83,7 +93,12 @@ module Aidp
83
93
  CSV.foreach(file_path) { count += 1 }
84
94
  count - 1 # Subtract 1 for header row
85
95
  rescue => error
86
- puts "Error counting rows in #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
96
+ log_rescue(error,
97
+ component: "csv_storage",
98
+ action: "count_rows",
99
+ fallback: 0,
100
+ filename: filename,
101
+ path: (defined?(file_path) ? file_path : nil))
87
102
  0
88
103
  end
89
104
 
@@ -127,7 +142,12 @@ module Aidp
127
142
 
128
143
  summary_data
129
144
  rescue => error
130
- puts "Error generating summary for #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
145
+ log_rescue(error,
146
+ component: "csv_storage",
147
+ action: "summary",
148
+ fallback: nil,
149
+ filename: filename,
150
+ path: (defined?(file_path) ? file_path : nil))
131
151
  nil
132
152
  end
133
153
 
@@ -144,6 +164,12 @@ module Aidp
144
164
  File.delete(file_path)
145
165
  {success: true, message: "File deleted"}
146
166
  rescue => error
167
+ log_rescue(error,
168
+ component: "csv_storage",
169
+ action: "delete",
170
+ fallback: {success: false},
171
+ filename: filename,
172
+ path: file_path)
147
173
  {success: false, error: error.message}
148
174
  end
149
175
 
@@ -2,11 +2,14 @@
2
2
 
3
3
  require "json"
4
4
  require "fileutils"
5
+ require "aidp/rescue_logging"
5
6
 
6
7
  module Aidp
7
8
  module Storage
8
9
  # Simple JSON file storage for structured data
9
10
  class JsonStorage
11
+ include Aidp::RescueLogging
12
+
10
13
  def initialize(base_dir = ".aidp")
11
14
  @base_dir = base_dir
12
15
  ensure_directory_exists
@@ -32,11 +35,13 @@ module Aidp
32
35
  success: true
33
36
  }
34
37
  rescue => error
35
- {
38
+ log_rescue(error,
39
+ component: "json_storage",
40
+ action: "store",
41
+ fallback: {success: false},
36
42
  filename: filename,
37
- error: error.message,
38
- success: false
39
- }
43
+ path: file_path)
44
+ {filename: filename, error: error.message, success: false}
40
45
  end
41
46
 
42
47
  # Load data from JSON file
@@ -48,7 +53,12 @@ module Aidp
48
53
  json_data = JSON.parse(content)
49
54
  json_data["data"]
50
55
  rescue => error
51
- puts "Error loading #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
56
+ log_rescue(error,
57
+ component: "json_storage",
58
+ action: "load",
59
+ fallback: nil,
60
+ filename: filename,
61
+ path: (defined?(file_path) ? file_path : nil))
52
62
  nil
53
63
  end
54
64
 
@@ -72,11 +82,13 @@ module Aidp
72
82
  success: true
73
83
  }
74
84
  rescue => error
75
- {
85
+ log_rescue(error,
86
+ component: "json_storage",
87
+ action: "update",
88
+ fallback: {success: false},
76
89
  filename: filename,
77
- error: error.message,
78
- success: false
79
- }
90
+ path: (defined?(file_path) ? file_path : nil))
91
+ {filename: filename, error: error.message, success: false}
80
92
  end
81
93
 
82
94
  # Check if file exists
@@ -92,6 +104,12 @@ module Aidp
92
104
  File.delete(file_path)
93
105
  {success: true, message: "File deleted"}
94
106
  rescue => error
107
+ log_rescue(error,
108
+ component: "json_storage",
109
+ action: "delete",
110
+ fallback: {success: false},
111
+ filename: filename,
112
+ path: file_path)
95
113
  {success: false, error: error.message}
96
114
  end
97
115
 
@@ -120,7 +138,12 @@ module Aidp
120
138
  size: File.size(file_path)
121
139
  }
122
140
  rescue => error
123
- puts "Error getting metadata for #{filename}: #{error.message}" if ENV["AIDP_DEBUG"]
141
+ log_rescue(error,
142
+ component: "json_storage",
143
+ action: "metadata",
144
+ fallback: nil,
145
+ filename: filename,
146
+ path: (defined?(file_path) ? file_path : nil))
124
147
  nil
125
148
  end
126
149
 
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.14.1"
4
+ VERSION = "0.15.0"
5
5
  end