This commit is contained in:
y574444354 2026-04-23 10:30:28 +08:00
commit 7806df4e3d
25 changed files with 136 additions and 91 deletions

View File

@ -0,0 +1,28 @@
# csc 项目 UI 文案可直接替换清单
- 目的:筛选出**只需要替换展示文案**、即可让 UI 页面显示 `CoStrict` 而不是 `Claude` / `Claude Code` / `Anthropic` 的内容。
- 筛选原则:
1. 仅保留用户可见文案,例如 `title`、`subtitle`、`message`、`placeholder`、`label`、`aria-label`、`<Text>` 文本。
2. 不纳入命令、包名、环境变量、URL、路径、主题 token、埋点名、注释说明。
3. 对账号体系、订阅体系、Desktop/Chrome 集成等文案,标记为“**可替换但需谨慎**”。
---
## 一、高置信:可直接文本替换的 UI 文案
这部分基本都属于纯展示文本,直接替换后主要影响界面显示,不会直接触发命令、配置或协议变更。
| 文件 | 行号 | 原文 | 建议替换为 |
| --- | ---: | --- | --- |
| `packages/remote-control-server/web/index.html` | L6 | `Remote Control — Claude Code` | `Remote Control — CoStrict` |
| `packages/remote-control-server/web/automation.js` | L280 | `Claude Code is in proactive mode and currently sleeping until the next wake-up or user message.` | `CoStrict is in proactive mode and currently sleeping until the next wake-up or user message.` |
| `packages/remote-control-server/web/automation.js` | L290 | `Claude Code is in proactive mode and waiting for the next scheduled check-in.` | `CoStrict is in proactive mode and waiting for the next scheduled check-in.` |
| `packages/remote-control-server/web/automation.js` | L299 / L309 | `Claude Code is in proactive mode and may continue working between user messages.` | `CoStrict is in proactive mode and may continue working between user messages.` |
| `packages/remote-control-server/web/automation.js` | L319 | `Claude Code is processing an automatic background trigger.` | `CoStrict is processing an automatic background trigger.` |
| `packages/remote-control-server/web/automation.js` | L361 | `aria-label="Claude Code status"` | `aria-label="CoStrict status"` |
| `packages/@ant/ink/src/theme/LoadingState.tsx` | L45 | `Fetching your Claude Code sessions...` | `Fetching your CoStrict sessions...` |
| `src/components/FeedbackSurvey/FeedbackSurveyView.tsx` | L26 | `How is Claude doing this session? (optional)` | `How is CoStrict doing this session? (optional)` |
| `src/components/FeedbackSurvey/TranscriptSharePrompt.tsx` | L43 | `Can Anthropic look at your session transcript to help us improve` | `Can CoStrict look at your session transcript to help us improve` |
| `src/components/CostThresholdDialog.tsx` | L12 | `You've spent $5 on the Anthropic API this session.` | `You've spent $5 on the CoStrict API this session.` |
| `src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx` | L47 | `Description (tell Claude when to use this agent)` | `Description (tell CoStrict when to use this agent)` |
| `src/components/agents/new-agent-creation/wizard-steps/DescriptionStep.tsx` | L68 | `When should Claude use this agent?` | `When should CoStrict use this agent?` |

View File

