aidp 0.14.1 → 0.14.2

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.
@@ -2,12 +2,14 @@
2
2
 
3
3
  require "tty-prompt"
4
4
  require_relative "provider_factory"
5
+ require_relative "../rescue_logging"
5
6
 
6
7
  module Aidp
7
8
  module Harness
8
9
  # Manages provider switching and fallback logic
9
10
  class ProviderManager
10
11
  include Aidp::MessageDisplay
12
+ include Aidp::RescueLogging
11
13
 
12
14
  def initialize(configuration, prompt: TTY::Prompt.new)
13
15
  @configuration = configuration
@@ -69,6 +71,8 @@ module Aidp
69
71
 
70
72
  # Switch to next available provider with sophisticated fallback logic
71
73
  def switch_provider(reason = "manual_switch", context = {})
74
+ Aidp.logger.info("provider_manager", "Attempting provider switch", reason: reason, current: current_provider, **context)
75
+
72
76
  # Get fallback chain for current provider
73
77
  provider_fallback_chain = fallback_chain(current_provider)
74
78
 
@@ -79,7 +83,10 @@ module Aidp
79
83
  success = set_current_provider(next_provider, reason, context)
80
84
  if success
81
85
  log_provider_switch(current_provider, next_provider, reason, context)
86
+ Aidp.logger.info("provider_manager", "Provider switched successfully", from: current_provider, to: next_provider, reason: reason)
82
87
  return next_provider
88
+ else
89
+ Aidp.logger.warn("provider_manager", "Failed to switch to provider", provider: next_provider, reason: reason)
83
90
  end
84
91
  end
85
92
 
@@ -107,14 +114,21 @@ module Aidp
107
114
 
108
115
  # No providers available
109
116
  log_no_providers_available(reason, context)
117
+ Aidp.logger.error("provider_manager", "No providers available for fallback", reason: reason, **context)
110
118
  nil
111
119
  end
112
120
 
113
121
  # Switch provider for specific error type
114
122
  def switch_provider_for_error(error_type, error_details = {})
123
+ Aidp.logger.warn("provider_manager", "Error triggered provider switch", error_type: error_type, **error_details)
124
+
115
125
  case error_type
116
126
  when "rate_limit"
117
127
  switch_provider("rate_limit", error_details)
128
+ when "resource_exhausted", "quota_exceeded"
129
+ # Treat capacity/resource exhaustion like rate limit for fallback purposes
130
+ Aidp.logger.warn("provider_manager", "Resource/quota exhaustion detected", classified_from: error_type)
131
+ switch_provider("rate_limit", error_details.merge(classified_from: error_type))
118
132
  when "authentication"
119
133
  switch_provider("authentication_error", error_details)
120
134
  when "network"
@@ -269,9 +283,27 @@ module Aidp
269
283
 
270
284
  # Set current provider with enhanced validation
271
285
  def set_current_provider(provider_name, reason = "manual_switch", context = {})
272
- return false unless @configuration.provider_configured?(provider_name)
273
- return false unless is_provider_healthy?(provider_name)
274
- return false if is_provider_circuit_breaker_open?(provider_name)
286
+ # Use provider_config for ConfigManager, provider_configured? for legacy Configuration
287
+ config_exists = if @configuration.respond_to?(:provider_config)
288
+ @configuration.provider_config(provider_name)
289
+ else
290
+ @configuration.provider_configured?(provider_name)
291
+ end
292
+
293
+ unless config_exists
294
+ Aidp.logger.warn("provider_manager", "Provider not configured", provider: provider_name)
295
+ return false
296
+ end
297
+
298
+ unless is_provider_healthy?(provider_name)
299
+ Aidp.logger.warn("provider_manager", "Provider not healthy", provider: provider_name)
300
+ return false
301
+ end
302
+
303
+ if is_provider_circuit_breaker_open?(provider_name)
304
+ Aidp.logger.warn("provider_manager", "Provider circuit breaker open", provider: provider_name)
305
+ return false
306
+ end
275
307
 
276
308
  # Update provider health
277
309
  update_provider_health(provider_name, "switched_to")
@@ -292,6 +324,7 @@ module Aidp
292
324
  @current_model = default_model(provider_name)
293
325
 
294
326
  @current_provider = provider_name
327
+ Aidp.logger.info("provider_manager", "Provider activated", provider: provider_name, reason: reason)
295
328
  true
296
329
  end
297
330
 
@@ -468,11 +501,24 @@ module Aidp
468
501
  # Build default fallback chain
469
502
  def build_default_fallback_chain(provider_name)
470
503
  all_providers = configured_providers
471
- fallback_chain = all_providers.dup
472
- fallback_chain.delete(provider_name)
473
- fallback_chain.unshift(provider_name) # Put current provider first
474
- @fallback_chains[provider_name] = fallback_chain
475
- fallback_chain
504
+
505
+ # Harness-defined explicit ordering has priority
506
+ harness_fallbacks = if @configuration.respond_to?(:fallback_providers)
507
+ Array(@configuration.fallback_providers).map(&:to_s)
508
+ else
509
+ []
510
+ end
511
+
512
+ # Construct ordered chain:
513
+ # 1. current provider first
514
+ # 2. harness fallback providers (excluding current and de-duplicated)
515
+ # 3. any remaining configured providers not already listed
516
+ ordered = [provider_name]
517
+ ordered += harness_fallbacks.reject { |p| p == provider_name || ordered.include?(p) }
518
+ ordered += all_providers.reject { |p| ordered.include?(p) }
519
+
520
+ @fallback_chains[provider_name] = ordered
521
+ ordered
476
522
  end
