From bc9655c890f02b0abf2136af77ae7fd649985895 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Thu, 15 Oct 2026 17:53:52 +0700 Subject: [PATCH 1/3] Use the shared browser order for Raycast cookie import --- CHANGELOG.md | 1 + rust/src/locale/en-US.ftl | 2 +- rust/src/locale/es-MX.ftl | 2 +- rust/src/locale/ja-JP.ftl | 2 +- rust/src/locale/ko-KR.ftl | 2 +- rust/src/locale/pt-BR.ftl | 2 +- rust/src/locale/ru-RU.ftl | 2 +- rust/src/locale/tr-TR.ftl | 2 +- rust/src/locale/uk-UA.ftl | 2 +- rust/src/locale/zh-CN.ftl | 2 +- rust/src/locale/zh-TW.ftl | 2 +- rust/src/providers/raycast/cookies.rs | 12 ++ rust/src/providers/raycast/mod.rs | 54 ++++++-- rust/src/providers/raycast/tests.rs | 187 ++++++++++++++++++++++++++ 14 files changed, 252 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd586df4bcc..709bdf5e856 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ## [Windows] Unreleased +- Raycast: automatic cookie import walks every supported browser in the shared order and uses the first accepted session, instead of reading Chrome alone (upstream #4443, 0aef66c73). - Ollama: cookie mode reads the current **Usage credits** wallet on the settings page and shows its credit balance, as API-key mode already does, without inventing a quota percentage or changing older usage meters (upstream #4370). - Session depleted and restored alerts start fresh after you switch accounts: the first reading for the new account sets its baseline without an alert, and switching back does not announce a restore that happened while another account was active (upstream #4138). - Show a Windows path (`~\.local\bin\cswap.exe`) as the placeholder for the Claude account switcher (cswap) executable in Settings instead of a Unix path (upstream v0.74.0). diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index 1208b0b9b9e..c1e8886d455 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -1135,7 +1135,7 @@ UsageSpendModelsPeriodCaption = Local model history for: { "{}" }. UsageSpendProjectsPeriodCaption = Ranked Codex local project spend for: { "{}" } BurndownChartTitle = Usage remaining BurndownChartAriaLabel = Recorded remaining quota burndown -ProviderRaycastAutoImportHelp = Automatically imports the signed-in www.raycast.com session from Chrome. +ProviderRaycastAutoImportHelp = Automatically imports the signed-in www.raycast.com session from your installed browsers. ProviderRaycastManualCookieHelp = Paste a Cookie header from www.raycast.com/settings. ProviderRaycastCookiesDisabled = Raycast cookies are disabled. CookieHelpCodexManual = Paste a Cookie header from a chatgpt.com request. diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index 38ee356ccc0..bae82eec619 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -1132,7 +1132,7 @@ UsageSpendModelsPeriodCaption = Historial local de modelos para: { "{}" }. UsageSpendProjectsPeriodCaption = Gasto local de proyectos de Codex, ordenado, para: { "{}" } BurndownChartTitle = Uso restante BurndownChartAriaLabel = Consumo registrado de la cuota restante -ProviderRaycastAutoImportHelp = Importa automáticamente desde Chrome la sesión iniciada en www.raycast.com. +ProviderRaycastAutoImportHelp = Importa automáticamente la sesión iniciada en www.raycast.com desde los navegadores instalados. ProviderRaycastManualCookieHelp = Pega un encabezado Cookie de www.raycast.com/settings. ProviderRaycastCookiesDisabled = Las cookies de Raycast están desactivadas. CookieHelpCodexManual = Pega un encabezado Cookie de una solicitud a chatgpt.com. diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index c72697624d7..d22791f1f7c 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = ローカルのモデル履歴の期間: { "{}" UsageSpendProjectsPeriodCaption = Codex ローカルプロジェクトのコストランキングの期間: { "{}" } BurndownChartTitle = 残り使用量 BurndownChartAriaLabel = 記録された残りクォータの推移 -ProviderRaycastAutoImportHelp = Chrome からサインイン済みの www.raycast.com セッションを自動でインポートします。 +ProviderRaycastAutoImportHelp = インストール済みのブラウザーからサインイン済みの www.raycast.com セッションを自動でインポートします。 ProviderRaycastManualCookieHelp = www.raycast.com/settings の Cookie ヘッダーを貼り付けます。 ProviderRaycastCookiesDisabled = Raycast の Cookie は無効です。 CookieHelpCodexManual = chatgpt.com へのリクエストの Cookie ヘッダーを貼り付けます。 diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 763e5488cea..77ffaa9cc67 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -1132,7 +1132,7 @@ UsageSpendModelsPeriodCaption = 로컬 모델 기록 기간: { "{}" }. UsageSpendProjectsPeriodCaption = Codex 로컬 프로젝트 비용 순위 기간: { "{}" } BurndownChartTitle = 남은 사용량 BurndownChartAriaLabel = 기록된 남은 할당량 소진 추이 -ProviderRaycastAutoImportHelp = Chrome에서 로그인된 www.raycast.com 세션을 자동으로 가져옵니다. +ProviderRaycastAutoImportHelp = 설치된 브라우저에서 로그인된 www.raycast.com 세션을 자동으로 가져옵니다. ProviderRaycastManualCookieHelp = www.raycast.com/settings의 Cookie 헤더를 붙여넣습니다. ProviderRaycastCookiesDisabled = Raycast 쿠키가 비활성화되었습니다. CookieHelpCodexManual = chatgpt.com 요청의 Cookie 헤더를 붙여넣습니다. diff --git a/rust/src/locale/pt-BR.ftl b/rust/src/locale/pt-BR.ftl index 1487ce7f165..fe008149568 100644 --- a/rust/src/locale/pt-BR.ftl +++ b/rust/src/locale/pt-BR.ftl @@ -1137,7 +1137,7 @@ ProviderLiteLLMModelActivity = Mostrar atividade por modelo ProviderLiteLLMModelActivityHelper = Adiciona tokens e solicitações por modelo do seu usuário nos últimos 30 dias UTC (top 20). Faz até 3 solicitações extras ao seu proxy LiteLLM por atualização; se ele estiver indisponível, a seção fica oculta e os orçamentos não são afetados. Desativado por padrão. ProviderClaudeWorkspaceSpend = Mostrar gasto por workspace ProviderClaudeWorkspaceSpendHelper = Adiciona o gasto da Admin API por workspace nos últimos 30 dias (top 20, exibido só quando mais de um workspace tem gasto). Os totais da organização não mudam e nenhuma solicitação extra é feita. Desativado por padrão. -ProviderRaycastAutoImportHelp = Importa automaticamente a sessão conectada de www.raycast.com do Chrome. +ProviderRaycastAutoImportHelp = Importa automaticamente a sessão conectada de www.raycast.com dos navegadores instalados. ProviderRaycastManualCookieHelp = Cole um cabeçalho Cookie de www.raycast.com/settings. ProviderRaycastCookiesDisabled = Os cookies do Raycast estão desativados. CookieHelpCodexManual = Cole um cabeçalho Cookie de uma solicitação para chatgpt.com. diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index 47c86f3e430..9ea0ed3af74 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = Локальная история моделей UsageSpendProjectsPeriodCaption = Расходы локальных проектов Codex по убыванию за период: { "{}" } BurndownChartTitle = Остаток лимита BurndownChartAriaLabel = График расхода оставшейся квоты -ProviderRaycastAutoImportHelp = Автоматически импортирует сеанс www.raycast.com, открытый в Chrome. +ProviderRaycastAutoImportHelp = Автоматически импортирует сеанс www.raycast.com, открытый в установленных браузерах. ProviderRaycastManualCookieHelp = Вставьте заголовок Cookie со страницы www.raycast.com/settings. ProviderRaycastCookiesDisabled = Файлы cookie Raycast отключены. CookieHelpCodexManual = Вставьте заголовок Cookie из запроса к chatgpt.com. diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index 3192c5bd346..2a34a0a062e 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -1133,7 +1133,7 @@ DeepSeekPricingEffective = Geçerli olduğu yerel saat: DeepSeekPricingAdvice = Resmî tarife: yoğun saatler UTC 01:00-04:00 ve 06:00-10:00; yoğun olmayan saatlerde yarı fiyat. MistralMonthlySpend = Aylık API harcaması MistralMonthlySpendHelper = Kullandıkça öde Mistral hesapları için geçerli ayın API kullanımı. -ProviderRaycastAutoImportHelp = Chrome'da açık olan www.raycast.com oturumunu otomatik olarak içe aktarır. +ProviderRaycastAutoImportHelp = Yüklü tarayıcılardaki açık www.raycast.com oturumunu otomatik olarak içe aktarır. ProviderRaycastManualCookieHelp = www.raycast.com/settings adresinden bir Cookie başlığı yapıştırın. ProviderRaycastCookiesDisabled = Raycast çerezleri devre dışı. CookieHelpCodexManual = chatgpt.com isteğinden bir Cookie başlığı yapıştırın. diff --git a/rust/src/locale/uk-UA.ftl b/rust/src/locale/uk-UA.ftl index 66f3adabe6f..73b364d4448 100644 --- a/rust/src/locale/uk-UA.ftl +++ b/rust/src/locale/uk-UA.ftl @@ -1131,7 +1131,7 @@ UsageSpendModelsPeriodCaption = Локальна історія моделей UsageSpendProjectsPeriodCaption = Витрати локальних проєктів Codex за спаданням за період: { "{}" } BurndownChartTitle = Залишок ліміту BurndownChartAriaLabel = Графік витрачання залишку квоти -ProviderRaycastAutoImportHelp = Автоматично імпортує сеанс www.raycast.com, у якому виконано вхід у Chrome. +ProviderRaycastAutoImportHelp = Автоматично імпортує сеанс www.raycast.com, у якому виконано вхід, з установлених браузерів. ProviderRaycastManualCookieHelp = Вставте заголовок Cookie зі сторінки www.raycast.com/settings. ProviderRaycastCookiesDisabled = Файли cookie Raycast вимкнено. CookieHelpCodexManual = Вставте заголовок Cookie із запиту до chatgpt.com. diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 15b403581b3..5ec0c1e96b0 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = 本地模型历史:{ "{}" }。 UsageSpendProjectsPeriodCaption = Codex 本地项目费用排名:{ "{}" } BurndownChartTitle = 剩余用量 BurndownChartAriaLabel = 已记录的剩余配额消耗曲线 -ProviderRaycastAutoImportHelp = 自动从 Chrome 导入已登录的 www.raycast.com 会话。 +ProviderRaycastAutoImportHelp = 自动从已安装的浏览器导入已登录的 www.raycast.com 会话。 ProviderRaycastManualCookieHelp = 粘贴来自 www.raycast.com/settings 的 Cookie 标头。 ProviderRaycastCookiesDisabled = Raycast Cookie 已禁用。 CookieHelpCodexManual = 粘贴来自 chatgpt.com 请求的 Cookie 标头。 diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index 4d2c95283ca..e911f0ff1d4 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -1133,7 +1133,7 @@ UsageSpendModelsPeriodCaption = 本機模型歷史:{ "{}" }。 UsageSpendProjectsPeriodCaption = Codex 本機專案費用排名:{ "{}" } BurndownChartTitle = 剩餘用量 BurndownChartAriaLabel = 已記錄的剩餘配額消耗趨勢 -ProviderRaycastAutoImportHelp = 自動從 Chrome 匯入已登入的 www.raycast.com 工作階段。 +ProviderRaycastAutoImportHelp = 自動從已安裝的瀏覽器匯入已登入的 www.raycast.com 工作階段。 ProviderRaycastManualCookieHelp = 貼上來自 www.raycast.com/settings 的 Cookie 標頭。 ProviderRaycastCookiesDisabled = Raycast Cookie 已停用。 CookieHelpCodexManual = 貼上來自 chatgpt.com 請求的 Cookie 標頭。 diff --git a/rust/src/providers/raycast/cookies.rs b/rust/src/providers/raycast/cookies.rs index ecc733efa69..838cbb7c68e 100644 --- a/rust/src/providers/raycast/cookies.rs +++ b/rust/src/providers/raycast/cookies.rs @@ -5,6 +5,7 @@ //! profile is dropped before a request is built. use crate::browser::cookies::Cookie; +use crate::browser::detection::BrowserType; pub(super) const SESSION_COOKIE: &str = "__raycast_session"; const CSRF_COOKIE: &str = "csrf_token"; @@ -66,6 +67,17 @@ pub(super) fn browser_candidates(cookies: &[Cookie]) -> Vec { headers } +/// Candidate headers for each scanned browser, in the scan's browser order and +/// labelled with the browser's display name. Browsers without a usable session +/// keep an empty list so callers can skip them. +pub(super) fn browser_groups( + scan: &[(BrowserType, Vec)], +) -> Vec<(&'static str, Vec)> { + scan.iter() + .map(|(browser, cookies)| (browser.display_name(), browser_candidates(cookies))) + .collect() +} + /// Reduce a pasted header to the two forwarded cookies. Returns `None` without /// a non-empty `__raycast_session`. The first occurrence of a name wins, which /// matches the most specific cookie a browser sends. diff --git a/rust/src/providers/raycast/mod.rs b/rust/src/providers/raycast/mod.rs index 8cd578462a9..bb3200d83b9 100644 --- a/rust/src/providers/raycast/mod.rs +++ b/rust/src/providers/raycast/mod.rs @@ -2,10 +2,11 @@ //! //! Raycast has no public credits API for a website session, so this provider //! calls the unofficial `frontend_api/current_user/ai_credits` route with the -//! `__raycast_session` and `csrf_token` cookies only. Automatic import reads -//! Chrome alone to avoid unrelated browser prompts; a Manual Cookie header is -//! pinned and never falls back to a browser; Off makes no request. Cookie -//! material stays in memory for the current fetch and is never logged. +//! `__raycast_session` and `csrf_token` cookies only. Automatic import walks +//! every detected browser in the shared browser order and stops at the first +//! accepted session; a Manual Cookie header is pinned and never falls back to a +//! browser; Off makes no request. Cookie material stays in memory for the +//! current fetch and is never logged. mod cookies; mod parse; @@ -16,7 +17,6 @@ use async_trait::async_trait; use reqwest::{Client, StatusCode}; use std::time::Duration; -use crate::browser::detection::BrowserType; use crate::core::{ FetchContext, ManualEmptyCookiePolicy, Provider, ProviderError, ProviderFetchResult, ProviderId, ProviderMetadata, ProviderStateKind, SourceMode, @@ -30,7 +30,6 @@ const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleW const MAX_RESPONSE_BYTES: usize = 1024 * 1024; const MIN_TIMEOUT_SECONDS: u64 = 1; const MAX_TIMEOUT_SECONDS: u64 = 30; -const BROWSER: BrowserType = BrowserType::Chrome; const MISSING_SESSION: &str = "No Raycast session cookies found. Sign in at www.raycast.com/settings or paste a Cookie header."; const SESSION_EXPIRED: &str = "Raycast website session expired. Sign in at www.raycast.com/settings or paste a fresh Cookie header."; @@ -141,18 +140,49 @@ impl RaycastProvider { if ctx.manual_cookie_missing { return Err(ProviderError::Other(MISSING_SESSION.to_string())); } - let extracted = tokio::task::spawn_blocking(|| { - crate::providers::browser_cookies_from_browser(BROWSER, cookies::EXTRACT_DOMAIN) + let scanned = tokio::task::spawn_blocking(|| { + crate::providers::browser_cookie_candidates_for_domain(cookies::EXTRACT_DOMAIN) }) .await .map_err(|_| ProviderError::Other("Raycast cookie import was interrupted.".into()))?; - let headers = match extracted { - Ok(found) => cookies::browser_candidates(&found), + let groups = match scanned { + Ok(scan) => cookies::browser_groups(&scan), Err(ProviderError::NoCookies) => Vec::new(), Err(error) => return Err(error), }; - self.fetch_candidates(&headers, BROWSER.display_name(), timeout) - .await + self.fetch_browser_groups(&groups, timeout).await + } + + /// Try each browser's candidate headers in the shared browser order. A + /// browser whose sessions are all rejected yields to the next browser; a + /// browser with no session is skipped without a request; any other outcome + /// (success or a non-authentication failure) ends the search. + async fn fetch_browser_groups( + &self, + groups: &[(&str, Vec)], + timeout: Duration, + ) -> Result { + let mut rejected = false; + for (source_label, headers) in groups { + if headers.is_empty() { + continue; + } + match self.fetch_candidates(headers, source_label, timeout).await { + Ok(result) => return Ok(result), + Err(ProviderError::Other(message)) if message == SESSION_EXPIRED => { + rejected = true; + } + Err(error) => return Err(error), + } + } + Err(ProviderError::Other( + if rejected { + SESSION_EXPIRED + } else { + MISSING_SESSION + } + .to_string(), + )) } } diff --git a/rust/src/providers/raycast/tests.rs b/rust/src/providers/raycast/tests.rs index 618988d3fef..4acf4dcc7e6 100644 --- a/rust/src/providers/raycast/tests.rs +++ b/rust/src/providers/raycast/tests.rs @@ -1,5 +1,6 @@ use super::*; use crate::browser::cookies::Cookie; +use crate::browser::detection::BrowserType; use chrono::TimeZone; use parse::parse_credits; @@ -706,3 +707,189 @@ async fn malformed_success_body_is_a_parse_failure_that_stops_retries() { mock.assert_async().await; assert!(matches!(error, ProviderError::Parse(_))); } + +// ---- browser order ------------------------------------------------------- + +#[test] +fn browser_groups_keep_the_shared_order_and_skip_browsers_without_a_session() { + let scan = vec![ + ( + BrowserType::Chrome, + vec![cookie("csrf_token", "chrome-csrf", "www.raycast.com")], + ), + ( + BrowserType::Edge, + vec![ + cookie("__raycast_session", "edge-session", "www.raycast.com"), + cookie("csrf_token", "edge-csrf", "www.raycast.com"), + ], + ), + ]; + + let groups = cookies::browser_groups(&scan); + + assert_eq!( + groups, + vec![ + ("Google Chrome", Vec::new()), + ( + "Microsoft Edge", + vec!["__raycast_session=edge-session; csrf_token=edge-csrf".to_string()], + ), + ] + ); +} + +#[tokio::test] +async fn browser_groups_move_past_a_rejected_browser_to_the_next_session() { + let mut server = mockito::Server::new_async().await; + let rejected = server + .mock("GET", "/frontend_api/current_user/ai_credits") + .match_header("cookie", "__raycast_session=chrome-rejected") + .with_status(401) + .expect(1) + .create_async() + .await; + let accepted = server + .mock("GET", "/frontend_api/current_user/ai_credits") + .match_header("cookie", FIXTURE_COOKIE) + .with_status(200) + .with_body(CREDITS) + .expect(1) + .create_async() + .await; + + let provider = RaycastProvider::with_origin(&server.url()); + let groups = vec![ + ( + "Google Chrome", + headers(&["__raycast_session=chrome-rejected"]), + ), + ("Microsoft Edge", headers(&[FIXTURE_COOKIE])), + ]; + let result = provider + .fetch_browser_groups(&groups, Duration::from_secs(5)) + .await + .expect("the Edge session should be accepted"); + + rejected.assert_async().await; + accepted.assert_async().await; + assert_eq!(result.source_label, "Microsoft Edge"); + assert_eq!(result.usage.primary.used_percent, 75.0); +} + +#[tokio::test] +async fn browser_groups_stop_at_the_first_accepted_session() { + let mut server = mockito::Server::new_async().await; + let chrome = server + .mock("GET", "/frontend_api/current_user/ai_credits") + .match_header("cookie", FIXTURE_COOKIE) + .with_status(200) + .with_body(CREDITS) + .expect(1) + .create_async() + .await; + let edge = server + .mock("GET", mockito::Matcher::Any) + .expect(0) + .create_async() + .await; + + let provider = RaycastProvider::with_origin(&server.url()); + let groups = vec![ + ("Google Chrome", headers(&[FIXTURE_COOKIE])), + ( + "Microsoft Edge", + headers(&["__raycast_session=edge-fixture-session"]), + ), + ]; + let result = provider + .fetch_browser_groups(&groups, Duration::from_secs(5)) + .await + .expect("Chrome session should be accepted"); + + chrome.assert_async().await; + edge.assert_async().await; + assert_eq!(result.source_label, "Google Chrome"); +} + +#[tokio::test] +async fn browser_groups_without_any_session_make_no_request() { + let mut server = mockito::Server::new_async().await; + let mock = server + .mock("GET", mockito::Matcher::Any) + .expect(0) + .create_async() + .await; + + let provider = RaycastProvider::with_origin(&server.url()); + let groups = vec![ + ("Google Chrome", Vec::new()), + ("Microsoft Edge", Vec::new()), + ]; + let error = provider + .fetch_browser_groups(&groups, Duration::from_secs(5)) + .await + .expect_err("no browser holds a session"); + + mock.assert_async().await; + assert_eq!(error_message(error), MISSING_SESSION); +} + +#[tokio::test] +async fn browser_groups_report_expiry_only_when_every_browser_rejects() { + let mut server = mockito::Server::new_async().await; + let mock = server + .mock("GET", "/frontend_api/current_user/ai_credits") + .with_status(401) + .expect(2) + .create_async() + .await; + + let provider = RaycastProvider::with_origin(&server.url()); + let groups = vec![ + ("Google Chrome", headers(&["__raycast_session=chrome-a"])), + ("Microsoft Edge", headers(&["__raycast_session=edge-b"])), + ]; + let error = provider + .fetch_browser_groups(&groups, Duration::from_secs(5)) + .await + .expect_err("every session rejected"); + + mock.assert_async().await; + assert_eq!(error_message(error), SESSION_EXPIRED); +} + +#[tokio::test] +async fn browser_groups_stop_on_a_service_failure_instead_of_trying_the_next_browser() { + let mut server = mockito::Server::new_async().await; + let unavailable = server + .mock("GET", "/frontend_api/current_user/ai_credits") + .match_header("cookie", "__raycast_session=chrome-session") + .with_status(503) + .expect(1) + .create_async() + .await; + let edge = server + .mock("GET", mockito::Matcher::Any) + .expect(0) + .create_async() + .await; + + let provider = RaycastProvider::with_origin(&server.url()); + let groups = vec![ + ( + "Google Chrome", + headers(&["__raycast_session=chrome-session"]), + ), + ("Microsoft Edge", headers(&[FIXTURE_COOKIE])), + ]; + let error = provider + .fetch_browser_groups(&groups, Duration::from_secs(5)) + .await + .expect_err("service outage"); + + unavailable.assert_async().await; + edge.assert_async().await; + assert!(error_message(error).contains("unavailable")); +} From a509e559b5f3df3f932fe0879df75268d46404d2 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Thu, 15 Oct 2026 17:55:18 +0700 Subject: [PATCH 2/3] Cover Firefox Developer Edition profile detection --- rust/src/browser/detection.rs | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/rust/src/browser/detection.rs b/rust/src/browser/detection.rs index bc932196211..ce4d5259400 100755 --- a/rust/src/browser/detection.rs +++ b/rust/src/browser/detection.rs @@ -365,6 +365,49 @@ mod tests { } } + /// Firefox Developer Edition keeps its profiles under the shared Roaming + /// root with a `.dev-edition-default` name. It must be detected as + /// the default profile, like `.default-release`, and not be dropped. + #[test] + fn firefox_developer_edition_profiles_are_detected_as_default() { + let temp = tempfile::tempdir().unwrap(); + let roaming = temp.path().join("Roaming"); + let profiles = roaming.join("Mozilla/Firefox/Profiles"); + let dev_edition = profiles.join("k7x2m9ab.dev-edition-default"); + std::fs::create_dir_all(&dev_edition).unwrap(); + std::fs::create_dir_all(profiles.join("q3w8e1rt.default-release")).unwrap(); + std::fs::create_dir_all(profiles.join("z9y8x7wv.other")).unwrap(); + + let detected = BrowserDetector::detect_in_roots( + BrowserType::Firefox, + temp.path(), + Some(roaming.as_path()), + ) + .unwrap(); + + assert_eq!(detected.user_data_dir, profiles); + assert_eq!(detected.profiles.len(), 3); + let dev = detected + .profiles + .iter() + .find(|profile| profile.name == "k7x2m9ab.dev-edition-default") + .expect("Developer Edition profile should be detected"); + assert_eq!(dev.path, dev_edition); + assert!(dev.is_default); + assert!( + detected + .profiles + .iter() + .any(|profile| profile.name == "q3w8e1rt.default-release" && profile.is_default) + ); + assert!( + detected + .profiles + .iter() + .any(|profile| profile.name == "z9y8x7wv.other" && !profile.is_default) + ); + } + #[test] fn test_browser_detection() { let browsers = BrowserDetector::detect_all(); From 38028ea117f00b896b228e3680c5851a4cf4cc47 Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Thu, 15 Oct 2026 18:10:56 +0700 Subject: [PATCH 3/3] Document Raycast browser cookie auto-import --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c390bbbf075..5283c490d3e 100755 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ See the full history in [CHANGELOG.md](CHANGELOG.md). | OpenAI | Admin API / API Key | Usage, Requests, Project-scoped cost, Credit Balance | | Grok | Cookies / auth.json | Billing | | Helmcode (also NaN Builders) | Browser cookies / manual Cookie header | Per-model token quotas, reset windows, Helmcode prepaid balance | -| Raycast | Cookies (Chrome auto or manual) | Monthly AI credits, plan, renewal | +| Raycast | Cookies (browser auto or manual) | Monthly AI credits, plan, renewal | | Replicate | Cookies / token accounts | Monthly spend, credit balance | | Aixy | API Key / token accounts | Applicable budget balances, 7-day key usage | | ClawRouter | API Key | Monthly budget, requests, tokens, routed-provider spend |