aidp 0.34.1 → 0.39.3

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/usage_limit.rb +179 -0
  61. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  62. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  63. data/lib/aidp/harness/usage_period.rb +234 -0
  64. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  65. data/lib/aidp/init/runner.rb +25 -4
  66. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  67. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  68. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  69. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  70. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  71. data/lib/aidp/logger.rb +25 -7
  72. data/lib/aidp/orchestration_adapter.rb +279 -0
  73. data/lib/aidp/pr_worktree_manager.rb +4 -15
  74. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  75. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  76. data/lib/aidp/prompts/template_evolver.rb +305 -0
  77. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  78. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  79. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  80. data/lib/aidp/setup/wizard.rb +616 -176
  81. data/lib/aidp/shell_executor.rb +58 -0
  82. data/lib/aidp/style_guide/selector.rb +56 -1
  83. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  84. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  85. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  86. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  87. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  88. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  89. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  90. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  91. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  92. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  93. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  94. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  95. data/lib/aidp/temporal/configuration.rb +169 -0
  96. data/lib/aidp/temporal/connection.rb +93 -0
  97. data/lib/aidp/temporal/worker.rb +119 -0
  98. data/lib/aidp/temporal/workflow_client.rb +171 -0
  99. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  100. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  101. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  102. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  103. data/lib/aidp/temporal.rb +120 -0
  104. data/lib/aidp/version.rb +1 -1
  105. data/lib/aidp/watch/base_processor.rb +35 -0
  106. data/lib/aidp/watch/build_processor.rb +132 -0
  107. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  108. data/lib/aidp/watch/rebase_processor.rb +245 -0
  109. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  110. data/lib/aidp/watch/runner.rb +366 -285
  111. data/lib/aidp/watch/state_store.rb +80 -0
  112. data/lib/aidp/watch/work_item.rb +96 -0
  113. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  114. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  115. data/lib/aidp/workflows/guided_agent.rb +1 -1
  116. data/lib/aidp/worktree.rb +62 -0
  117. data/templates/aidp.yml.example +70 -0
  118. metadata +117 -16
  119. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  120. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  121. data/lib/aidp/providers/adapter.rb +0 -239
  122. data/lib/aidp/providers/aider.rb +0 -264
  123. data/lib/aidp/providers/anthropic.rb +0 -614
  124. data/lib/aidp/providers/base.rb +0 -617
  125. data/lib/aidp/providers/capability_registry.rb +0 -204
  126. data/lib/aidp/providers/codex.rb +0 -267
  127. data/lib/aidp/providers/cursor.rb +0 -304
  128. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  129. data/lib/aidp/providers/gemini.rb +0 -110
  130. data/lib/aidp/providers/github_copilot.rb +0 -227
  131. data/lib/aidp/providers/kilocode.rb +0 -157
  132. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for provider info cache
