Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bin/claude-restored.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bun

// Keep process.cwd() unchanged so each invocation operates on the directory
// where the user launched it, while module resolution stays rooted in this
// installed repository.
await import('../src/dev-entry.ts')
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"description": "Restored Claude Code source tree reconstructed from source maps.",
"license": "SEE LICENSE IN LICENSE.md",
"type": "module",
"bin": {
"claude-restored": "./bin/claude-restored.ts"
},
"packageManager": "bun@1.3.5",
"repository": {
"type": "git",
Expand All @@ -17,7 +20,8 @@
"scripts": {
"dev": "bun run ./src/dev-entry.ts",
"start": "bun run ./src/dev-entry.ts",
"version": "bun run ./src/dev-entry.ts --version"
"version": "bun run ./src/dev-entry.ts --version",
"install:global": "bun install --global ."
},
"dependencies": {
"@alcalzone/ansi-tokenize": "*",
Expand Down
4 changes: 3 additions & 1 deletion src/components/Messages.tsx

Large diffs are not rendered by default.

50 changes: 50 additions & 0 deletions src/components/QuestionHistoryBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import figures from 'figures'
import React, { useMemo, useState } from 'react'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { Box, Text } from '../ink.js'
import type { Message } from '../types/message.js'
import { truncateToWidth } from '../utils/format.js'
import { getContentText, stripPromptXMLTags } from '../utils/messages.js'
import { selectableUserMessagesFilter } from './MessageSelector.js'

type Props = {
messages: Message[]
}

function questionText(message: Message): string | null {
if (!selectableUserMessagesFilter(message)) return null
const content = getContentText(message.message.content)
if (!content) return null
const normalized = stripPromptXMLTags(content).replace(/\s+/g, ' ').trim()
return normalized || null
}

export function QuestionHistoryBar({ messages }: Props): React.ReactNode {
const [expanded, setExpanded] = useState(false)
const { columns } = useTerminalSize()
const questions = useMemo(
() => messages.map(questionText).filter((text): text is string => text !== null).reverse(),
[messages],
)

if (questions.length === 0) return null

const toggle = () => setExpanded(value => !value)
return (
<Box flexDirection="column" flexShrink={0} width="100%">
<Box width="100%" paddingX={1} backgroundColor="gray" onClick={toggle}>
<Text bold>{expanded ? figures.arrowDown : figures.arrowRight} </Text>
<Text>{truncateToWidth(questions[0]!, Math.max(8, columns - 6))}</Text>
</Box>
{expanded && (
<Box flexDirection="column" width="100%" paddingX={2} borderStyle="single" borderTop={false}>
{questions.map((question, index) => (
<Text key={`${index}:${question}`}>
{truncateToWidth(question, Math.max(8, columns - 5))}
</Text>
))}
</Box>
)}
</Box>
)
}
58 changes: 6 additions & 52 deletions src/constants/system.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
// Critical system constants extracted to break circular dependencies

import { feature } from 'bun:bundle'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
import { logForDebugging } from '../utils/debug.js'
import { isEnvDefinedFalsy } from '../utils/envUtils.js'
import { getAPIProvider } from '../utils/model/providers.js'
import { getWorkload } from '../utils/workloadContext.js'

const DEFAULT_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude.`
const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.`
const DEFAULT_PREFIX = `You are Super Cat, Ricky's official CLI for Cat.`
const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Super Cat, Ricky's official CLI for Cat, running within the Cat Agent SDK.`
const AGENT_SDK_PREFIX = `You are a Claude agent, built on Anthropic's Claude Agent SDK.`

const CLI_SYSPROMPT_PREFIX_VALUES = [
Expand Down Expand Up @@ -46,50 +41,9 @@ export function getCLISyspromptPrefix(options?: {
}

/**
* Check if attribution header is enabled.
* Enabled by default, can be disabled via env var or GrowthBook killswitch.
* Billing attribution is intentionally omitted from the system prompt.
* Keep this function so request-building call sites remain compatible.
*/
function isAttributionHeaderEnabled(): boolean {
if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) {
return false
}
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_attribution_header', true)
}

/**
* Get attribution header for API requests.
* Returns a header string with cc_version (including fingerprint) and cc_entrypoint.
* Enabled by default, can be disabled via env var or GrowthBook killswitch.
*
* When NATIVE_CLIENT_ATTESTATION is enabled, includes a `cch=00000` placeholder.
* Before the request is sent, Bun's native HTTP stack finds this placeholder
* in the request body and overwrites the zeros with a computed hash. The
* server verifies this token to confirm the request came from a real Claude
* Code client. See bun-anthropic/src/http/Attestation.zig for implementation.
*
* We use a placeholder (instead of injecting from Zig) because same-length
* replacement avoids Content-Length changes and buffer reallocation.
*/
export function getAttributionHeader(fingerprint: string): string {
if (!isAttributionHeaderEnabled()) {
return ''
}

const version = `${MACRO.VERSION}.${fingerprint}`
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? 'unknown'

// cch=00000 placeholder is overwritten by Bun's HTTP stack with attestation token
const cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : ''
// cc_workload: turn-scoped hint so the API can route e.g. cron-initiated
// requests to a lower QoS pool. Absent = interactive default. Safe re:
// fingerprint (computed from msg chars + version only, line 78 above) and
// cch attestation (placeholder overwritten in serialized body bytes after
// this string is built). Server _parse_cc_header tolerates unknown extra
// fields so old API deploys silently ignore this.
const workload = getWorkload()
const workloadPair = workload ? ` cc_workload=${workload};` : ''
const header = `x-anthropic-billing-header: cc_version=${version}; cc_entrypoint=${entrypoint};${cch}${workloadPair}`

logForDebugging(`attribution header ${header}`)
return header
export function getAttributionHeader(_fingerprint: string): string {
return ''
}
2 changes: 1 addition & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ if ("external" !== 'ant' && isBeingDebugged()) {
// Use process.exit directly here since we're in the top-level code before imports
// and gracefulShutdown is not yet available
// eslint-disable-next-line custom-rules/no-top-level-side-effects
process.exit(1);
// process.exit(1);
}

/**
Expand Down
2 changes: 0 additions & 2 deletions src/services/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
refreshAndGetAwsCredentials,
refreshGcpCredentialsIfNeeded,
} from 'src/utils/auth.js'
import { getUserAgent } from 'src/utils/http.js'
import { getSmallFastModel } from 'src/utils/model/model.js'
import {
getAPIProvider,
Expand Down Expand Up @@ -104,7 +103,6 @@ export async function getAnthropicClient({
const customHeaders = getCustomHeaders()
const defaultHeaders: { [key: string]: string } = {
'x-app': 'cli',
'User-Agent': getUserAgent(),
'X-Claude-Code-Session-Id': getSessionId(),
...customHeaders,
...(containerId ? { 'x-claude-remote-container-id': containerId } : {}),
Expand Down
2 changes: 1 addition & 1 deletion src/tools/BashTool/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ export function getSimplePrompt(): string {
const instructionItems: Array<string | string[]> = [
'If your command will create new directories or files, first use this tool to run `ls` to verify the parent directory exists and is the correct location.',
'Always quote file paths that contain spaces with double quotes in your command (e.g., cd "path with spaces/file.txt")',
'Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.',
'Always maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.',
`You may specify an optional timeout in milliseconds (up to ${getMaxTimeoutMs()}ms / ${getMaxTimeoutMs() / 60000} minutes). By default, your command will timeout after ${getDefaultTimeoutMs()}ms (${getDefaultTimeoutMs() / 60000} minutes).`,
...(backgroundNote !== null ? [backgroundNote] : []),
'When issuing multiple commands:',
Expand Down
7 changes: 3 additions & 4 deletions src/utils/fullscreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,8 @@ export function _resetTmuxControlModeProbeForTesting(): void {
}

/**
* Runtime env-var check only. Ants default to on (CLAUDE_CODE_NO_FLICKER=0
* to opt out); external users default to off (CLAUDE_CODE_NO_FLICKER=1 to
* opt in).
* Runtime env-var check only. Interactive users default to on
* (CLAUDE_CODE_NO_FLICKER=0 to opt out).
*/
export function isFullscreenEnvEnabled(): boolean {
// Explicit user opt-out always wins.
Expand All @@ -125,7 +124,7 @@ export function isFullscreenEnvEnabled(): boolean {
}
return false
}
return process.env.USER_TYPE === 'ant'
return true
}

/**
Expand Down
18 changes: 17 additions & 1 deletion src/utils/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2199,6 +2199,22 @@ export function normalizeMessagesForAPI(
return
}
case 'assistant': {
// Interrupted streams and some Anthropic-compatible providers can
// persist a placeholder thinking block without its required text.
// Re-sending that block makes the API reject the entire history with
// "each thinking block must contain thinking". Drop only malformed
// thinking blocks; valid thinking and redacted_thinking blocks stay.
const validContent = message.message.content.filter(
block =>
block.type !== 'thinking' ||
(typeof block.thinking === 'string' &&
block.thinking.trim().length > 0),
)

// An assistant turn containing only a malformed thinking placeholder
// has no API-visible content after cleanup, so omit the empty turn.
if (validContent.length === 0) return

// Normalize tool inputs for API (strip fields like plan from ExitPlanModeV2)
// When tool search is NOT enabled, we must strip tool_search-specific fields
// like 'caller' from tool_use blocks, as these are only valid with the
Expand All @@ -2208,7 +2224,7 @@ export function normalizeMessagesForAPI(
...message,
message: {
...message.message,
content: message.message.content.map(block => {
content: validContent.map(block => {
if (block.type === 'tool_use') {
const tool = tools.find(t => toolMatchesName(t, block.name))
const normalizedInput = tool
Expand Down