Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions Scripts/test_rovodev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env bash
# Test the Rovo Dev usage fetcher end-to-end.
#
# Usage:
# ./Scripts/test_rovodev.sh # build + run (full app, adhoc signed)
# ./Scripts/test_rovodev.sh --fetch-only # print raw API response without launching the app
#
# The --fetch-only mode reads ~/.config/acli/global_auth_config.yaml, imports
# browser cookies for your Atlassian site, and prints the usage JSON.

set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

FETCH_ONLY=0
for arg in "$@"; do
case "${arg}" in
--fetch-only|-f) FETCH_ONLY=1 ;;
--help|-h)
echo "Usage: $(basename "$0") [--fetch-only]"
exit 0
;;
esac
done

# ── helpers ──────────────────────────────────────────────────────────────────
log() { printf '==> %s\n' "$*"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

CONFIG_FILE="${HOME}/.config/acli/global_auth_config.yaml"

check_config() {
if [[ ! -f "${CONFIG_FILE}" ]]; then
fail "Atlassian CLI config not found: ${CONFIG_FILE}
Run 'acli' to set up your profile first."
fi
local site cloud_id
site="$(grep 'site:' "${CONFIG_FILE}" | head -1 | awk -F': ' '{print $2}' | tr -d '[:space:]')"
cloud_id="$(grep 'cloud_id:' "${CONFIG_FILE}" | head -1 | awk -F': ' '{print $2}' | tr -d '[:space:]')"
if [[ -z "${site}" || -z "${cloud_id}" ]]; then
fail "Could not parse site/cloud_id from ${CONFIG_FILE}"
fi
echo "${site}:${cloud_id}"
}

fetch_usage() {
local site="$1"
local cloud_id="$2"
local api_url="https://${site}/gateway/api/rovodev/v3/credits/entitlements/entitlement-allowance"

log "Fetching Rovo Dev usage from ${api_url}"

# Pull browser cookies for the Atlassian domain.
# The easiest cross-browser method on macOS is to read Chrome's cookie DB directly,
# but that requires unlocking Keychain. We use a simpler curl with --cookie-jar approach
# by leveraging the fact that 'cookies' in Safari are accessible without decryption.
#
# Preferred: use the app's built-in debug probe. We trigger that below.
# Fallback shown here calls the API directly with cookies from the system.

log "NOTE: For full cookie import use the app's Debug pane → Rovo Dev → 'Run Probe'."
log "Calling API directly (may 401 without a valid browser session cookie)..."

local body
body="$(printf '{"cloudId":"%s","entitlementId":"unknown","productKey":"unknown"}' "${cloud_id}")"

local response
if ! response="$(curl -sf \
--max-time 15 \
-X POST "${api_url}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Origin: https://${site}" \
-H "Referer: https://${site}/rovodev/your-usage" \
-b "${HOME}/Library/Cookies/Cookies.binarycookies" \
--data "${body}" 2>&1)"; then
log "WARN: curl failed (likely no cookies). Trying without cookie file..."
response="$(curl -sf \
--max-time 15 \
-X POST "${api_url}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Origin: https://${site}" \
-H "Referer: https://${site}/rovodev/your-usage" \
--data "${body}" 2>&1 || echo '{"error":"request failed"}')"
fi

echo ""
log "Raw API response:"
echo "${response}" | python3 -m json.tool 2>/dev/null || echo "${response}"

# Parse key fields
local current_usage credit_cap
current_usage="$(echo "${response}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('currentUsage','?'))" 2>/dev/null || echo "?")"
credit_cap="$(echo "${response}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('creditCap','?'))" 2>/dev/null || echo "?")"

echo ""
log "Summary:"
echo " Current usage : ${current_usage}"
echo " Credit cap : ${credit_cap}"
if [[ "${current_usage}" != "?" && "${credit_cap}" != "?" && "${credit_cap}" != "0" ]]; then
local pct
pct="$(python3 -c "print(f'{${current_usage}/${credit_cap}*100:.1f}%')" 2>/dev/null || echo "?")"
echo " Used : ${pct}"
fi
}

