Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 357 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358import axios from 'axios'import memoize from 'lodash-es/memoize.js'import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent,} from 'src/services/analytics/index.js'import { getOauthAccountInfo, isConsumerSubscriber } from 'src/utils/auth.js'import { logForDebugging } from 'src/utils/debug.js'import { gracefulShutdown } from 'src/utils/gracefulShutdown.js'import { isEssentialTrafficOnly } from 'src/utils/privacyLevel.js'import { writeToStderr } from 'src/utils/process.js'import { getOauthConfig } from '../../constants/oauth.js'import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'import { getAuthHeaders, getUserAgent, withOAuth401Retry,} from '../../utils/http.js'import { logError } from '../../utils/log.js'import { getClaudeCodeUserAgent } from '../../utils/userAgent.js'
// Cache expiration: 24 hoursconst GROVE_CACHE_EXPIRATION_MS = 24 * 60 * 60 * 1000
export type AccountSettings = { grove_enabled: boolean | null grove_notice_viewed_at: string | null}
export type GroveConfig = { grove_enabled: boolean domain_excluded: boolean notice_is_grace_period: boolean notice_reminder_frequency: number | null}
/** * Result type that distinguishes between API failure and success. * - success: true means API call succeeded (data may still contain null fields) * - success: false means API call failed after retry */export type ApiResult<T> = { success: true; data: T } | { success: false }
/** * Get the current Grove settings for the user account. * Returns ApiResult to distinguish between API failure and success. * Uses existing OAuth 401 retry, then returns failure if that doesn't help. * * Memoized for the session to avoid redundant per-render requests. * Cache is invalidated in updateGroveSettings() so post-toggle reads are fresh. */export const getGroveSettings = memoize( async (): Promise<ApiResult<AccountSettings>> => { // Grove is a notification feature; during an outage, skipping it is correct. if (isEssentialTrafficOnly()) { return { success: false } } try { const response = await withOAuth401Retry(() => { const authHeaders = getAuthHeaders() if (authHeaders.error) { throw new Error(`Failed to get auth headers: ${authHeaders.error}`) } return axios.get<AccountSettings>( `${getOauthConfig().BASE_API_URL}/api/oauth/account/settings`, { headers: { ...authHeaders.headers, 'User-Agent': getClaudeCodeUserAgent(), }, }, ) }) return { success: true, data: response.data } } catch (err) { logError(err) // Don't cache failures — transient network issues would lock the user // out of privacy settings for the entire session (deadlock: dialog needs // success to render the toggle, toggle calls updateGroveSettings which // is the only other place the cache is cleared). getGroveSettings.cache.clear?.() return { success: false } } },)
/** * Mark that the Grove notice has been viewed by the user */export async function markGroveNoticeViewed(): Promise<void> { try { await withOAuth401Retry(() => { const authHeaders = getAuthHeaders() if (authHeaders.error) { throw new Error(`Failed to get auth headers: ${authHeaders.error}`) } return axios.post( `${getOauthConfig().BASE_API_URL}/api/oauth/account/grove_notice_viewed`, {}, { headers: { ...authHeaders.headers, 'User-Agent': getClaudeCodeUserAgent(), }, }, ) }) // This mutates grove_notice_viewed_at server-side — Grove.tsx:87 reads it // to decide whether to show the dialog. Without invalidation a same-session // remount would read stale viewed_at:null and re-show the dialog. getGroveSettings.cache.clear?.() } catch (err) { logError(err) }}
/** * Update Grove settings for the user account */export async function updateGroveSettings( groveEnabled: boolean,): Promise<void> { try { await withOAuth401Retry(() => { const authHeaders = getAuthHeaders() if (authHeaders.error) { throw new Error(`Failed to get auth headers: ${authHeaders.error}`) } return axios.patch( `${getOauthConfig().BASE_API_URL}/api/oauth/account/settings`, { grove_enabled: groveEnabled, }, { headers: { ...authHeaders.headers, 'User-Agent': getClaudeCodeUserAgent(), }, }, ) }) // Invalidate memoized settings so the post-toggle confirmation // read in privacy-settings.tsx picks up the new value. getGroveSettings.cache.clear?.() } catch (err) { logError(err) }}
/** * Check if user is qualified for Grove (non-blocking, cache-first). * * This function never blocks on network - it returns cached data immediately * and fetches in the background if needed. On cold start (no cache), it returns * false and the Grove dialog won't show until the next session. */export async function isQualifiedForGrove(): Promise<boolean> { if (!isConsumerSubscriber()) { return false }
const accountId = getOauthAccountInfo()?.accountUuid if (!accountId) { return false }
const globalConfig = getGlobalConfig() const cachedEntry = globalConfig.groveConfigCache?.[accountId] const now = Date.now()
// No cache - trigger background fetch and return false (non-blocking) // The Grove dialog won't show this session, but will next time if eligible if (!cachedEntry) { logForDebugging( 'Grove: No cache, fetching config in background (dialog skipped this session)', ) void fetchAndStoreGroveConfig(accountId) return false }
// Cache exists but is stale - return cached value and refresh in background if (now - cachedEntry.timestamp > GROVE_CACHE_EXPIRATION_MS) { logForDebugging( 'Grove: Cache stale, returning cached data and refreshing in background', ) void fetchAndStoreGroveConfig(accountId) return cachedEntry.grove_enabled }
// Cache is fresh - return it immediately logForDebugging('Grove: Using fresh cached config') return cachedEntry.grove_enabled}
/** * Fetch Grove config from API and store in cache */async function fetchAndStoreGroveConfig(accountId: string): Promise<void> { try { const result = await getGroveNoticeConfig() if (!result.success) { return } const groveEnabled = result.data.grove_enabled const cachedEntry = getGlobalConfig().groveConfigCache?.[accountId] if ( cachedEntry?.grove_enabled === groveEnabled && Date.now() - cachedEntry.timestamp <= GROVE_CACHE_EXPIRATION_MS ) { return } saveGlobalConfig(current => ({ ...current, groveConfigCache: { ...current.groveConfigCache, [accountId]: { grove_enabled: groveEnabled, timestamp: Date.now(), }, }, })) } catch (err) { logForDebugging(`Grove: Failed to fetch and store config: ${err}`) }}
/** * Get Grove Statsig configuration from the API. * Returns ApiResult to distinguish between API failure and success. * Uses existing OAuth 401 retry, then returns failure if that doesn't help. */export const getGroveNoticeConfig = memoize( async (): Promise<ApiResult<GroveConfig>> => { // Grove is a notification feature; during an outage, skipping it is correct. if (isEssentialTrafficOnly()) { return { success: false } } try { const response = await withOAuth401Retry(() => { const authHeaders = getAuthHeaders() if (authHeaders.error) { throw new Error(`Failed to get auth headers: ${authHeaders.error}`) } return axios.get<GroveConfig>( `${getOauthConfig().BASE_API_URL}/api/claude_code_grove`, { headers: { ...authHeaders.headers, 'User-Agent': getUserAgent(), }, timeout: 3000, // Short timeout - if slow, skip Grove dialog }, ) })
// Map the API response to the GroveConfig type const { grove_enabled, domain_excluded, notice_is_grace_period, notice_reminder_frequency, } = response.data
return { success: true, data: { grove_enabled, domain_excluded: domain_excluded ?? false, notice_is_grace_period: notice_is_grace_period ?? true, notice_reminder_frequency, }, } } catch (err) { logForDebugging(`Failed to fetch Grove notice config: ${err}`) return { success: false } } },)
/** * Determines whether the Grove dialog should be shown. * Returns false if either API call failed (after retry) - we hide the dialog on API failure. */export function calculateShouldShowGrove( settingsResult: ApiResult<AccountSettings>, configResult: ApiResult<GroveConfig>, showIfAlreadyViewed: boolean,): boolean { // Hide dialog on API failure (after retry) if (!settingsResult.success || !configResult.success) { return false }
const settings = settingsResult.data const config = configResult.data
const hasChosen = settings.grove_enabled !== null if (hasChosen) { return false } if (showIfAlreadyViewed) { return true } if (!config.notice_is_grace_period) { return true } // Check if we need to remind the user to accept the terms and choose // whether to help improve Claude. const reminderFrequency = config.notice_reminder_frequency if (reminderFrequency !== null && settings.grove_notice_viewed_at) { const daysSinceViewed = Math.floor( (Date.now() - new Date(settings.grove_notice_viewed_at).getTime()) / (1000 * 60 * 60 * 24), ) return daysSinceViewed >= reminderFrequency } else { // Show if never viewed before const viewedAt = settings.grove_notice_viewed_at return viewedAt === null || viewedAt === undefined }}
export async function checkGroveForNonInteractive(): Promise<void> { const [settingsResult, configResult] = await Promise.all([ getGroveSettings(), getGroveNoticeConfig(), ])
// Check if user hasn't made a choice yet (returns false on API failure) const shouldShowGrove = calculateShouldShowGrove( settingsResult, configResult, false, )
if (shouldShowGrove) { // shouldShowGrove is only true if both API calls succeeded const config = configResult.success ? configResult.data : null logEvent('tengu_grove_print_viewed', { dismissable: config?.notice_is_grace_period as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, }) if (config === null || config.notice_is_grace_period) { // Grace period is still active - show informational message and continue writeToStderr( '\nAn update to our Consumer Terms and Privacy Policy will take effect on October 8, 2025. Run `claude` to review the updated terms.\n\n', ) await markGroveNoticeViewed() } else { // Grace period has ended - show error message and exit writeToStderr( '\n[ACTION REQUIRED] An update to our Consumer Terms and Privacy Policy has taken effect on October 8, 2025. You must run `claude` to review the updated terms.\n\n', ) await gracefulShutdown(1) } }}