bugsage 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +442 -0
- data/CHANGELOG.md +28 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +301 -0
- data/LICENSE.txt +21 -0
- data/README.md +344 -0
- data/ROADMAP.md +217 -0
- data/SECURITY.md +83 -0
- data/docs/AI.md +167 -0
- data/docs/Configuration.md +168 -0
- data/docs/GettingStarted.md +146 -0
- data/docs/RELEASE_CHECKLIST.md +346 -0
- data/docs/Troubleshooting.md +181 -0
- data/docs/images/BadRequest.png +0 -0
- data/docs/images/BadRequest2.png +0 -0
- data/docs/images/BugSage_Logo.png +0 -0
- data/docs/images/BugSage_Social_Preview.png +0 -0
- data/docs/images/NoMethodError.png +0 -0
- data/docs/images/NoMethodError2.png +0 -0
- data/docs/images/README.md +38 -0
- data/docs/releases/README.md +21 -0
- data/docs/releases/v0.2.0.md +40 -0
- data/exe/bugsage +7 -0
- data/lib/bugsage/ai_analyzer.rb +145 -0
- data/lib/bugsage/ai_chat.rb +388 -0
- data/lib/bugsage/ai_context.rb +69 -0
- data/lib/bugsage/ai_panel.rb +708 -0
- data/lib/bugsage/ai_support.rb +36 -0
- data/lib/bugsage/auto_configurator.rb +97 -0
- data/lib/bugsage/cli.rb +59 -0
- data/lib/bugsage/code_context.rb +81 -0
- data/lib/bugsage/code_patch.rb +147 -0
- data/lib/bugsage/configuration.rb +149 -0
- data/lib/bugsage/console_context.rb +51 -0
- data/lib/bugsage/cursor_client.rb +165 -0
- data/lib/bugsage/dashboard.rb +627 -0
- data/lib/bugsage/editor_links.rb +34 -0
- data/lib/bugsage/error_page.rb +298 -0
- data/lib/bugsage/exception_handler.rb +66 -0
- data/lib/bugsage/exception_support.rb +95 -0
- data/lib/bugsage/exceptions_app.rb +31 -0
- data/lib/bugsage/fix_applicator.rb +107 -0
- data/lib/bugsage/formatter.rb +37 -0
- data/lib/bugsage/http_error_capture.rb +104 -0
- data/lib/bugsage/http_response_error.rb +14 -0
- data/lib/bugsage/inline_console.rb +226 -0
- data/lib/bugsage/installation.rb +94 -0
- data/lib/bugsage/installer.rb +66 -0
- data/lib/bugsage/json_endpoint.rb +34 -0
- data/lib/bugsage/locales/en.yml +439 -0
- data/lib/bugsage/middleware.rb +152 -0
- data/lib/bugsage/openai_client.rb +103 -0
- data/lib/bugsage/page_actions.rb +255 -0
- data/lib/bugsage/railtie.rb +49 -0
- data/lib/bugsage/request_context.rb +56 -0
- data/lib/bugsage/rule.rb +271 -0
- data/lib/bugsage/session_clear.rb +35 -0
- data/lib/bugsage/store.rb +60 -0
- data/lib/bugsage/suggestion.rb +58 -0
- data/lib/bugsage/trace_cleaner.rb +24 -0
- data/lib/bugsage/translations.rb +85 -0
- data/lib/bugsage/version.rb +5 -0
- data/lib/bugsage.rb +65 -0
- data/lib/generators/bugsage/install/install_generator.rb +21 -0
- data/lib/generators/bugsage/install/templates/bugsage.rb +22 -0
- data/scripts/publish-github-release.sh +38 -0
- data/sig/bugsage.rbs +4 -0
- metadata +157 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Bugsage
|
|
8
|
+
class CursorClient
|
|
9
|
+
TERMINAL_STATUSES = %w[FINISHED ERROR CANCELLED EXPIRED].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(config: Bugsage.configuration)
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def complete(system_prompt:, user_prompt:)
|
|
16
|
+
agent_id = nil
|
|
17
|
+
agent_id, run_id = create_agent(system_prompt, user_prompt, json_only: true)
|
|
18
|
+
result = poll_run(agent_id, run_id)
|
|
19
|
+
extract_json_content(result)
|
|
20
|
+
ensure
|
|
21
|
+
delete_agent(agent_id) if agent_id
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def chat(system_prompt:, messages:)
|
|
25
|
+
prompt = messages.map { |entry| "#{entry[:role].to_s.capitalize}: #{entry[:content]}" }.join("\n\n")
|
|
26
|
+
agent_id = nil
|
|
27
|
+
agent_id, run_id = create_agent(system_prompt, prompt, json_only: false)
|
|
28
|
+
result = poll_run(agent_id, run_id)
|
|
29
|
+
extract_text_content(result)
|
|
30
|
+
ensure
|
|
31
|
+
delete_agent(agent_id) if agent_id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def create_agent(system_prompt, user_prompt, json_only: true)
|
|
37
|
+
body = {
|
|
38
|
+
prompt: {
|
|
39
|
+
text: build_agent_prompt(system_prompt, user_prompt, json_only: json_only)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
body[:model] = { id: @config.cursor_model } if @config.cursor_model.to_s.strip != ""
|
|
43
|
+
|
|
44
|
+
response = request(
|
|
45
|
+
method: :post,
|
|
46
|
+
path: "/v1/agents",
|
|
47
|
+
body: body
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
agent_id = response.dig("agent", "id")
|
|
51
|
+
run_id = response.dig("run", "id")
|
|
52
|
+
if agent_id.to_s.empty? || run_id.to_s.empty?
|
|
53
|
+
raise Error,
|
|
54
|
+
"Cursor agent response did not include agent and run ids"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
[agent_id, run_id]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def poll_run(agent_id, run_id)
|
|
61
|
+
deadline = Time.now + @config.effective_ai_timeout
|
|
62
|
+
sleep_interval = 1.0
|
|
63
|
+
|
|
64
|
+
loop do
|
|
65
|
+
run = request(method: :get, path: "/v1/agents/#{agent_id}/runs/#{run_id}")
|
|
66
|
+
status = run["status"].to_s
|
|
67
|
+
|
|
68
|
+
if status == "FINISHED"
|
|
69
|
+
result = run["result"].to_s
|
|
70
|
+
raise Error, "Cursor run finished without a result" if result.strip.empty?
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
raise Error, "Cursor run ended with status #{status}" if TERMINAL_STATUSES.include?(status)
|
|
76
|
+
|
|
77
|
+
raise Error, "Cursor run timed out after #{@config.effective_ai_timeout}s" if Time.now >= deadline
|
|
78
|
+
|
|
79
|
+
sleep(sleep_interval)
|
|
80
|
+
sleep_interval = [sleep_interval * 1.5, 5.0].min
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def delete_agent(agent_id)
|
|
85
|
+
request(method: :delete, path: "/v1/agents/#{agent_id}")
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_agent_prompt(system_prompt, user_prompt, json_only: true)
|
|
91
|
+
reply_instruction = if json_only
|
|
92
|
+
"Reply with JSON only. Do not create files, run shell commands, or modify a repository."
|
|
93
|
+
else
|
|
94
|
+
"Reply in plain text. Do not create files, run shell commands, or modify a repository."
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
<<~PROMPT
|
|
98
|
+
#{system_prompt}
|
|
99
|
+
|
|
100
|
+
#{user_prompt}
|
|
101
|
+
|
|
102
|
+
#{reply_instruction}
|
|
103
|
+
PROMPT
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_json_content(text)
|
|
107
|
+
stripped = text.to_s.strip
|
|
108
|
+
stripped = stripped.sub(/\A.*?```(?:json)?\s*/im, "").sub(/\s*```.*\z/m, "") if stripped.include?("```")
|
|
109
|
+
|
|
110
|
+
match = stripped.match(/\{.*\}/m)
|
|
111
|
+
match ? match[0] : stripped
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def extract_text_content(text)
|
|
115
|
+
stripped = text.to_s.strip
|
|
116
|
+
stripped = stripped.sub(/\A```(?:\w*)\s*/m, "").sub(/\s*```\z/m, "") if stripped.start_with?("```")
|
|
117
|
+
stripped
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def request(method:, path:, body: nil)
|
|
121
|
+
uri = URI.join(api_base, path.sub(%r{\A/}, ""))
|
|
122
|
+
request_class = {
|
|
123
|
+
get: Net::HTTP::Get,
|
|
124
|
+
post: Net::HTTP::Post,
|
|
125
|
+
delete: Net::HTTP::Delete
|
|
126
|
+
}.fetch(method)
|
|
127
|
+
|
|
128
|
+
http_request = request_class.new(uri)
|
|
129
|
+
http_request["Authorization"] = "Bearer #{@config.resolved_cursor_api_key}"
|
|
130
|
+
http_request["Content-Type"] = "application/json"
|
|
131
|
+
http_request.body = JSON.generate(body) if body
|
|
132
|
+
|
|
133
|
+
response = Net::HTTP.start(
|
|
134
|
+
uri.host,
|
|
135
|
+
uri.port,
|
|
136
|
+
use_ssl: uri.scheme == "https",
|
|
137
|
+
open_timeout: @config.ai_timeout,
|
|
138
|
+
read_timeout: @config.ai_timeout
|
|
139
|
+
) do |http|
|
|
140
|
+
http.request(http_request)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise Error, error_message_for(response) unless response.is_a?(Net::HTTPSuccess)
|
|
144
|
+
|
|
145
|
+
return {} if response.body.to_s.strip.empty?
|
|
146
|
+
|
|
147
|
+
JSON.parse(response.body)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def api_base
|
|
151
|
+
base = @config.cursor_api_base.to_s
|
|
152
|
+
base.end_with?("/") ? base : "#{base}/"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def error_message_for(response)
|
|
156
|
+
body = JSON.parse(response.body)
|
|
157
|
+
detail = body["message"] || body.dig("error", "message") || body["error"]
|
|
158
|
+
message = "Cursor request failed with status #{response.code}"
|
|
159
|
+
message += ": #{detail}" if detail
|
|
160
|
+
message
|
|
161
|
+
rescue JSON::ParserError
|
|
162
|
+
"Cursor request failed with status #{response.code}"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|