# ── main ─────────────────────────────────────────────────────────────────────
log "Rovo Dev Provider Test"
echo ""

log "Checking ACLI config at ${CONFIG_FILE}..."
IFS=':' read -r site cloud_id <<< "$(check_config)"
log "Site : ${site}"
log "Cloud ID : ${cloud_id}"
echo ""

if [[ "${FETCH_ONLY}" == "1" ]]; then
fetch_usage "${site}" "${cloud_id}"
echo ""
log "Done. To test with full browser cookie import, run the app and open:"
echo " Preferences → Providers → Rovo Dev → Enable"
echo " Debug pane → Probe Logs → Rovo Dev"
exit 0
fi

# Full build + launch
log "Building and launching CodexBar with Rovo Dev provider..."
echo " The app will appear in the menu bar."
echo " Go to Preferences → Providers → Rovo Dev to enable it."
echo ""

exec "${ROOT_DIR}/Scripts/compile_and_run.sh" "$@"
9 changes: 8 additions & 1 deletion Sources/CodexBar/MenuHighlightStyle.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import SwiftUI

extension EnvironmentValues {
@Entry var menuItemHighlighted: Bool = false
// Manual expansion of @Entry (SwiftUIMacros plugin not available in CLI builds)
private struct __Key_menuItemHighlighted: EnvironmentKey {
static let defaultValue: Bool = false
}
var menuItemHighlighted: Bool {
get { self[__Key_menuItemHighlighted.self] }
set { self[__Key_menuItemHighlighted.self] = newValue }
}
}

enum MenuHighlightStyle {
Expand Down
102 changes: 102 additions & 0 deletions Sources/CodexBar/Providers/RovoDev/RovoDevProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct RovoDevProviderImplementation: ProviderImplementation {
let id: UsageProvider = .rovodev

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.rovodevCookieSource
_ = settings.rovodevCookieHeader
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.rovodev(context.settings.rovodevSettingsSnapshot(tokenOverride: context.tokenOverride))
}

@MainActor
func tokenAccountsVisibility(context: ProviderSettingsContext, support: TokenAccountSupport) -> Bool {
guard support.requiresManualCookieSource else { return true }
if !context.settings.tokenAccounts(for: context.provider).isEmpty { return true }
return context.settings.rovodevCookieSource == .manual
}

@MainActor
func applyTokenAccountCookieSource(settings: SettingsStore) {
if settings.rovodevCookieSource != .manual {
settings.rovodevCookieSource = .manual
}
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.rovodevCookieSource.rawValue },
set: { raw in
context.settings.rovodevCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: false,
keychainDisabled: context.settings.debugDisableKeychainAccess)

let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.rovodevCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports cookies from your browser.",
manual: "Paste a Cookie header captured from your Atlassian browser session.",
off: "Rovo Dev cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "rovodev-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports cookies from your browser.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: nil,
onChange: nil),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
let siteURL: String = {
if let config = try? RovoDevACLIConfig.load() {
return "https://\(config.site)/rovodev/your-usage"
}
return "https://atlassian.net/rovodev/your-usage"
}()

return [
ProviderSettingsFieldDescriptor(
id: "rovodev-cookie",
title: "",
subtitle: "",
kind: .secure,
placeholder: "Cookie: …",
binding: context.stringBinding(\.rovodevCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "rovodev-open-usage",
title: "Open Rovo Dev Usage",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: siteURL) {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: { context.settings.rovodevCookieSource == .manual },
onActivate: { context.settings.ensureRovoDevCookieLoaded() }),
]
}
}
63 changes: 63 additions & 0 deletions Sources/CodexBar/Providers/RovoDev/RovoDevSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var rovodevCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .rovodev)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .rovodev) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .rovodev, field: "cookieHeader", value: newValue)
}
}

var rovodevCookieSource: ProviderCookieSource {
get { self.resolvedCookieSource(provider: .rovodev, fallback: .auto) }
set {
self.updateProviderConfig(provider: .rovodev) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(provider: .rovodev, field: "cookieSource", value: newValue.rawValue)
}
}

func ensureRovoDevCookieLoaded() {}
}

