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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +442 -0
  3. data/CHANGELOG.md +28 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/CONTRIBUTING.md +301 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +344 -0
  8. data/ROADMAP.md +217 -0
  9. data/SECURITY.md +83 -0
  10. data/docs/AI.md +167 -0
  11. data/docs/Configuration.md +168 -0
  12. data/docs/GettingStarted.md +146 -0
  13. data/docs/RELEASE_CHECKLIST.md +346 -0
  14. data/docs/Troubleshooting.md +181 -0
  15. data/docs/images/BadRequest.png +0 -0
  16. data/docs/images/BadRequest2.png +0 -0
  17. data/docs/images/BugSage_Logo.png +0 -0
  18. data/docs/images/BugSage_Social_Preview.png +0 -0
  19. data/docs/images/NoMethodError.png +0 -0
  20. data/docs/images/NoMethodError2.png +0 -0
  21. data/docs/images/README.md +38 -0
  22. data/docs/releases/README.md +21 -0
  23. data/docs/releases/v0.2.0.md +40 -0
  24. data/exe/bugsage +7 -0
  25. data/lib/bugsage/ai_analyzer.rb +145 -0
  26. data/lib/bugsage/ai_chat.rb +388 -0
  27. data/lib/bugsage/ai_context.rb +69 -0
  28. data/lib/bugsage/ai_panel.rb +708 -0
  29. data/lib/bugsage/ai_support.rb +36 -0
  30. data/lib/bugsage/auto_configurator.rb +97 -0
  31. data/lib/bugsage/cli.rb +59 -0
  32. data/lib/bugsage/code_context.rb +81 -0
  33. data/lib/bugsage/code_patch.rb +147 -0
  34. data/lib/bugsage/configuration.rb +149 -0
  35. data/lib/bugsage/console_context.rb +51 -0
  36. data/lib/bugsage/cursor_client.rb +165 -0
  37. data/lib/bugsage/dashboard.rb +627 -0
  38. data/lib/bugsage/editor_links.rb +34 -0
  39. data/lib/bugsage/error_page.rb +298 -0
  40. data/lib/bugsage/exception_handler.rb +66 -0
  41. data/lib/bugsage/exception_support.rb +95 -0
  42. data/lib/bugsage/exceptions_app.rb +31 -0
  43. data/lib/bugsage/fix_applicator.rb +107 -0
  44. data/lib/bugsage/formatter.rb +37 -0
  45. data/lib/bugsage/http_error_capture.rb +104 -0
  46. data/lib/bugsage/http_response_error.rb +14 -0
  47. data/lib/bugsage/inline_console.rb +226 -0
  48. data/lib/bugsage/installation.rb +94 -0
  49. data/lib/bugsage/installer.rb +66 -0
  50. data/lib/bugsage/json_endpoint.rb +34 -0
  51. data/lib/bugsage/locales/en.yml +439 -0
  52. data/lib/bugsage/middleware.rb +152 -0
  53. data/lib/bugsage/openai_client.rb +103 -0
  54. data/lib/bugsage/page_actions.rb +255 -0
  55. data/lib/bugsage/railtie.rb +49 -0
  56. data/lib/bugsage/request_context.rb +56 -0
  57. data/lib/bugsage/rule.rb +271 -0
  58. data/lib/bugsage/session_clear.rb +35 -0
  59. data/lib/bugsage/store.rb +60 -0
  60. data/lib/bugsage/suggestion.rb +58 -0
  61. data/lib/bugsage/trace_cleaner.rb +24 -0
  62. data/lib/bugsage/translations.rb +85 -0
  63. data/lib/bugsage/version.rb +5 -0
  64. data/lib/bugsage.rb +65 -0
  65. data/lib/generators/bugsage/install/install_generator.rb +21 -0
  66. data/lib/generators/bugsage/install/templates/bugsage.rb +22 -0
  67. data/scripts/publish-github-release.sh +38 -0
  68. data/sig/bugsage.rbs +4 -0
  69. 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