@ -42,7 +42,7 @@ type LoadingStateProps = {
* <LoadingState
* message="Loading sessions"
* bold
* subtitle="Fetching your Claude Code sessions..."
* subtitle="Fetching your CoStrict sessions..."
* />
*/
export function LoadingState({

View File

@ -428,7 +428,7 @@ describe('DeepSeek thinking mode (enableThinking)', () => {
{ enableThinking: true },
)
const assistant = result.filter(m => m.role === 'assistant')[0] as any
expect(assistant.reasoning_content).toBeUndefined()
expect(assistant.reasoning_content).toBe(' ')
})
// ── fix: reorder tool and user messages for OpenAI API compatibility (#168) ──

View File

@ -211,21 +211,29 @@ function convertInternalAssistantMessage(
const content = msg.message.content
if (typeof content === 'string') {
return [
{
role: 'assistant',
content,
} satisfies ChatCompletionAssistantMessageParam,
]
const result: ChatCompletionAssistantMessageParam & { reasoning_content?: string } = {
role: 'assistant',
content,
}
// 如果开启了thinking模式必须提供reasoning_content字段
if (preserveReasoning) {
// content是字符串没有reasoning内容设置为空字符串
result.reasoning_content = ' '
}
return [result]
}
if (!Array.isArray(content)) {
return [
{
role: 'assistant',
content: '',
} satisfies ChatCompletionAssistantMessageParam,
]
const result: ChatCompletionAssistantMessageParam & { reasoning_content?: string } = {
role: 'assistant',
content: '',
}
// 如果开启了thinking模式必须提供reasoning_content字段
if (preserveReasoning) {
// content不是数组没有reasoning内容设置为空字符串
result.reasoning_content = ' '
}
return [result]
}
const textParts: string[] = []
@ -258,11 +266,16 @@ function convertInternalAssistantMessage(
// Skip redacted_thinking, server_tool_use, etc.
}
const result: ChatCompletionAssistantMessageParam = {
const result: ChatCompletionAssistantMessageParam & { reasoning_content?: string } = {
role: 'assistant',
content: textParts.length > 0 ? textParts.join('\n') : null,
...(toolCalls.length > 0 && { tool_calls: toolCalls }),
...(reasoningParts.length > 0 && { reasoning_content: reasoningParts.join('\n') }),
}
// 如果开启了thinking模式必须提供reasoning_content字段
if (preserveReasoning) {
// 如果有reasoning内容就用这些内容没有才设置成空字符串
result.reasoning_content = reasoningParts.length > 0 ? reasoningParts.join('\n') : ' '
}
return [result]

View File

@ -277,7 +277,7 @@ export function getAutomationIndicator(state) {
visible: true,
label: "Autopilot",
tone: "sleeping",
title: "Claude Code is in proactive mode and currently sleeping until the next wake-up or user message.",
title: "CoStrict is in proactive mode and currently sleeping until the next wake-up or user message.",
iconVariant: "sleeping",
};
}
@ -287,7 +287,7 @@ export function getAutomationIndicator(state) {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and waiting for the next scheduled check-in.",
title: "CoStrict is in proactive mode and waiting for the next scheduled check-in.",
iconVariant: "standby",
};
}
@ -296,7 +296,7 @@ export function getAutomationIndicator(state) {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and may continue working between user messages.",
title: "CoStrict is in proactive mode and may continue working between user messages.",
iconVariant: "active",
};
}
@ -306,7 +306,7 @@ export function getAutomationIndicator(state) {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and may continue working between user messages.",
title: "CoStrict is in proactive mode and may continue working between user messages.",
iconVariant: "active",
};
}
@ -316,7 +316,7 @@ export function getAutomationIndicator(state) {
visible: true,
label: "Auto Run",
tone: "auto-run",
title: "Claude Code is processing an automatic background trigger.",
title: "CoStrict is processing an automatic background trigger.",
iconVariant: "active",
};
}
@ -358,7 +358,7 @@ export function getAutomationActivity(state) {
export function renderAutomationIcon(variant = "active", { className = "", decorative = true } = {}) {
const classes = ["clawd-icon", `clawd-icon-${variant}`, className].filter(Boolean).join(" ");
const ariaAttrs = decorative ? 'aria-hidden="true"' : 'role="img" aria-label="Claude Code status"';
const ariaAttrs = decorative ? 'aria-hidden="true"' : 'role="img" aria-label="CoStrict status"';
return `
<span class="${classes}" ${ariaAttrs}>

View File

@ -96,7 +96,7 @@ describe("automation helpers", () => {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and may continue working between user messages.",
title: "CoStrict is in proactive mode and may continue working between user messages.",
iconVariant: "active",
});
@ -154,7 +154,7 @@ describe("automation helpers", () => {
visible: true,
label: "Autopilot",
tone: "proactive",
title: "Claude Code is in proactive mode and waiting for the next scheduled check-in.",
title: "CoStrict is in proactive mode and waiting for the next scheduled check-in.",
iconVariant: "standby",
});
expect(getAutomationActivity(state)).toEqual({

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Remote Control — Claude Code</title>
<title>Remote Control — CoStrict</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700&family=Figtree:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" />

View File

@ -37,6 +37,7 @@ import { generateMachineId, saveCoStrictCredentials } from '../../costrict/provi
import { extractExpiryFromJWT } from '../../costrict/provider/token.js';
import { updateSettingsForSource } from '../../utils/settings/settings.js';
import { useSetAppState as useSetGlobalAppState } from '../../state/AppState.js';
import { isNotLoggedIn } from '../../utils/logoV2Utils.js';
function ModelPickerWrapper({
onDone,
@ -339,6 +340,15 @@ export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
return <SetModelAndClose args={args} onDone={onDone} />;
}
// Check if user is logged in before showing model picker
if (isNotLoggedIn()) {
// User is not logged in, redirect to login
onDone('You need to login first. Use /login to authenticate.', {
display: 'system',
});
return;
}
return <ModelPickerWrapper onDone={onDone} />;
};

View File

@ -288,6 +288,8 @@ export function ConsoleOAuthFlow({
updateSettingsForSource('userSettings', { model: getDefaultSonnetModel() } as any)
// Clear costrict env var to prevent conflicts
delete process.env.CLAUDE_CODE_USE_COSTRICT
// Reset AppState model so logo reflects the new provider default
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void sendNotification(
@ -734,6 +736,8 @@ function OAuthStatusMessage({
delete process.env.CLAUDE_CODE_USE_COSTRICT
// Set default model to sonnet for this provider
updateSettingsForSource('userSettings', { model: getDefaultSonnetModel() } as any)
// Reset AppState model so logo reflects the new provider default
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}
@ -958,6 +962,8 @@ function OAuthStatusMessage({
delete process.env.CLAUDE_CODE_USE_COSTRICT
// Set default model to sonnet for this provider
updateSettingsForSource('userSettings', { model: getDefaultSonnetModel() } as any)
// Reset AppState model so logo reflects the new provider default
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}
@ -1195,6 +1201,8 @@ function OAuthStatusMessage({
delete process.env.CLAUDE_CODE_USE_COSTRICT
// Set default model to sonnet for this provider
updateSettingsForSource('userSettings', { model: getDefaultSonnetModel() } as any)
// Reset AppState model so logo reflects the new provider default
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}

View File

@ -9,7 +9,7 @@ type Props = {
export function CostThresholdDialog({ onDone }: Props): React.ReactNode {
return (
<Dialog
title="You've spent $5 on the Anthropic API this session."
title="You've spent $5 on the CoStrict API this session."
onCancel={onDone}
>
<Box flexDirection="column">

View File

@ -23,7 +23,7 @@ const inputToResponse: Record<ResponseInput, FeedbackSurveyResponse> = {
export const isValidResponseInput = (input: string): input is ResponseInput =>
(RESPONSE_INPUTS as readonly string[]).includes(input)
const DEFAULT_MESSAGE = 'How is Claude doing this session? (optional)'
const DEFAULT_MESSAGE = 'How is CoStrict doing this session? (optional)'
export function FeedbackSurveyView({
onSelect,

View File

@ -40,8 +40,8 @@ export function TranscriptSharePrompt({
<Box>
<Text color="ansi:cyan">{BLACK_CIRCLE} </Text>
<Text bold>
Can Anthropic look at your session transcript to help us improve
CoStrict?
Can the CoStrict team look at your session transcript to help us
improve CoStrict?
</Text>
</Box>

View File

@ -44,7 +44,7 @@ export function DescriptionStep(): ReactNode {
return (
<WizardDialogLayout
subtitle="Description (tell Claude when to use this agent)"
subtitle="Description (tell CoStrict when to use this agent)"
footerText={
<Byline>
<KeyboardShortcutHint shortcut="Type" action="enter text" />
@ -65,7 +65,7 @@ export function DescriptionStep(): ReactNode {
}
>
<Box flexDirection="column">
<Text>When should Claude use this agent?</Text>
<Text>When should CoStrict use this agent?</Text>
<Box marginTop={1}>
<TextInput

View File

@ -135,7 +135,7 @@ export function UltraplanChoiceDialog({
if (transcriptSaved) {
setMessages(prev => [
...prev,
createSystemMessage(`Previous session saved · resume with: claude --resume ${previousSessionId}`, 'suggestion'),
createSystemMessage(`Previous session saved · resume with: csc --resume ${previousSessionId}`, 'suggestion'),
]);
}

View File

@ -31,6 +31,7 @@ import { createCoStrictFetch } from './fetch.js'
import { resolveCoStrictModel } from './modelMapping.js'
import { getCoStrictBaseURL } from './auth.js'
import { loadCoStrictCredentials } from './credentials.js'
import { isOpenAIThinkingEnabled } from '../../services/api/openai/requestBody.js'
/**
* CoStrict
@ -80,9 +81,12 @@ export async function* queryModelCoStrict(
)
// 5. 转换为 OpenAI 格式
// 根据模型名称自动检测是否启用thinking模式
const enableThinking = isOpenAIThinkingEnabled(costrictModel)
const openaiMessages = anthropicMessagesToOpenAI(
messagesForAPI,
systemPrompt
systemPrompt,
{ enableThinking }
)
const openaiTools = anthropicToolsToOpenAI(standardTools)
const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice)

View File

@ -17,10 +17,9 @@ function getModelFamily(model: string): 'haiku' | 'sonnet' | 'opus' | null {
*
* :
* 1. model CoStrict ID /model
* 2. COSTRICT_MODEL /
* 3. COSTRICT_DEFAULT_{FAMILY}_MODEL
* 4. CoStrict
* 5.
* 2. COSTRICT_DEFAULT_{FAMILY}_MODEL
* 3.
* 4. COSTRICT_MODEL
*/
export function resolveCoStrictModel(anthropicModel: string): string {
const cleanModel = anthropicModel.replace(/\[1m\]$/, '')
@ -29,10 +28,7 @@ export function resolveCoStrictModel(anthropicModel: string): string {
const cached = getCachedCoStrictModels()
if (cached.some(m => m.id === cleanModel)) return cleanModel
// 优先级 2: COSTRICT_MODEL 环境变量
if (process.env.COSTRICT_MODEL) return process.env.COSTRICT_MODEL
// 优先级 3: COSTRICT_DEFAULT_{FAMILY}_MODEL 环境变量
// 优先级 2: COSTRICT_DEFAULT_{FAMILY}_MODEL 环境变量
const family = getModelFamily(cleanModel)
if (family) {
const envVar = `COSTRICT_DEFAULT_${family.toUpperCase()}_MODEL`
@ -40,9 +36,6 @@ export function resolveCoStrictModel(anthropicModel: string): string {
if (override) return override
}
// 优先级 4: 缓存中的第一个模型
if (cached.length > 0) return cached[0].id
// 优先级 5: 透传原始模型名
// 优先级 3: 直接透传原始模型名(用户手动配置的模型名优先于环境变量)
return cleanModel
}

View File

@ -721,7 +721,7 @@ function TranscriptSearchBar({
}
const TITLE_ANIMATION_FRAMES = ['⠂', '⠐'];
const TITLE_STATIC_PREFIX = '';
const TITLE_STATIC_PREFIX = '';
const TITLE_ANIMATION_INTERVAL_MS = 960;
/**

View File

@ -25,9 +25,9 @@ export function isOpenAIThinkingEnabled(model: string): boolean {
if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false
// Explicit enable
if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true
// Auto-detect from model name (deepseek-reasoner and DeepSeek-V3.2 support thinking mode)
// Auto-detect from model name (deepseek-reasoner, DeepSeek-V3.2, kimi support thinking mode)
const modelLower = model.toLowerCase()
return modelLower.includes('deepseek-reasoner') || modelLower.includes('deepseek-v3.2')
return modelLower.includes('deepseek-reasoner') || modelLower.includes('deepseek-v3.2') || modelLower.includes('kimi')
}
/**

View File

@ -376,7 +376,7 @@ const externalTips: Tip[] = [
{
id: 'continue',
content: async () =>
'Run claude --continue or claude --resume to resume a conversation',
'Run csc --continue or csc --resume to resume a conversation',
cooldownSessions: 10,
isRelevant: async () => true,
},

View File

@ -433,9 +433,8 @@ export async function setup(
// 预取模型列表,填充同步缓存
const baseUrl = getCoStrictBaseURL(activeCreds.base_url)
const models = await fetchCoStrictModels(baseUrl, activeCreds.access_token)
if (models.length > 0 && !process.env.COSTRICT_MODEL) {
process.env.COSTRICT_MODEL = models[0].id
}
// 模型列表已预取并缓存,不再自动设置 COSTRICT_MODEL 环境变量
// resolveCoStrictModel() 会直接透传用户配置的模型名
process.env.CLAUDE_CODE_USE_COSTRICT = '1'
} catch {
// 初始化失败不阻断启动

View File

@ -41,7 +41,7 @@ export function checkCrossProjectResume(
// Gate worktree detection to ants only for staged rollout
if (process.env.USER_TYPE !== 'ant') {
const sessionId = getSessionIdFromLog(log)
const command = `cd ${quote([log.projectPath])} && claude --resume ${sessionId}`
const command = `cd ${quote([log.projectPath])} && csc --resume ${sessionId}`
return {
isCrossProject: true,
isSameRepoWorktree: false,
@ -65,7 +65,7 @@ export function checkCrossProjectResume(
// Different repo - generate cd command
const sessionId = getSessionIdFromLog(log)
const command = `cd ${quote([log.projectPath])} && claude --resume ${sessionId}`
const command = `cd ${quote([log.projectPath])} && csc --resume ${sessionId}`
return {
isCrossProject: true,
isSameRepoWorktree: false,

View File

@ -156,7 +156,7 @@ function printResumeHint(): void {
writeSync(
1,
chalk.dim(
`\nResume this session with:\nclaude --resume ${resumeArg}\n`,
`\nResume this session with:\ncsc --resume ${resumeArg}\n`,
),
)
resumeHintPrinted = true

View File

@ -36,6 +36,8 @@ export type ModelSetting = ModelName | ModelAlias | null
export function getSmallFastModel(): ModelName {
const provider = getAPIProvider()
// CoStrict has no haiku alias — use the main loop model to stay on the same provider
if (provider === 'costrict') return getMainLoopModel()
// Provider-specific small fast model
if (provider === 'openai' && process.env.OPENAI_SMALL_FAST_MODEL) {
return process.env.OPENAI_SMALL_FAST_MODEL
@ -149,6 +151,8 @@ export function getDefaultOpusModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Sonnet model (3P providers may lag so keep defaults unchanged).
export function getDefaultSonnetModel(): ModelName {
const provider = getAPIProvider()
// CoStrict has no sonnet alias — use the main loop model to stay on the same provider
if (provider === 'costrict') return getMainLoopModel()
// For OpenAI provider, check OPENAI_DEFAULT_SONNET_MODEL first
if (provider === 'openai' && process.env.OPENAI_DEFAULT_SONNET_MODEL) {
return process.env.OPENAI_DEFAULT_SONNET_MODEL
@ -171,6 +175,8 @@ export function getDefaultSonnetModel(): ModelName {
// @[MODEL LAUNCH]: Update the default Haiku model (3P providers may lag so keep defaults unchanged).
export function getDefaultHaikuModel(): ModelName {
const provider = getAPIProvider()
// CoStrict has no haiku alias — use the main loop model to stay on the same provider
if (provider === 'costrict') return getMainLoopModel()
// For OpenAI provider, check OPENAI_DEFAULT_HAIKU_MODEL first
if (provider === 'openai' && process.env.OPENAI_DEFAULT_HAIKU_MODEL) {
return process.env.OPENAI_DEFAULT_HAIKU_MODEL

View File

@ -1,5 +1,4 @@
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { getInitialMainLoopModel } from '../../bootstrap/state.js'
import {
isClaudeAISubscriber,
isMaxSubscriber,
@ -25,7 +24,6 @@ import {
getDefaultHaikuModel,
getDefaultMainLoopModelSetting,
getMarketingNameForModel,
getUserSpecifiedModelSetting,
isOpus1mMergeEnabled,
getOpus46PricingSuffix,
renderDefaultModelSetting,
@ -542,45 +540,31 @@ export function getModelOptions(fastMode = false): ModelOption[] {
}
}
// Add custom model from either the current model value or the initial one
// if it is not already in the options.
let customModel: ModelSetting = null
const currentMainLoopModel = getUserSpecifiedModelSetting()
const initialMainLoopModel = getInitialMainLoopModel()
if (currentMainLoopModel !== undefined && currentMainLoopModel !== null) {
customModel = currentMainLoopModel
} else if (initialMainLoopModel !== null) {
customModel = initialMainLoopModel
}
if (customModel === null || options.some(opt => opt.value === customModel)) {
return filterModelOptionsByAllowlist(options)
} else if (customModel === 'opusplan') {
return filterModelOptionsByAllowlist([...options, getOpusPlanOption()])
} else if (customModel === 'opus' && getAPIProvider() === 'firstParty') {
return filterModelOptionsByAllowlist([
...options,
getMaxOpusOption(fastMode),
])
} else if (customModel === 'opus[1m]' && getAPIProvider() === 'firstParty') {
return filterModelOptionsByAllowlist([
...options,
getMergedOpus1MOption(fastMode),
])
} else {
// Try to show a human-readable label for known Anthropic models, with an
// upgrade hint if the alias now resolves to a newer version.
const knownOption = getKnownModelOption(customModel)
// Add ANTHROPIC_MODEL env var model as a custom option, but only for
// anthropic-compatible providers. settings.model is NOT added here.
const provider = getAPIProvider()
const envModel =
provider === 'firstParty' ||
provider === 'bedrock' ||
provider === 'vertex' ||
provider === 'foundry'
? process.env.ANTHROPIC_MODEL
: undefined
if (envModel && !options.some(opt => opt.value === envModel)) {
const knownOption = getKnownModelOption(envModel)
if (knownOption) {
options.push(knownOption)
} else {
options.push({
value: customModel,
label: customModel,
description: 'Custom model',
value: envModel,
label: envModel,
description: 'Custom model (ANTHROPIC_MODEL)',
})
}
return filterModelOptionsByAllowlist(options)
}
return filterModelOptionsByAllowlist(options)
}
/**

View File

@ -26,9 +26,9 @@ const MAX_RELEASE_NOTES_SHOWN = 5
* 3. Next time the user starts Claude, the cached changelog is available immediately
*/
export const CHANGELOG_URL =
'https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md'
'https://github.com/y574444354/csc/blob/main/CHANGELOG.md'
const RAW_CHANGELOG_URL =
'https://raw.githubusercontent.com/anthropics/claude-code/refs/heads/main/CHANGELOG.md'
'https://raw.githubusercontent.com/y574444354/csc/refs/heads/main/CHANGELOG.md'
/**
* Get the path for the cached changelog file.