| import { feature } from 'bun:bundle' |
| import memoize from 'lodash-es/memoize.js' |
| import { |
| getAdditionalDirectoriesForClaudeMd, |
| setCachedClaudeMdContent, |
| } from './bootstrap/state.js' |
| import { getLocalISODate } from './constants/common.js' |
| import { |
| filterInjectedMemoryFiles, |
| getClaudeMds, |
| getMemoryFiles, |
| } from './utils/claudemd.js' |
| import { logForDiagnosticsNoPII } from './utils/diagLogs.js' |
| import { isBareMode, isEnvTruthy } from './utils/envUtils.js' |
| import { execFileNoThrow } from './utils/execFileNoThrow.js' |
| import { getBranch, getDefaultBranch, getIsGit, gitExe } from './utils/git.js' |
| import { shouldIncludeGitInstructions } from './utils/gitSettings.js' |
| import { logError } from './utils/log.js' |
|
|
| const MAX_STATUS_CHARS = 2000 |
|
|
| |
| let systemPromptInjection: string | null = null |
|
|
| export function getSystemPromptInjection(): string | null { |
| return systemPromptInjection |
| } |
|
|
| export function setSystemPromptInjection(value: string | null): void { |
| systemPromptInjection = value |
| |
| getUserContext.cache.clear?.() |
| getSystemContext.cache.clear?.() |
| } |
|
|
| export const getGitStatus = memoize(async (): Promise<string | null> => { |
| if (process.env.NODE_ENV === 'test') { |
| |
| return null |
| } |
|
|
| const startTime = Date.now() |
| logForDiagnosticsNoPII('info', 'git_status_started') |
|
|
| const isGitStart = Date.now() |
| const isGit = await getIsGit() |
| logForDiagnosticsNoPII('info', 'git_is_git_check_completed', { |
| duration_ms: Date.now() - isGitStart, |
| is_git: isGit, |
| }) |
|
|
| if (!isGit) { |
| logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', { |
| duration_ms: Date.now() - startTime, |
| }) |
| return null |
| } |
|
|
| try { |
| const gitCmdsStart = Date.now() |
| const [branch, mainBranch, status, log, userName] = await Promise.all([ |
| getBranch(), |
| getDefaultBranch(), |
| execFileNoThrow(gitExe(), ['--no-optional-locks', 'status', '--short'], { |
| preserveOutputOnError: false, |
| }).then(({ stdout }) => stdout.trim()), |
| execFileNoThrow( |
| gitExe(), |
| ['--no-optional-locks', 'log', '--oneline', '-n', '5'], |
| { |
| preserveOutputOnError: false, |
| }, |
| ).then(({ stdout }) => stdout.trim()), |
| execFileNoThrow(gitExe(), ['config', 'user.name'], { |
| preserveOutputOnError: false, |
| }).then(({ stdout }) => stdout.trim()), |
| ]) |
|
|
| logForDiagnosticsNoPII('info', 'git_commands_completed', { |
| duration_ms: Date.now() - gitCmdsStart, |
| status_length: status.length, |
| }) |
|
|
| |
| const truncatedStatus = |
| status.length > MAX_STATUS_CHARS |
| ? status.substring(0, MAX_STATUS_CHARS) + |
| '\n... (truncated because it exceeds 2k characters. If you need more information, run "git status" using BashTool)' |
| : status |
|
|
| logForDiagnosticsNoPII('info', 'git_status_completed', { |
| duration_ms: Date.now() - startTime, |
| truncated: status.length > MAX_STATUS_CHARS, |
| }) |
|
|
| return [ |
| `This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.`, |
| `Current branch: ${branch}`, |
| `Main branch (you will usually use this for PRs): ${mainBranch}`, |
| ...(userName ? [`Git user: ${userName}`] : []), |
| `Status:\n${truncatedStatus || '(clean)'}`, |
| `Recent commits:\n${log}`, |
| ].join('\n\n') |
| } catch (error) { |
| logForDiagnosticsNoPII('error', 'git_status_failed', { |
| duration_ms: Date.now() - startTime, |
| }) |
| logError(error) |
| return null |
| } |
| }) |
|
|
| |
| |
| |
| export const getSystemContext = memoize( |
| async (): Promise<{ |
| [k: string]: string |
| }> => { |
| const startTime = Date.now() |
| logForDiagnosticsNoPII('info', 'system_context_started') |
|
|
| |
| const gitStatus = |
| isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) || |
| !shouldIncludeGitInstructions() |
| ? null |
| : await getGitStatus() |
|
|
| |
| const injection = feature('BREAK_CACHE_COMMAND') |
| ? getSystemPromptInjection() |
| : null |
|
|
| logForDiagnosticsNoPII('info', 'system_context_completed', { |
| duration_ms: Date.now() - startTime, |
| has_git_status: gitStatus !== null, |
| has_injection: injection !== null, |
| }) |
|
|
| return { |
| ...(gitStatus && { gitStatus }), |
| ...(feature('BREAK_CACHE_COMMAND') && injection |
| ? { |
| cacheBreaker: `[CACHE_BREAKER: ${injection}]`, |
| } |
| : {}), |
| } |
| }, |
| ) |
|
|
| |
| |
| |
| export const getUserContext = memoize( |
| async (): Promise<{ |
| [k: string]: string |
| }> => { |
| const startTime = Date.now() |
| logForDiagnosticsNoPII('info', 'user_context_started') |
|
|
| |
| |
| |
| const shouldDisableClaudeMd = |
| isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) || |
| (isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0) |
| |
| |
| const claudeMd = shouldDisableClaudeMd |
| ? null |
| : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles())) |
| |
| |
| |
| setCachedClaudeMdContent(claudeMd || null) |
|
|
| logForDiagnosticsNoPII('info', 'user_context_completed', { |
| duration_ms: Date.now() - startTime, |
| claudemd_length: claudeMd?.length ?? 0, |
| claudemd_disabled: Boolean(shouldDisableClaudeMd), |
| }) |
|
|
| return { |
| ...(claudeMd && { claudeMd }), |
| currentDate: `Today's date is ${getLocalISODate()}.`, |
| } |
| }, |
| ) |
|
|