9
+ # Replaces: .aidp/providers/{provider_name}_info.yml
10
+ #
11
+ # Stores detailed information about AI providers gathered from CLI introspection
12
+ class ProviderInfoCacheRepository < Repository
13
+ DEFAULT_TTL = 86400 # 24 hours in seconds
14
+
15
+ def initialize(project_dir: Dir.pwd)
16
+ super(project_dir: project_dir, table_name: "provider_info_cache")
17
+ end
18
+
19
+ # Store provider info in cache
20
+ #
21
+ # @param provider_name [String] Provider name (e.g., "claude", "cursor")
22
+ # @param info [Hash] Provider info hash
23
+ # @param ttl [Integer] Time to live in seconds
24
+ # @return [Boolean] Success status
25
+ def cache(provider_name, info, ttl: DEFAULT_TTL)
26
+ Aidp.log_debug("provider_info_cache_repo", "cache",
27
+ provider: provider_name, ttl: ttl)
28
+
29
+ expires_at = Time.now.utc + ttl
30
+
31
+ execute(<<~SQL, [project_dir, provider_name, serialize_json(info), current_timestamp, expires_at.strftime("%Y-%m-%d %H:%M:%S")])
32
+ INSERT INTO #{table_name} (project_dir, provider_name, info, cached_at, expires_at)
33
+ VALUES (?, ?, ?, ?, ?)
34
+ ON CONFLICT(project_dir, provider_name) DO UPDATE SET
35
+ info = excluded.info,
36
+ cached_at = excluded.cached_at,
37
+ expires_at = excluded.expires_at
38
+ SQL
39
+
40
+ true
41
+ rescue => e
42
+ Aidp.log_error("provider_info_cache_repo", "cache_failed",
43
+ provider: provider_name, error: e.message)
44
+ false
45
+ end
46
+
47
+ # Get cached provider info if not expired
48
+ #
49
+ # @param provider_name [String] Provider name
50
+ # @return [Hash, nil] Cached info or nil if expired/not found
51
+ def get(provider_name)
52
+ Aidp.log_debug("provider_info_cache_repo", "get", provider: provider_name)
53
+
54
+ row = query_one(<<~SQL, [project_dir, provider_name])
55
+ SELECT info, cached_at, expires_at
56
+ FROM #{table_name}
57
+ WHERE project_dir = ? AND provider_name = ?
58
+ SQL
59
+
60
+ return nil unless row
61
+
62
+ # Check expiration
63
+ if row["expires_at"]
64
+ # Parse as UTC since timestamps are stored in UTC
65
+ expires_at = Time.parse(row["expires_at"] + " UTC")
66
+ if Time.now.utc > expires_at
67
+ Aidp.log_debug("provider_info_cache_repo", "cache_expired",
68
+ provider: provider_name, expires_at: expires_at)
69
+ return nil
70
+ end
71
+ end
72
+
73
+ deserialize_json(row["info"])
74
+ end
75
+
76
+ # Get cached info, ignoring expiration
77
+ #
78
+ # @param provider_name [String] Provider name
79
+ # @return [Hash, nil] Cached info or nil if not found
80
+ def get_stale(provider_name)
81
+ row = query_one(<<~SQL, [project_dir, provider_name])
82
+ SELECT info FROM #{table_name}
83
+ WHERE project_dir = ? AND provider_name = ?
84
+ SQL
85
+
86
+ return nil unless row
87
+
88
+ deserialize_json(row["info"])
89
+ end
90
+
91
+ # Check if cache is stale for a provider
92
+ #
93
+ # @param provider_name [String] Provider name
94
+ # @param max_age [Integer] Maximum age in seconds
95
+ # @return [Boolean] True if stale or not found
96
+ def stale?(provider_name, max_age: DEFAULT_TTL)
97
+ row = query_one(<<~SQL, [project_dir, provider_name])
98
+ SELECT cached_at FROM #{table_name}
99
+ WHERE project_dir = ? AND provider_name = ?
100
+ SQL
101
+
102
+ return true unless row
103
+
104
+ # Parse as UTC since timestamps are stored in UTC
105
+ cached_at = Time.parse(row["cached_at"] + " UTC")
106
+ (Time.now.utc - cached_at) > max_age
107
+ end
108
+
109
+ # Invalidate cache for a specific provider
110
+ #
111
+ # @param provider_name [String] Provider name
112
+ def invalidate(provider_name)
113
+ Aidp.log_debug("provider_info_cache_repo", "invalidate",
114
+ provider: provider_name)
115
+
116
+ execute(<<~SQL, [project_dir, provider_name])
117
+ DELETE FROM #{table_name}
118
+ WHERE project_dir = ? AND provider_name = ?
119
+ SQL
120
+ end
121
+
122
+ # Invalidate all cached providers for project
123
+ def invalidate_all
124
+ Aidp.log_debug("provider_info_cache_repo", "invalidate_all")
125
+ delete_by_project
126
+ end
127
+
128
+ # List all cached providers
129
+ #
130
+ # @param include_expired [Boolean] Include expired entries
131
+ # @return [Array<String>] Provider names
132
+ def cached_providers(include_expired: false)
133
+ sql = if include_expired
134
+ <<~SQL
135
+ SELECT provider_name FROM #{table_name}
136
+ WHERE project_dir = ?
137
+ ORDER BY provider_name
138
+ SQL
139
+ else
140
+ <<~SQL
141
+ SELECT provider_name FROM #{table_name}
142
+ WHERE project_dir = ?
143
+ AND (expires_at IS NULL OR datetime(expires_at) > datetime('now'))
144
+ ORDER BY provider_name
145
+ SQL
146
+ end
147
+
148
+ query(sql, [project_dir]).map { |row| row["provider_name"] }
149
+ end
150
+
151
+ # Get cache statistics
152
+ #
153
+ # @return [Hash] Statistics
154
+ def stats
155
+ total = query_value(<<~SQL, [project_dir])
156
+ SELECT COUNT(*) FROM #{table_name} WHERE project_dir = ?
157
+ SQL
158
+
159
+ valid = query_value(<<~SQL, [project_dir])
160
+ SELECT COUNT(*) FROM #{table_name}
161
+ WHERE project_dir = ?
162
+ AND (expires_at IS NULL OR datetime(expires_at) > datetime('now'))
163
+ SQL
164
+
165
+ expired = query_value(<<~SQL, [project_dir])
166
+ SELECT COUNT(*) FROM #{table_name}
167
+ WHERE project_dir = ?
168
+ AND expires_at IS NOT NULL
169
+ AND datetime(expires_at) <= datetime('now')
170
+ SQL
171
+
172
+ {
173
+ total_cached: total || 0,
174
+ valid_entries: valid || 0,
175
+ expired_entries: expired || 0,
176
+ providers: cached_providers(include_expired: true)
177
+ }
178
+ end
179
+
180
+ # Clean up expired entries
181
+ #
182
+ # @return [Integer] Number of entries deleted
183
+ def cleanup_expired
184
+ result = query_value(<<~SQL, [project_dir])
185
+ SELECT COUNT(*) FROM #{table_name}
186
+ WHERE project_dir = ?
187
+ AND expires_at IS NOT NULL
188
+ AND datetime(expires_at) <= datetime('now')
189
+ SQL
190
+
191
+ execute(<<~SQL, [project_dir])
192
+ DELETE FROM #{table_name}
193
+ WHERE project_dir = ?
194
+ AND expires_at IS NOT NULL
195
+ AND datetime(expires_at) <= datetime('now')
196
+ SQL
197
+
198
+ count = result || 0
199
+ if count > 0
200
+ Aidp.log_info("provider_info_cache_repo", "cleanup_expired",
201
+ deleted: count)
202
+ end
203
+ count
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+
5
+ module Aidp
6
+ module Database
7
+ module Repositories
8
+ # Repository for provider_metrics and provider_rate_limits tables
9
+ # Replaces provider_metrics.yml and provider_rate_limits.yml
10
+ class ProviderMetricsRepository < Repository
11
+ def initialize(project_dir: Dir.pwd)
12
+ super(project_dir: project_dir, table_name: "provider_metrics")
13
+ end
14
+
15
+ # Save metrics for a provider
16
+ #
17
+ # @param provider_name [String] Provider name
18
+ # @param metrics [Hash] Metrics data (success_count, error_count, avg_latency, etc.)
19
+ # @param model_name [String, nil] Optional model name
20
+ def save_metrics(provider_name, metrics, model_name: nil)
21
+ now = current_timestamp
22
+
23
+ # Convert Time values to ISO8601 strings
24
+ normalized = metrics.transform_values do |value|
25
+ value.is_a?(Time) ? value.iso8601 : value
26
+ end
27
+
28
+ execute(
29
+ insert_sql([:project_dir, :provider_name, :model_name, :metrics, :recorded_at]),
30
+ [project_dir, provider_name, model_name, serialize_json(normalized), now]
31
+ )
32
+
33
+ Aidp.log_debug("provider_metrics_repository", "saved_metrics",
34
+ provider: provider_name, metrics_count: metrics.size)
35
+ end
36
+
37
+ # Load metrics for all providers
38
+ #
39
+ # @return [Hash] Map of provider_name => metrics hash
40
+ def load_metrics
41
+ # Get the latest metrics for each provider
42
+ rows = query(
43
+ <<~SQL,
44
+ SELECT provider_name, model_name, metrics, recorded_at
45
+ FROM provider_metrics
46
+ WHERE project_dir = ?
47
+ AND id IN (
48
+ SELECT MAX(id) FROM provider_metrics
49
+ WHERE project_dir = ?
50
+ GROUP BY provider_name
51
+ )
52
+ SQL
53
+ [project_dir, project_dir]
54
+ )
55
+
56
+ result = {}
57
+ rows.each do |row|
58
+ provider = row["provider_name"]
59
+ metrics = deserialize_json(row["metrics"]) || {}
60
+ result[provider] = symbolize_keys(metrics)
61
+ result[provider][:recorded_at] = row["recorded_at"]
62
+ end
63
+
64
+ result
65
+ end
66
+
67
+ # Load metrics for a specific provider
68
+ #
69
+ # @param provider_name [String] Provider name
70
+ # @return [Hash] Metrics
71
+ def load_provider_metrics(provider_name)
72
+ row = query_one(
73
+ <<~SQL,
74
+ SELECT metrics, recorded_at
75
+ FROM provider_metrics
76
+ WHERE project_dir = ? AND provider_name = ?
77
+ ORDER BY recorded_at DESC
78
+ LIMIT 1
79
+ SQL
80
+ [project_dir, provider_name]
81
+ )
82
+
83
+ return {} unless row
84
+
85
+ metrics = deserialize_json(row["metrics"]) || {}
86
+ symbolize_keys(metrics)
87
+ end
88
+
89
+ # Save rate limit info for a provider
90
+ #
91
+ # @param provider_name [String] Provider name
92
+ # @param rate_limits [Hash] Rate limit data
93
+ # @param model_name [String, nil] Optional model name
94
+ def save_rate_limits(provider_name, rate_limits, model_name: nil)
95
+ now = current_timestamp
96
+
97
+ # Convert Time values to ISO8601 strings
98
+ normalized = rate_limits.transform_values do |info|
99
+ next info unless info.is_a?(Hash)
100
+
101
+ info.transform_values do |value|
102
+ value.is_a?(Time) ? value.iso8601 : value
103
+ end
104
+ end
105
+
106
+ upsert_rate_limit(
107
+ provider_name: provider_name,
108
+ model_name: model_name,
109
+ rate_limit_info: serialize_json(normalized),
110
+ updated_at: now
111
+ )
112
+
113
+ Aidp.log_debug("provider_metrics_repository", "saved_rate_limits",
114
+ provider: provider_name)
115
+ end
116
+
117
+ # Load rate limits for all providers
118
+ #
119
+ # @return [Hash] Map of provider_name => rate limits hash
120
+ def load_rate_limits
121
+ rows = query(
122
+ "SELECT provider_name, model_name, rate_limit_info, updated_at FROM provider_rate_limits WHERE project_dir = ?",
123
+ [project_dir]
124
+ )
125
+
126
+ result = {}
127
+ rows.each do |row|
128
+ provider = row["provider_name"]
129
+ limits = deserialize_json(row["rate_limit_info"]) || {}
130
+ result[provider] = symbolize_keys_deep(limits)
131
+ end
132
+
133
+ result
134
+ end
135
+
136
+ # Load rate limits for a specific provider
137
+ #
138
+ # @param provider_name [String] Provider name
139
+ # @return [Hash] Rate limits
140
+ def load_provider_rate_limits(provider_name)
141
+ row = query_one(
142
+ "SELECT rate_limit_info, updated_at FROM provider_rate_limits WHERE project_dir = ? AND provider_name = ?",
143
+ [project_dir, provider_name]
144
+ )
145
+
146
+ return {} unless row
147
+
148
+ limits = deserialize_json(row["rate_limit_info"]) || {}
149
+ symbolize_keys_deep(limits)
150
+ end
151
+
152
+ # Clear all metrics and rate limits
153
+ def clear
154
+ transaction do
155
+ execute("DELETE FROM provider_metrics WHERE project_dir = ?", [project_dir])
156
+ execute("DELETE FROM provider_rate_limits WHERE project_dir = ?", [project_dir])
157
+ end
158
+ Aidp.log_debug("provider_metrics_repository", "cleared")
159
+ end
160
+
161
+ # Get metrics history for a provider
162
+ #
163
+ # @param provider_name [String] Provider name
164
+ # @param metric_type [String, nil] Metric type to extract (optional)
165
+ # @param limit [Integer] Max records
166
+ # @return [Array<Hash>] Historical values
167
+ def metrics_history(provider_name, metric_type = nil, limit: 100)
168
+ rows = query(
169
+ <<~SQL,
170
+ SELECT metrics, recorded_at
171
+ FROM provider_metrics
172
+ WHERE project_dir = ? AND provider_name = ?
173
+ ORDER BY recorded_at DESC
174
+ LIMIT ?
175
+ SQL
176
+ [project_dir, provider_name, limit]
177
+ )
178
+
179
+ rows.map do |row|
180
+ metrics = deserialize_json(row["metrics"]) || {}
181
+ if metric_type
182
+ {value: metrics[metric_type.to_s] || metrics[metric_type.to_sym], recorded_at: row["recorded_at"]}
183
+ else
184
+ {metrics: symbolize_keys(metrics), recorded_at: row["recorded_at"]}
185
+ end
186
+ end
187
+ end
188
+
189
+ private
190
+
191
+ def upsert_rate_limit(provider_name:, model_name:, rate_limit_info:, updated_at:)
192
+ existing = query_one(
193
+ "SELECT id FROM provider_rate_limits WHERE project_dir = ? AND provider_name = ? AND (model_name = ? OR (model_name IS NULL AND ? IS NULL))",
194
+ [project_dir, provider_name, model_name, model_name]
195
+ )
196
+
197
+ if existing
198
+ execute(
199
+ <<~SQL,
200
+ UPDATE provider_rate_limits SET
201
+ rate_limit_info = ?, updated_at = ?
202
+ WHERE id = ?
203
+ SQL
204
+ [rate_limit_info, updated_at, existing["id"]]
205
+ )
206
+ else
207
+ execute(
208
+ <<~SQL,
209
+ INSERT INTO provider_rate_limits
210
+ (project_dir, provider_name, model_name, rate_limit_info, updated_at)
211
+ VALUES (?, ?, ?, ?, ?)
212
+ SQL
213
+ [project_dir, provider_name, model_name, rate_limit_info, updated_at]
214
+ )
215
+ end
216
+ end
217
+
218
+ def symbolize_keys(hash)
219
+ return {} unless hash
220
+
221
+ hash.each_with_object({}) do |(key, value), memo|
222
+ memo[key.to_sym] = value
223
+ end
224
+ end
225
+
226
+ def symbolize_keys_deep(hash)
227
+ return {} unless hash
228
+
229
+ hash.each_with_object({}) do |(key, value), memo|
230
+ memo[key.to_sym] = value.is_a?(Hash) ? symbolize_keys_deep(value) : value
231
+ end
232
+ end
233
+
234
+ def parse_time(value)
235
+ return nil if value.nil?
236
+ return value if value.is_a?(Time)
237
+
238
+ Time.parse(value)
239
+ rescue ArgumentError
240
+ nil
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../repository"
4
+ require "securerandom"
5
+
6
+ module Aidp
7
+ module Database
8
+ module Repositories
9
+ # Repository for secrets_registry table
10
+ # Replaces security/secrets_registry.json
11
+ # Note: Only stores metadata about secrets, never actual values
12
+ class SecretsRepository < Repository
13
+ def initialize(project_dir: Dir.pwd)
14
+ super(project_dir: project_dir, table_name: "secrets_registry")
15
+ end
16
+
17
+ # Register a secret
18
+ #
19
+ # @param name [String] Secret name
20
+ # @param env_var [String] Environment variable containing the secret
21
+ # @param description [String, nil] Description
22
+ # @param scopes [Array<String>] Allowed operation scopes
23
+ # @return [Hash] Registration details
24
+ def register(name:, env_var:, description: nil, scopes: [])
25
+ now = current_timestamp
26
+ secret_id = SecureRandom.hex(8)
27
+
28
+ # Warn if env var doesn't exist
29
+ unless ENV.key?(env_var)
30
+ Aidp.log_warn("secrets_repository", "env_var_not_found",
31
+ name: name, env_var: env_var)
32
+ end
33
+
34
+ execute(
35
+ insert_sql([
36
+ :project_dir, :secret_id, :secret_name, :env_var,
37
+ :description, :scopes, :registered_at
38
+ ]),
39
+ [
40
+ project_dir,
41
+ secret_id,
42
+ name,
43
+ env_var,
44
+ description,
45
+ serialize_json(scopes),
46
+ now
47
+ ]
48
+ )
49
+
50
+ Aidp.log_debug("secrets_repository", "registered",
51
+ name: name, env_var: env_var)
52
+
53
+ {
54
+ id: secret_id,
55
+ name: name,
56
+ env_var: env_var,
57
+ description: description,
58
+ scopes: scopes,
59
+ registered_at: now
60
+ }
61
+ end
62
+
63
+ # Unregister a secret
64
+ #
65
+ # @param name [String] Secret name
66
+ # @return [Boolean] true if removed
67
+ def unregister(name:)
68
+ existing = find(name)
69
+ return false unless existing
70
+
71
+ execute(
72
+ "DELETE FROM secrets_registry WHERE project_dir = ? AND secret_name = ?",
73
+ [project_dir, name]
74
+ )
75
+
76
+ Aidp.log_debug("secrets_repository", "unregistered", name: name)
77
+ true
78
+ end
79
+
80
+ # Check if secret is registered
81
+ #
82
+ # @param name [String] Secret name
83
+ # @return [Boolean]
84
+ def registered?(name)
85
+ count = query_value(
86
+ "SELECT COUNT(*) FROM secrets_registry WHERE project_dir = ? AND secret_name = ?",
87
+ [project_dir, name]
88
+ )
89
+ count.positive?
90
+ end
91
+
92
+ # Get registration details
93
+ #
94
+ # @param name [String] Secret name
95
+ # @return [Hash, nil] Registration or nil
96
+ def find(name)
97
+ row = query_one(
98
+ "SELECT * FROM secrets_registry WHERE project_dir = ? AND secret_name = ?",
99
+ [project_dir, name]
100
+ )
101
+ deserialize_secret(row)
102
+ end
103
+
104
+ # Alias for find
105
+ alias_method :get, :find
106
+
107
+ # Get env var for a secret
108
+ #
109
+ # @param name [String] Secret name
110
+ # @return [String, nil] Env var name or nil
111
+ def env_var_for(name)
112
+ row = query_one(
113
+ "SELECT env_var FROM secrets_registry WHERE project_dir = ? AND secret_name = ?",
114
+ [project_dir, name]
115
+ )
116
+ row&.dig("env_var")
117
+ end
118
+
119
+ # List all registered secrets
120
+ #
121
+ # @return [Array<Hash>]
122
+ def list
123
+ rows = query(
124
+ "SELECT * FROM secrets_registry WHERE project_dir = ? ORDER BY registered_at",
125
+ [project_dir]
126
+ )
127
+
128
+ rows.map do |row|
129
+ secret = deserialize_secret(row)
130
+ secret[:has_value] = ENV.key?(secret[:env_var]) if secret
131
+ secret
132
+ end.compact
133
+ end
134
+
135
+ # Get env vars that should be stripped
136
+ #
137
+ # @return [Array<String>]
138
+ def env_vars_to_strip
139
+ rows = query(
140
+ "SELECT DISTINCT env_var FROM secrets_registry WHERE project_dir = ?",
141
+ [project_dir]
142
+ )
143
+ rows.map { |r| r["env_var"] }.compact
144
+ end
145
+
146
+ # Check if an env var is registered
147
+ #
148
+ # @param env_var [String] Environment variable name
149
+ # @return [Boolean]
150
+ def env_var_registered?(env_var)
151
+ count = query_value(
152
+ "SELECT COUNT(*) FROM secrets_registry WHERE project_dir = ? AND env_var = ?",
153
+ [project_dir, env_var]
154
+ )
155
+ count.positive?
156
+ end
157
+
158
+ # Get secret name for an env var
159
+ #
160
+ # @param env_var [String] Environment variable name
161
+ # @return [String, nil] Secret name or nil
162
+ def name_for_env_var(env_var)
163
+ row = query_one(
164
+ "SELECT secret_name FROM secrets_registry WHERE project_dir = ? AND env_var = ?",
165
+ [project_dir, env_var]
166
+ )
167
+ row&.dig("secret_name")
168
+ end
169
+
170
+ private
171
+
172
+ def deserialize_secret(row)
173
+ return nil unless row
174
+
175
+ {
176
+ id: row["secret_id"],
177
+ name: row["secret_name"],
178
+ env_var: row["env_var"],
179
+ description: row["description"],
180
+ scopes: deserialize_json(row["scopes"]) || [],
181
+ registered_at: row["registered_at"]
182
+ }
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end