class Cursorusage
Constants
- COMPACT_TRIGGER_PATTERN
- COOKIE_NAME
- CURSOR_ORIGIN
- DASHBOARD_USAGE_URL
- DEFAULT_CACHE_TTL_SEC
- MISSING_TOKEN_MESSAGE
- RECENT_EVENTS_COUNT
- SESSION_TOKEN_ENV
- STYLE_BANNER
- TOKEN_FILE
- USAGE_CACHE
- USAGE_CACHE_MUTEX
- USAGE_EVENTS_URL
- USAGE_SUMMARY_URL
Public Class Methods
Source
# File plugins/cursorusage.rb, line 83 def self.auth_error_message(error) case error.to_s when "not_authenticated" "Cursor session not authenticated โ refresh your #{COOKIE_NAME} token" else "Cursor usage error: #{error}" end end
Source
# File plugins/cursorusage.rb, line 202 def self.billing_cycle_ms_range(billing_cycle_start, billing_cycle_end) { "startDate" => iso_to_ms(billing_cycle_start), "endDate" => iso_to_ms(billing_cycle_end) } end
Source
# File plugins/cursorusage.rb, line 195 def self.build_post_request_headers(token) build_request_headers(token).merge( "Origin" => CURSOR_ORIGIN, "Content-Type" => "application/json" ) end
Source
# File plugins/cursorusage.rb, line 191 def self.build_request_headers(token) { "Cookie" => "#{COOKIE_NAME}=#{token}" } end
Source
# File plugins/cursorusage.rb, line 122 def self.cached_value(cache_key, ttl:, now: Time.now) USAGE_CACHE_MUTEX.synchronize do entry = USAGE_CACHE[cache_key] return entry[:value] if entry && entry[:expires_at] > now end value = yield USAGE_CACHE_MUTEX.synchronize do USAGE_CACHE[cache_key] = { expires_at: now + ttl, value: value } end value end
Source
# File plugins/cursorusage.rb, line 118 def self.clear_cache! USAGE_CACHE_MUTEX.synchronize { USAGE_CACHE.clear } end
Source
# File plugins/cursorusage.rb, line 36 def self.command_pattern /cursorusage$/i end
Source
# File plugins/cursorusage.rb, line 96 def self.compact_trigger_message?(message) message.to_s.lstrip.match?(COMPACT_TRIGGER_PATTERN) end
Source
# File plugins/cursorusage.rb, line 100 def self.compact_usage_header IrcFormat.report_header(tag: "CURSOR", title: "usage", style: STYLE_BANNER) end
Source
# File plugins/cursorusage.rb, line 294 def self.compact_usage_report(token:, fetch: method(:default_fetch), now: Time.now, cache_ttl: DEFAULT_CACHE_TTL_SEC, use_cache: true) token = token.to_s.strip return MISSING_TOKEN_MESSAGE if token.empty? response = fetch_usage_summary(token: token, fetch: fetch, cache_ttl: cache_ttl, now: now, use_cache: use_cache) return auth_error_message("not_authenticated") if response[:status] == 401 summary = parse_usage_summary(response[:body]) format_compact_usage(summary) rescue StandardError "Unable to fetch Cursor usage." end
Source
# File plugins/cursorusage.rb, line 42 def self.days_remaining(cycle_end, now: Time.now) return 0 if cycle_end.to_s.strip.empty? end_time = Time.parse(cycle_end) days = (end_time.utc.to_date - now.utc.to_date).to_i [days, 0].max rescue ArgumentError, TypeError 0 end
Source
# File plugins/cursorusage.rb, line 262 def self.default_fetch(url, headers) uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" request = Net::HTTP::Get.new(uri) headers.each { |key, value| request[key] = value } response = http.request(request) { status: response.code.to_i, body: response.body.to_s } end
Source
# File plugins/cursorusage.rb, line 272 def self.default_post_fetch(url, headers, body) uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == "https" request = Net::HTTP::Post.new(uri) headers.each { |key, value| request[key] = value } request.body = JSON.generate(body) response = http.request(request) { status: response.code.to_i, body: response.body.to_s } end
Source
# File plugins/cursorusage.rb, line 145 def self.fetch_cached_recent_events(token:, billing_cycle_start:, billing_cycle_end:, post_fetch:, cache_ttl: DEFAULT_CACHE_TTL_SEC, now: Time.now, use_cache: true) cache_key = "events:#{token}:#{billing_cycle_start}:#{billing_cycle_end}" request = lambda do fetch_recent_events( token: token, billing_cycle_start: billing_cycle_start, billing_cycle_end: billing_cycle_end, post_fetch: post_fetch ) end if use_cache cached_value(cache_key, ttl: cache_ttl, now: now, &request) else request.call end end
Source
# File plugins/cursorusage.rb, line 283 def self.fetch_recent_events(token:, billing_cycle_start:, billing_cycle_end:, post_fetch: method(:default_post_fetch)) body = usage_events_request_body( billing_cycle_start: billing_cycle_start, billing_cycle_end: billing_cycle_end ) response = post_fetch.call(USAGE_EVENTS_URL, build_post_request_headers(token), body) return { success: false, error: auth_error_message("not_authenticated") } if response[:status] == 401 parse_usage_events(response[:body]) end
Source
# File plugins/cursorusage.rb, line 135 def self.fetch_usage_summary(token:, fetch:, cache_ttl: DEFAULT_CACHE_TTL_SEC, now: Time.now, use_cache: true) request = lambda { fetch.call(USAGE_SUMMARY_URL, build_request_headers(token)) } if use_cache cached_value("summary:#{token}", ttl: cache_ttl, now: now, &request) else request.call end end
Source
# File plugins/cursorusage.rb, line 104 def self.format_compact_percentages(summary) "auto #{summary[:auto_percent_used]}% ยท api #{summary[:api_percent_used]}% ยท total #{summary[:total_percent_used]}%" end
Source
# File plugins/cursorusage.rb, line 108 def self.format_compact_usage(summary) return summary[:error].to_s unless summary[:success] [ compact_usage_header, format_compact_percentages(summary), IrcFormat.report_footer("cursor.com") ].join("\n") end
Source
# File plugins/cursorusage.rb, line 92 def self.format_on_demand_dollars(cents) format("$%.2f", cents.to_i / 100.0) end
Source
# File plugins/cursorusage.rb, line 253 def self.format_recent_events(events_data) return "" unless events_data[:success] return "" if events_data[:events].empty? lines = ["Recent events (#{events_data[:total_count]} total):"] events_data[:events].each { |event| lines << format_usage_event(event) } lines.join("\n") end
Source
# File plugins/cursorusage.rb, line 164 def self.format_usage(summary, now: Time.now) return summary[:error].to_s unless summary[:success] days = days_remaining(summary[:billing_cycle_end], now: now) end_date = summary[:billing_cycle_end].to_s[0, 10] membership = summary[:membership_type].empty? ? "Cursor" : "Cursor #{summary[:membership_type]}" lines = [IrcFormat.report_header(tag: "CURSOR", title: membership, style: STYLE_BANNER)] lines << "#{days}d left in cycle (ends #{end_date})" lines << "Plan: #{summary[:plan_used]}/#{summary[:plan_limit]} โ " \ "total #{summary[:total_percent_used]}%, " \ "API #{summary[:api_percent_used]}%, " \ "auto #{summary[:auto_percent_used]}%" if summary[:plan_included] && summary[:plan_bonus] && summary[:plan_total] lines << "Allowance: #{summary[:plan_included]} included + " \ "#{summary[:plan_bonus]} bonus = #{summary[:plan_total]} total" end if summary[:on_demand_enabled] lines << "On-demand: #{format_on_demand_dollars(summary[:on_demand_used])} used" end lines << IrcFormat.report_footer("cursor.com") lines.join("\n") end
Source
# File plugins/cursorusage.rb, line 249 def self.format_usage_event(event) "#{event[:model]}: #{event[:usage_based_costs]} (#{event[:output_tokens]} output tokens)" end
Source
# File plugins/cursorusage.rb, line 209 def self.iso_to_ms(iso_timestamp) (Time.parse(iso_timestamp).utc.to_f * 1000).to_i.to_s rescue ArgumentError, TypeError "" end
Source
# File plugins/cursorusage.rb, line 224 def self.parse_usage_events(data) payload = data.is_a?(String) ? JSON.parse(data) : data if payload["error"] return { success: false, error: auth_error_message(payload["error"]) } end events = Array(payload["usageEventsDisplay"]).map do |event| token_usage = event["tokenUsage"] || {} { model: event["model"].to_s, usage_based_costs: event["usageBasedCosts"].to_s, output_tokens: token_usage["outputTokens"].to_i } end { success: true, total_count: payload["totalUsageEventsCount"].to_i, events: events } rescue JSON::ParserError { success: false, error: "Invalid Cursor usage events response" } end
Source
# File plugins/cursorusage.rb, line 52 def self.parse_usage_summary(data) payload = data.is_a?(String) ? JSON.parse(data) : data if payload["error"] return { success: false, error: auth_error_message(payload["error"]) } end plan = payload.dig("individualUsage", "plan") || {} breakdown = plan["breakdown"] || {} on_demand = payload.dig("individualUsage", "onDemand") || {} { success: true, membership_type: payload["membershipType"].to_s, billing_cycle_start: payload["billingCycleStart"].to_s, billing_cycle_end: payload["billingCycleEnd"].to_s, plan_used: plan["used"].to_i, plan_limit: plan["limit"].to_i, plan_included: breakdown["included"], plan_bonus: breakdown["bonus"], plan_total: breakdown["total"], total_percent_used: plan["totalPercentUsed"].to_i, api_percent_used: plan["apiPercentUsed"].to_i, auto_percent_used: plan["autoPercentUsed"].to_i, on_demand_enabled: !!on_demand["enabled"], on_demand_used: on_demand["used"].to_i } rescue JSON::ParserError { success: false, error: "Invalid Cursor usage response" } end
Source
# File plugins/cursorusage.rb, line 348 def self.session_token(token_file: TOKEN_FILE) token = session_token_from_file(path: token_file) token = ENV[SESSION_TOKEN_ENV].to_s.strip if token.empty? token end
Source
# File plugins/cursorusage.rb, line 340 def self.session_token_from_file(path: TOKEN_FILE) return "" unless File.file?(path) File.read(path).to_s.strip rescue StandardError "" end
Source
# File plugins/cursorusage.rb, line 215 def self.usage_events_request_body(billing_cycle_start:, billing_cycle_end:, page: 1, page_size: RECENT_EVENTS_COUNT) body = { "page" => page, "pageSize" => page_size } range = billing_cycle_ms_range(billing_cycle_start, billing_cycle_end) if range["startDate"] && !range["startDate"].empty? && range["endDate"] && !range["endDate"].empty? body = body.merge(range) end body end
Source
# File plugins/cursorusage.rb, line 308 def self.usage_report(token:, fetch: method(:default_fetch), post_fetch: method(:default_post_fetch), now: Time.now, cache_ttl: DEFAULT_CACHE_TTL_SEC, use_cache: true) token = token.to_s.strip return MISSING_TOKEN_MESSAGE if token.empty? response = fetch_usage_summary(token: token, fetch: fetch, cache_ttl: cache_ttl, now: now, use_cache: use_cache) return auth_error_message("not_authenticated") if response[:status] == 401 summary = parse_usage_summary(response[:body]) return summary[:error].to_s unless summary[:success] lines = [format_usage(summary, now: now)] events = begin fetch_cached_recent_events( token: token, billing_cycle_start: summary[:billing_cycle_start], billing_cycle_end: summary[:billing_cycle_end], post_fetch: post_fetch, cache_ttl: cache_ttl, now: now, use_cache: use_cache ) rescue StandardError { success: false } end recent = format_recent_events(events) lines << recent unless recent.empty? lines.join("\n") rescue StandardError "Unable to fetch Cursor usage." end
Public Instance Methods
Source
# File plugins/cursorusage.rb, line 367 def compact_usage_report(token: self.class.session_token, fetch: self.class.method(:default_fetch), now: Time.now, cache_ttl: self.class::DEFAULT_CACHE_TTL_SEC, use_cache: true) self.class.compact_usage_report( token: token, fetch: fetch, now: now, cache_ttl: cache_ttl, use_cache: use_cache ) end
Source
# File plugins/cursorusage.rb, line 384 def execute(m) themed_flood_safe_reply(m, usage_report) end
Source
# File plugins/cursorusage.rb, line 378 def maybe_reply_compact_usage(m) return unless self.class.compact_trigger_message?(m.message) themed_flood_safe_reply(m, compact_usage_report) end
Source
# File plugins/cursorusage.rb, line 354 def usage_report(token: self.class.session_token, fetch: self.class.method(:default_fetch), post_fetch: self.class.method(:default_post_fetch), now: Time.now, cache_ttl: self.class::DEFAULT_CACHE_TTL_SEC, use_cache: true) self.class.usage_report( token: token, fetch: fetch, post_fetch: post_fetch, now: now, cache_ttl: cache_ttl, use_cache: use_cache ) end