477
523
 
478
524
  # Find next healthy provider in fallback chain
@@ -1008,7 +1054,8 @@ module Aidp
1008
1054
  require_relative "../providers/cursor"
1009
1055
  installed = Aidp::Providers::Cursor.available?
1010
1056
  end
1011
- rescue LoadError
1057
+ rescue LoadError => e
1058
+ log_rescue(e, component: "provider_manager", action: "check_provider_availability", fallback: false, provider: provider_name)
1012
1059
  installed = false
1013
1060
  end
1014
1061
  @unavailable_cache[provider_name] = installed
@@ -1048,7 +1095,8 @@ module Aidp
1048
1095
  end
1049
1096
  path = begin
1050
1097
  Aidp::Util.which(binary)
1051
- rescue
1098
+ rescue => e
1099
+ log_rescue(e, component: "provider_manager", action: "locate_binary", fallback: nil, binary: binary)
1052
1100
  nil
1053
1101
  end
1054
1102
  unless path
@@ -1074,13 +1122,15 @@ module Aidp
1074
1122
  # Timeout -> kill
1075
1123
  begin
1076
1124
  Process.kill("TERM", pid)
1077
- rescue
1125
+ rescue => e
1126
+ log_rescue(e, component: "provider_manager", action: "kill_timeout_process_term", fallback: nil, binary: binary, pid: pid)
1078
1127
  nil
1079
1128
  end
1080
1129
  sleep 0.1
1081
1130
  begin
1082
1131
  Process.kill("KILL", pid)
1083
- rescue
1132
+ rescue => e
1133
+ log_rescue(e, component: "provider_manager", action: "kill_timeout_process_kill", fallback: nil, binary: binary, pid: pid)
1084
1134
  nil
1085
1135
  end
1086
1136
  ok = false
@@ -1093,6 +1143,7 @@ module Aidp
1093
1143
  ok = true
1094
1144
  end
1095
1145
  rescue => e
1146
+ log_rescue(e, component: "provider_manager", action: "verify_binary_health", fallback: "binary_error", binary: binary)
1096
1147
  ok = false
1097
1148
  reason = e.class.name.downcase.include?("enoent") ? "binary_missing" : "binary_error"
1098
1149
  end
@@ -1340,6 +1391,7 @@ module Aidp
1340
1391
  }
1341
1392
  }
1342
1393
  rescue => e
1394
+ log_rescue(e, component: "provider_manager", action: "execute_with_provider", fallback: "error_result", provider: provider_type, prompt_length: prompt.length)
1343
1395
  # Return error result
1344
1396
  {
1345
1397
  status: "error",
@@ -3,6 +3,7 @@
3
3
  require "securerandom"
4
4
  require "yaml"
5
5
  require "fileutils"
6
+ require_relative "../rescue_logging"
6
7
 
7
8
  module Aidp
8
9
  module Jobs
@@ -10,6 +11,7 @@ module Aidp
10
11
  # Runs harness in daemon process and tracks job metadata
11
12
  class BackgroundRunner
12
13
  include Aidp::MessageDisplay
14
+ include Aidp::RescueLogging
13
15
 
14
16
  attr_reader :project_dir, :jobs_dir
15
17
 
@@ -55,6 +57,7 @@ module Aidp
55
57
  puts "[#{Time.now}] Job completed with status: #{result[:status]}"
56
58
  mark_job_completed(job_id, result)
57
59
  rescue => e
60
+ log_rescue(e, component: "background_runner", action: "execute_job", fallback: "mark_failed", job_id: job_id, mode: mode)
58
61
  puts "[#{Time.now}] Job failed with error: #{e.message}"
59
62
  puts e.backtrace.join("\n")
60
63
  mark_job_failed(job_id, e)
@@ -136,11 +139,13 @@ module Aidp
136
139
 
137
140
  mark_job_stopped(job_id)
138
141
  {success: true, message: "Job stopped successfully"}
139
- rescue Errno::ESRCH
142
+ rescue Errno::ESRCH => e
143
+ log_rescue(e, component: "background_runner", action: "stop_job", fallback: "mark_stopped", job_id: job_id, pid: pid, level: :info)
140
144
  # Process already dead
141
145
  mark_job_stopped(job_id)
142
146
  {success: true, message: "Job was already stopped"}
143
147
  rescue => e
148
+ log_rescue(e, component: "background_runner", action: "stop_job", fallback: "error_result", job_id: job_id, pid: pid)
144
149
  {success: false, message: "Failed to stop job: #{e.message}"}
145
150
  end
146
151
  end
@@ -244,7 +249,8 @@ module Aidp
244
249
 
245
250
  Process.kill(0, pid)
246
251
  true
247
- rescue Errno::ESRCH, Errno::EPERM
252
+ rescue Errno::ESRCH, Errno::EPERM => e
253
+ log_rescue(e, component: "background_runner", action: "check_process_running", fallback: false, pid: pid, level: :debug)
248
254
  false
249
255
  end
250
256
 
@@ -252,7 +258,8 @@ module Aidp
252
258
  # Try to load checkpoint from project directory
253
259
  checkpoint = Aidp::Execute::Checkpoint.new(@project_dir)
254
260
  checkpoint.latest_checkpoint
255
- rescue
261
+ rescue => e
262
+ log_rescue(e, component: "background_runner", action: "get_job_checkpoint", fallback: nil, job_id: job_id)
256
263
  nil
257
264
  end
258
265
 
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