source dump of claude code
at main 194 lines 5.7 kB view raw
1import { execa } from 'execa' 2import memoize from 'lodash-es/memoize.js' 3import { getSessionId } from '../bootstrap/state.js' 4import { 5 getOauthAccountInfo, 6 getRateLimitTier, 7 getSubscriptionType, 8} from './auth.js' 9import { getGlobalConfig, getOrCreateUserID } from './config.js' 10import { getCwd } from './cwd.js' 11import { type env, getHostPlatformForAnalytics } from './env.js' 12import { isEnvTruthy } from './envUtils.js' 13 14// Cache for email fetched asynchronously at startup 15let cachedEmail: string | undefined | null = null // null means not fetched yet 16let emailFetchPromise: Promise<string | undefined> | null = null 17 18/** 19 * GitHub Actions metadata when running in CI 20 */ 21export type GitHubActionsMetadata = { 22 actor?: string 23 actorId?: string 24 repository?: string 25 repositoryId?: string 26 repositoryOwner?: string 27 repositoryOwnerId?: string 28} 29 30/** 31 * Core user data used as base for all analytics providers. 32 * This is also the format used by GrowthBook. 33 */ 34export type CoreUserData = { 35 deviceId: string 36 sessionId: string 37 email?: string 38 appVersion: string 39 platform: typeof env.platform 40 organizationUuid?: string 41 accountUuid?: string 42 userType?: string 43 subscriptionType?: string 44 rateLimitTier?: string 45 firstTokenTime?: number 46 githubActionsMetadata?: GitHubActionsMetadata 47} 48 49/** 50 * Initialize user data asynchronously. Should be called early in startup. 51 * This pre-fetches the email so getUser() can remain synchronous. 52 */ 53export async function initUser(): Promise<void> { 54 if (cachedEmail === null && !emailFetchPromise) { 55 emailFetchPromise = getEmailAsync() 56 cachedEmail = await emailFetchPromise 57 emailFetchPromise = null 58 // Clear memoization cache so next call picks up the email 59 getCoreUserData.cache.clear?.() 60 } 61} 62 63/** 64 * Reset all user data caches. Call on auth changes (login/logout/account switch) 65 * so the next getCoreUserData() call picks up fresh credentials and email. 66 */ 67export function resetUserCache(): void { 68 cachedEmail = null 69 emailFetchPromise = null 70 getCoreUserData.cache.clear?.() 71 getGitEmail.cache.clear?.() 72} 73 74/** 75 * Get core user data. 76 * This is the base representation that gets transformed for different analytics providers. 77 */ 78export const getCoreUserData = memoize( 79 (includeAnalyticsMetadata?: boolean): CoreUserData => { 80 const deviceId = getOrCreateUserID() 81 const config = getGlobalConfig() 82 83 let subscriptionType: string | undefined 84 let rateLimitTier: string | undefined 85 let firstTokenTime: number | undefined 86 if (includeAnalyticsMetadata) { 87 subscriptionType = getSubscriptionType() ?? undefined 88 rateLimitTier = getRateLimitTier() ?? undefined 89 if (subscriptionType && config.claudeCodeFirstTokenDate) { 90 const configFirstTokenTime = new Date( 91 config.claudeCodeFirstTokenDate, 92 ).getTime() 93 if (!isNaN(configFirstTokenTime)) { 94 firstTokenTime = configFirstTokenTime 95 } 96 } 97 } 98 99 // Only include OAuth account data when actively using OAuth authentication 100 const oauthAccount = getOauthAccountInfo() 101 const organizationUuid = oauthAccount?.organizationUuid 102 const accountUuid = oauthAccount?.accountUuid 103 104 return { 105 deviceId, 106 sessionId: getSessionId(), 107 email: getEmail(), 108 appVersion: MACRO.VERSION, 109 platform: getHostPlatformForAnalytics(), 110 organizationUuid, 111 accountUuid, 112 userType: process.env.USER_TYPE, 113 subscriptionType, 114 rateLimitTier, 115 firstTokenTime, 116 ...(isEnvTruthy(process.env.GITHUB_ACTIONS) && { 117 githubActionsMetadata: { 118 actor: process.env.GITHUB_ACTOR, 119 actorId: process.env.GITHUB_ACTOR_ID, 120 repository: process.env.GITHUB_REPOSITORY, 121 repositoryId: process.env.GITHUB_REPOSITORY_ID, 122 repositoryOwner: process.env.GITHUB_REPOSITORY_OWNER, 123 repositoryOwnerId: process.env.GITHUB_REPOSITORY_OWNER_ID, 124 }, 125 }), 126 } 127 }, 128) 129 130/** 131 * Get user data for GrowthBook (same as core data with analytics metadata). 132 */ 133export function getUserForGrowthBook(): CoreUserData { 134 return getCoreUserData(true) 135} 136 137function getEmail(): string | undefined { 138 // Return cached email if available (from async initialization) 139 if (cachedEmail !== null) { 140 return cachedEmail 141 } 142 143 // Only include OAuth email when actively using OAuth authentication 144 const oauthAccount = getOauthAccountInfo() 145 if (oauthAccount?.emailAddress) { 146 return oauthAccount.emailAddress 147 } 148 149 // Ant-only fallbacks below (no execSync) 150 if (process.env.USER_TYPE !== 'ant') { 151 return undefined 152 } 153 154 if (process.env.COO_CREATOR) { 155 return `${process.env.COO_CREATOR}@anthropic.com` 156 } 157 158 // If initUser() wasn't called, we return undefined instead of blocking 159 return undefined 160} 161 162async function getEmailAsync(): Promise<string | undefined> { 163 // Only include OAuth email when actively using OAuth authentication 164 const oauthAccount = getOauthAccountInfo() 165 if (oauthAccount?.emailAddress) { 166 return oauthAccount.emailAddress 167 } 168 169 // Ant-only fallbacks below 170 if (process.env.USER_TYPE !== 'ant') { 171 return undefined 172 } 173 174 if (process.env.COO_CREATOR) { 175 return `${process.env.COO_CREATOR}@anthropic.com` 176 } 177 178 return getGitEmail() 179} 180 181/** 182 * Get the user's git email from `git config user.email`. 183 * Memoized so the subprocess only spawns once per process. 184 */ 185export const getGitEmail = memoize(async (): Promise<string | undefined> => { 186 const result = await execa('git config --get user.email', { 187 shell: true, 188 reject: false, 189 cwd: getCwd(), 190 }) 191 return result.exitCode === 0 && result.stdout 192 ? result.stdout.trim() 193 : undefined 194})