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.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- 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
|