Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
13 kB · 393 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394import { feature } from 'bun:bundle'import { stat } from 'fs/promises'import { getClientType } from '../bootstrap/state.js'import { getRemoteSessionUrl, isRemoteSessionLocal, PRODUCT_URL,} from '../constants/product.js'import { TERMINAL_OUTPUT_TAGS } from '../constants/xml.js'import type { AppState } from '../state/AppState.js'import { FILE_EDIT_TOOL_NAME } from '../tools/FileEditTool/constants.js'import { FILE_READ_TOOL_NAME } from '../tools/FileReadTool/prompt.js'import { FILE_WRITE_TOOL_NAME } from '../tools/FileWriteTool/prompt.js'import { GLOB_TOOL_NAME } from '../tools/GlobTool/prompt.js'import { GREP_TOOL_NAME } from '../tools/GrepTool/prompt.js'import type { Entry } from '../types/logs.js'import { type AttributionData, calculateCommitAttribution, isInternalModelRepo, isInternalModelRepoCached, sanitizeModelName,} from './commitAttribution.js'import { logForDebugging } from './debug.js'import { parseJSONL } from './json.js'import { logError } from './log.js'import { getCanonicalName, getMainLoopModel, getPublicModelDisplayName, getPublicModelName,} from './model/model.js'import { isMemoryFileAccess } from './sessionFileAccessHooks.js'import { getTranscriptPath } from './sessionStorage.js'import { readTranscriptForLoad } from './sessionStoragePortable.js'import { getInitialSettings } from './settings/settings.js'import { isUndercover } from './undercover.js'
export type AttributionTexts = { commit: string pr: string}
/** * Returns attribution text for commits and PRs based on user settings. * Handles: * - Dynamic model name via getPublicModelName() * - Custom attribution settings (settings.attribution.commit/pr) * - Backward compatibility with deprecated includeCoAuthoredBy setting * - Remote mode: returns session URL for attribution */export function getAttributionTexts(): AttributionTexts { if (process.env.USER_TYPE === 'ant' && isUndercover()) { return { commit: '', pr: '' } }
if (getClientType() === 'remote') { const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID if (remoteSessionId) { const ingressUrl = process.env.SESSION_INGRESS_URL // Skip for local dev - URLs won't persist if (!isRemoteSessionLocal(remoteSessionId, ingressUrl)) { const sessionUrl = getRemoteSessionUrl(remoteSessionId, ingressUrl) return { commit: sessionUrl, pr: sessionUrl } } } return { commit: '', pr: '' } }
// @[MODEL LAUNCH]: Update the hardcoded fallback model name below (guards against codename leaks). // For internal repos, use the real model name. For external repos, // fall back to "Claude Opus 4.6" for unrecognized models to avoid leaking codenames. const model = getMainLoopModel() const isKnownPublicModel = getPublicModelDisplayName(model) !== null const modelName = isInternalModelRepoCached() || isKnownPublicModel ? getPublicModelName(model) : 'Claude Opus 4.6' const defaultAttribution = `🤖 Generated with [Claude Code](${PRODUCT_URL})` const defaultCommit = `Co-Authored-By: ${modelName} <noreply@anthropic.com>`
const settings = getInitialSettings()
// New attribution setting takes precedence over deprecated includeCoAuthoredBy if (settings.attribution) { return { commit: settings.attribution.commit ?? defaultCommit, pr: settings.attribution.pr ?? defaultAttribution, } }
// Backward compatibility: deprecated includeCoAuthoredBy setting if (settings.includeCoAuthoredBy === false) { return { commit: '', pr: '' } }
return { commit: defaultCommit, pr: defaultAttribution }}
/** * Check if a message content string is terminal output rather than a user prompt. * Terminal output includes bash input/output tags and caveat messages about local commands. */function isTerminalOutput(content: string): boolean { for (const tag of TERMINAL_OUTPUT_TAGS) { if (content.includes(`<${tag}>`)) { return true } } return false}
/** * Count user messages with visible text content in a list of non-sidechain messages. * Excludes tool_result blocks, terminal output, and empty messages. * * Callers should pass messages already filtered to exclude sidechain messages. */export function countUserPromptsInMessages( messages: ReadonlyArray<{ type: string; message?: { content?: unknown } }>,): number { let count = 0
for (const message of messages) { if (message.type !== 'user') { continue }
const content = message.message?.content if (!content) { continue }
let hasUserText = false
if (typeof content === 'string') { if (isTerminalOutput(content)) { continue } hasUserText = content.trim().length > 0 } else if (Array.isArray(content)) { hasUserText = content.some(block => { if (!block || typeof block !== 'object' || !('type' in block)) { return false } return ( (block.type === 'text' && typeof block.text === 'string' && !isTerminalOutput(block.text)) || block.type === 'image' || block.type === 'document' ) }) }
if (hasUserText) { count++ } }
return count}
/** * Count non-sidechain user messages in transcript entries. * Used to calculate the number of "steers" (user prompts - 1). * * Counts user messages that contain actual user-typed text, * excluding tool_result blocks, sidechain messages, and terminal output. */function countUserPromptsFromEntries(entries: ReadonlyArray<Entry>): number { const nonSidechain = entries.filter( entry => entry.type === 'user' && !('isSidechain' in entry && entry.isSidechain), ) return countUserPromptsInMessages(nonSidechain)}
/** * Get full attribution data from the provided AppState's attribution state. * Uses ALL tracked files from the attribution state (not just staged files) * because for PR attribution, files may not be staged yet. * Returns null if no attribution data is available. */async function getPRAttributionData( appState: AppState,): Promise<AttributionData | null> { const attribution = appState.attribution
if (!attribution) { return null }
// Handle both Map and plain object (in case of serialization) const fileStates = attribution.fileStates const isMap = fileStates instanceof Map const trackedFiles = isMap ? Array.from(fileStates.keys()) : Object.keys(fileStates)
if (trackedFiles.length === 0) { return null }
try { return await calculateCommitAttribution([attribution], trackedFiles) } catch (error) { logError(error as Error) return null }}
const MEMORY_ACCESS_TOOL_NAMES = new Set([ FILE_READ_TOOL_NAME, GREP_TOOL_NAME, GLOB_TOOL_NAME, FILE_EDIT_TOOL_NAME, FILE_WRITE_TOOL_NAME,])
/** * Count memory file accesses in transcript entries. * Uses the same detection conditions as the PostToolUse session file access hooks. */function countMemoryFileAccessFromEntries( entries: ReadonlyArray<Entry>,): number { let count = 0 for (const entry of entries) { if (entry.type !== 'assistant') continue const content = entry.message?.content if (!Array.isArray(content)) continue for (const block of content) { if ( block.type !== 'tool_use' || !MEMORY_ACCESS_TOOL_NAMES.has(block.name) ) continue if (isMemoryFileAccess(block.name, block.input)) count++ } } return count}
/** * Read session transcript entries and compute prompt count and memory access * count. Pre-compact entries are skipped — the N-shot count and memory-access * count should reflect only the current conversation arc, not accumulated * prompts from before a compaction boundary. */async function getTranscriptStats(): Promise<{ promptCount: number memoryAccessCount: number}> { try { const filePath = getTranscriptPath() const fileSize = (await stat(filePath)).size // Fused reader: attr-snap lines (84% of a long session by bytes) are // skipped at the fd level so peak scales with output, not file size. The // one surviving attr-snap at EOF is a no-op for the count functions // (neither checks type === 'attribution-snapshot'). When the last // boundary has preservedSegment the reader returns full (no truncate); // the findLastIndex below still slices to post-boundary. const scan = await readTranscriptForLoad(filePath, fileSize) const buf = scan.postBoundaryBuf const entries = parseJSONL<Entry>(buf) const lastBoundaryIdx = entries.findLastIndex( e => e.type === 'system' && 'subtype' in e && e.subtype === 'compact_boundary', ) const postBoundary = lastBoundaryIdx >= 0 ? entries.slice(lastBoundaryIdx + 1) : entries return { promptCount: countUserPromptsFromEntries(postBoundary), memoryAccessCount: countMemoryFileAccessFromEntries(postBoundary), } } catch { return { promptCount: 0, memoryAccessCount: 0 } }}
/** * Get enhanced PR attribution text with Claude contribution stats. * * Format: "🤖 Generated with Claude Code (93% 3-shotted by claude-opus-4-5)" * * Rules: * - Shows Claude contribution percentage from commit attribution * - Shows N-shotted where N is the prompt count (1-shotted, 2-shotted, etc.) * - Shows short model name (e.g., claude-opus-4-5) * - Returns default attribution if stats can't be computed * * @param getAppState Function to get the current AppState (from command context) */export async function getEnhancedPRAttribution( getAppState: () => AppState,): Promise<string> { if (process.env.USER_TYPE === 'ant' && isUndercover()) { return '' }
if (getClientType() === 'remote') { const remoteSessionId = process.env.CLAUDE_CODE_REMOTE_SESSION_ID if (remoteSessionId) { const ingressUrl = process.env.SESSION_INGRESS_URL // Skip for local dev - URLs won't persist if (!isRemoteSessionLocal(remoteSessionId, ingressUrl)) { return getRemoteSessionUrl(remoteSessionId, ingressUrl) } } return '' }
const settings = getInitialSettings()
// If user has custom PR attribution, use that if (settings.attribution?.pr) { return settings.attribution.pr }
// Backward compatibility: deprecated includeCoAuthoredBy setting if (settings.includeCoAuthoredBy === false) { return '' }
const defaultAttribution = `🤖 Generated with [Claude Code](${PRODUCT_URL})`
// Get AppState first const appState = getAppState()
logForDebugging( `PR Attribution: appState.attribution exists: ${!!appState.attribution}`, ) if (appState.attribution) { const fileStates = appState.attribution.fileStates const isMap = fileStates instanceof Map const fileCount = isMap ? fileStates.size : Object.keys(fileStates).length logForDebugging(`PR Attribution: fileStates count: ${fileCount}`) }
// Get attribution stats (transcript is read once for both prompt count and memory access) const [attributionData, { promptCount, memoryAccessCount }, isInternal] = await Promise.all([ getPRAttributionData(appState), getTranscriptStats(), isInternalModelRepo(), ])
const claudePercent = attributionData?.summary.claudePercent ?? 0
logForDebugging( `PR Attribution: claudePercent: ${claudePercent}, promptCount: ${promptCount}, memoryAccessCount: ${memoryAccessCount}`, )
// Get short model name, sanitized for non-internal repos const rawModelName = getCanonicalName(getMainLoopModel()) const shortModelName = isInternal ? rawModelName : sanitizeModelName(rawModelName)
// If no attribution data, return default if (claudePercent === 0 && promptCount === 0 && memoryAccessCount === 0) { logForDebugging('PR Attribution: returning default (no data)') return defaultAttribution }
// Build the enhanced attribution: "🤖 Generated with Claude Code (93% 3-shotted by claude-opus-4-5, 2 memories recalled)" const memSuffix = memoryAccessCount > 0 ? `, ${memoryAccessCount} ${memoryAccessCount === 1 ? 'memory' : 'memories'} recalled` : '' const summary = `🤖 Generated with [Claude Code](${PRODUCT_URL}) (${claudePercent}% ${promptCount}-shotted by ${shortModelName}${memSuffix})`
// Append trailer lines for squash-merge survival. Only for allowlisted repos // (INTERNAL_MODEL_REPOS) and only in builds with COMMIT_ATTRIBUTION enabled — // attributionTrailer.ts contains excluded strings, so reach it via dynamic // import behind feature(). When the repo is configured with // squash_merge_commit_message=PR_BODY (cli, apps), the PR body becomes the // squash commit body verbatim — trailer lines at the end become proper git // trailers on the squash commit. if (feature('COMMIT_ATTRIBUTION') && isInternal && attributionData) { const { buildPRTrailers } = await import('./attributionTrailer.js') const trailers = buildPRTrailers(attributionData, appState.attribution) const result = `${summary}\n\n${trailers.join('\n')}` logForDebugging(`PR Attribution: returning with trailers: ${result}`) return result }
logForDebugging(`PR Attribution: returning summary: ${summary}`) return summary}