extension SettingsStore {
func rovodevSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot
.RovoDevProviderSettings {
ProviderSettingsSnapshot.RovoDevProviderSettings(
cookieSource: self.rovodevSnapshotCookieSource(tokenOverride: tokenOverride),
manualCookieHeader: self.rovodevSnapshotCookieHeader(tokenOverride: tokenOverride))
}

private func rovodevSnapshotCookieHeader(tokenOverride: TokenAccountOverride?) -> String? {
let fallback = self.rovodevCookieHeader.isEmpty ? nil : self.rovodevCookieHeader
guard let support = TokenAccountSupportCatalog.support(for: .rovodev),
case .cookieHeader = support.injection
else {
return fallback
}
guard let account = ProviderTokenAccountSelection.selectedAccount(
provider: .rovodev,
settings: self,
override: tokenOverride)
else {
return fallback
}
return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
}

private func rovodevSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
let fallback = self.rovodevCookieSource
guard let support = TokenAccountSupportCatalog.support(for: .rovodev),
support.requiresManualCookieSource
else {
return fallback
}
if self.tokenAccounts(for: .rovodev).isEmpty { return fallback }
return .manual
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum ProviderImplementationRegistry {
case .kimik2: KimiK2ProviderImplementation()
case .amp: AmpProviderImplementation()
case .ollama: OllamaProviderImplementation()
case .rovodev: RovoDevProviderImplementation()
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
Expand Down
5 changes: 5 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-rovodev.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,8 @@ extension UsageStore {
let hasAny = resolution != nil
let source = resolution?.source.rawValue ?? "none"
return "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .rovodev:
return await Self.debugRovoDevLog(browserDetection: browserDetection)
case .gemini, .antigravity, .opencode, .opencodego, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains, .perplexity:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
Expand Down Expand Up @@ -1034,6 +1036,13 @@ extension UsageStore {
}
}

private static func debugRovoDevLog(browserDetection: BrowserDetection) async -> String {
await runWithTimeout(seconds: 15) {
let fetcher = RovoDevUsageFetcher(browserDetection: browserDetection)
return await fetcher.debugRawProbe()
}
}

private func detectVersions() {
let implementations = ProviderCatalog.all
let browserDetection = self.browserDetection
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ struct TokenAccountCLIContext {
ollama: ProviderSettingsSnapshot.OllamaProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .rovodev:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
return self.makeSnapshot(
rovodev: ProviderSettingsSnapshot.RovoDevProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .kimi:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
Expand Down Expand Up @@ -206,6 +213,7 @@ struct TokenAccountCLIContext {
augment: ProviderSettingsSnapshot.AugmentProviderSettings? = nil,
amp: ProviderSettingsSnapshot.AmpProviderSettings? = nil,
ollama: ProviderSettingsSnapshot.OllamaProviderSettings? = nil,
rovodev: ProviderSettingsSnapshot.RovoDevProviderSettings? = nil,
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil,
perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings? = nil) -> ProviderSettingsSnapshot
{
Expand All @@ -224,6 +232,7 @@ struct TokenAccountCLIContext {
augment: augment,
amp: amp,
ollama: ollama,
rovodev: rovodev,
jetbrains: jetbrains,
perplexity: perplexity)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public enum LogCategories {
public static let openAIWeb = "openai-web"
public static let openAIWebview = "openai-webview"
public static let ollama = "ollama"
public static let rovodev = "rovodev"
public static let opencodeUsage = "opencode-usage"
public static let opencodeGoUsage = "opencode-go-usage"
public static let openRouterUsage = "openrouter-usage"
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/ProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public enum ProviderDescriptorRegistry {
.kimik2: KimiK2ProviderDescriptor.descriptor,
.amp: AmpProviderDescriptor.descriptor,
.ollama: OllamaProviderDescriptor.descriptor,
.rovodev: RovoDevProviderDescriptor.descriptor,
.synthetic: SyntheticProviderDescriptor.descriptor,
.openrouter: OpenRouterProviderDescriptor.descriptor,
.warp: WarpProviderDescriptor.descriptor,
Expand Down
Loading