chore: initialize recovered claude workspace
This commit is contained in:
189
src/context.ts
Normal file
189
src/context.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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
|
||||
|
||||
// System prompt injection for cache breaking (ant-only, ephemeral debugging state)
|
||||
let systemPromptInjection: string | null = null
|
||||
|
||||
export function getSystemPromptInjection(): string | null {
|
||||
return systemPromptInjection
|
||||
}
|
||||
|
||||
export function setSystemPromptInjection(value: string | null): void {
|
||||
systemPromptInjection = value
|
||||
// Clear context caches immediately when injection changes
|
||||
getUserContext.cache.clear?.()
|
||||
getSystemContext.cache.clear?.()
|
||||
}
|
||||
|
||||
export const getGitStatus = memoize(async (): Promise<string | null> => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// Avoid cycles in tests
|
||||
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,
|
||||
})
|
||||
|
||||
// Check if status exceeds character limit
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This context is prepended to each conversation, and cached for the duration of the conversation.
|
||||
*/
|
||||
export const getSystemContext = memoize(
|
||||
async (): Promise<{
|
||||
[k: string]: string
|
||||
}> => {
|
||||
const startTime = Date.now()
|
||||
logForDiagnosticsNoPII('info', 'system_context_started')
|
||||
|
||||
// Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled
|
||||
const gitStatus =
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
|
||||
!shouldIncludeGitInstructions()
|
||||
? null
|
||||
: await getGitStatus()
|
||||
|
||||
// Include system prompt injection if set (for cache breaking, ant-only)
|
||||
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}]`,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* This context is prepended to each conversation, and cached for the duration of the conversation.
|
||||
*/
|
||||
export const getUserContext = memoize(
|
||||
async (): Promise<{
|
||||
[k: string]: string
|
||||
}> => {
|
||||
const startTime = Date.now()
|
||||
logForDiagnosticsNoPII('info', 'user_context_started')
|
||||
|
||||
// CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always.
|
||||
// --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir.
|
||||
// --bare means "skip what I didn't ask for", not "ignore what I asked for".
|
||||
const shouldDisableClaudeMd =
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
|
||||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
|
||||
// Await the async I/O (readFile/readdir directory walk) so the event
|
||||
// loop yields naturally at the first fs.readFile.
|
||||
const claudeMd = shouldDisableClaudeMd
|
||||
? null
|
||||
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
|
||||
// Cache for the auto-mode classifier (yoloClassifier.ts reads this
|
||||
// instead of importing claudemd.ts directly, which would create a
|
||||
// cycle through permissions/filesystem → permissions → yoloClassifier).
|
||||
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()}.`,
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user