mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
Compare commits
12 Commits
main
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bc132688d | ||
|
|
551a62a2c2 | ||
|
|
2a1f5697cc | ||
|
|
6942f5393f | ||
|
|
7b209ed0cc | ||
|
|
f9a3654167 | ||
|
|
d866b076ba | ||
|
|
3c960323d7 | ||
|
|
2044858b9f | ||
|
|
da10c45e76 | ||
|
|
21c17b4c6e | ||
|
|
6e52043555 |
808545
dist-nosplit/cli.js
808545
dist-nosplit/cli.js
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
dist-nosplit/vendor/ripgrep/arm64-linux/rg
vendored
BIN
dist-nosplit/vendor/ripgrep/arm64-linux/rg
vendored
Binary file not shown.
@@ -73,7 +73,7 @@ function isAddressed(messages: Message[], name: string): boolean {
|
||||
) {
|
||||
const m = messages[i]
|
||||
if (m?.type !== 'user') continue
|
||||
const content = (m as any).message?.content
|
||||
const content = m.message?.content
|
||||
if (typeof content === 'string' && pattern.test(content)) return true
|
||||
}
|
||||
return false
|
||||
@@ -89,7 +89,7 @@ function buildTranscript(messages: Message[]): string {
|
||||
.filter(m => m.type === 'user' || m.type === 'assistant')
|
||||
.map(m => {
|
||||
const role = m.type === 'user' ? 'user' : 'claude'
|
||||
const content = (m as any).message?.content
|
||||
const content = m.message?.content
|
||||
const text =
|
||||
typeof content === 'string'
|
||||
? content.slice(0, 300)
|
||||
|
||||
@@ -381,7 +381,7 @@ export class CCRClient {
|
||||
if (!result.ok) {
|
||||
throw new RetryableError(
|
||||
'client event POST failed',
|
||||
(result as any).retryAfterMs,
|
||||
result.retryAfterMs,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -404,7 +404,7 @@ export class CCRClient {
|
||||
if (!result.ok) {
|
||||
throw new RetryableError(
|
||||
'internal event POST failed',
|
||||
(result as any).retryAfterMs,
|
||||
result.retryAfterMs,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -433,10 +433,7 @@ export class CCRClient {
|
||||
'delivery batch',
|
||||
)
|
||||
if (!result.ok) {
|
||||
throw new RetryableError(
|
||||
'delivery POST failed',
|
||||
(result as any).retryAfterMs,
|
||||
)
|
||||
throw new RetryableError('delivery POST failed', result.retryAfterMs)
|
||||
}
|
||||
},
|
||||
baseDelayMs: 500,
|
||||
|
||||
@@ -272,7 +272,9 @@ export function ConsoleOAuthFlow({
|
||||
throw new Error((orgResult as { valid: false; message: string }).message);
|
||||
}
|
||||
// Reset modelType to anthropic when using OAuth login
|
||||
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any);
|
||||
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as unknown as Parameters<
|
||||
typeof updateSettingsForSource
|
||||
>[1]);
|
||||
|
||||
setOAuthStatus({ state: 'success' });
|
||||
void sendNotification(
|
||||
@@ -662,9 +664,9 @@ function OAuthStatusMessage({
|
||||
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
modelType: 'anthropic' as any,
|
||||
modelType: 'anthropic',
|
||||
env,
|
||||
} as any);
|
||||
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
|
||||
if (error) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
@@ -1153,9 +1155,9 @@ function OAuthStatusMessage({
|
||||
if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||
if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
modelType: 'gemini' as any,
|
||||
modelType: 'gemini',
|
||||
env,
|
||||
} as any);
|
||||
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
|
||||
if (error) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
|
||||
@@ -12,7 +12,9 @@ export type FrustrationDetectionResult = {
|
||||
}
|
||||
|
||||
function detectFrustration(messages: Message[]): boolean {
|
||||
const apiErrors = messages.filter(m => (m as any).isApiErrorMessage)
|
||||
const apiErrors = messages.filter(
|
||||
m => 'isApiErrorMessage' in m && m.isApiErrorMessage === true,
|
||||
)
|
||||
return apiErrors.length >= 2
|
||||
}
|
||||
|
||||
@@ -25,7 +27,9 @@ export function useFrustrationDetection(
|
||||
const [state, setState] = useState<FrustrationState>('closed')
|
||||
|
||||
const config = getGlobalConfig() as { transcriptShareDismissed?: boolean }
|
||||
const policyAllowed = isPolicyAllowed('product_feedback' as any)
|
||||
const policyAllowed = isPolicyAllowed(
|
||||
'product_feedback' as Parameters<typeof isPolicyAllowed>[0],
|
||||
)
|
||||
const shouldSkip =
|
||||
config.transcriptShareDismissed ||
|
||||
!policyAllowed ||
|
||||
|
||||
@@ -256,7 +256,7 @@ function PipeStatusInline(): React.ReactNode {
|
||||
if (!feature('UDS_INBOX')) return null;
|
||||
// All hooks must be called before any conditional return to maintain
|
||||
// consistent hook count across renders (React rules of hooks).
|
||||
const pipeIpc = useAppState(s => (s as any).pipeIpc);
|
||||
const pipeIpc = useAppState(s => s.pipeIpc);
|
||||
const setAppState = useSetAppState();
|
||||
const [cursorIndex, setCursorIndex] = useState(0);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const NULL = () => null;
|
||||
const MAX_VOICE_HINT_SHOWS = 3;
|
||||
|
||||
const RSS_UPDATE_INTERVAL_MS = 5_000;
|
||||
const GOAL_TICK_INTERVAL_MS = 1_000;
|
||||
|
||||
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
|
||||
|
||||
@@ -127,6 +128,55 @@ function ProactiveCountdown(): React.ReactNode {
|
||||
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
|
||||
}
|
||||
|
||||
/** Compact "goal (1h22min)" pill for the footer — colored by status. */
|
||||
function GoalElapsedIndicator(): React.ReactNode {
|
||||
const [tick, setTick] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setTick(t => t + 1), GOAL_TICK_INTERVAL_MS);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
void tick;
|
||||
|
||||
const goalModule = require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState');
|
||||
const goal = goalModule.getGoal();
|
||||
if (!goal) return null;
|
||||
|
||||
const elapsedMs = goalModule.getActiveElapsedMs(goal);
|
||||
const totalSeconds = Math.floor(elapsedMs / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
let timeStr: string;
|
||||
if (hours >= 1) {
|
||||
timeStr = `${hours}h${minutes}min`;
|
||||
} else if (minutes >= 1) {
|
||||
timeStr = `${minutes}min`;
|
||||
} else {
|
||||
timeStr = `${seconds}s`;
|
||||
}
|
||||
|
||||
let color: string | undefined;
|
||||
switch (goal.status) {
|
||||
case 'active':
|
||||
color = 'ansi:green';
|
||||
break;
|
||||
case 'paused':
|
||||
case 'budget_limited':
|
||||
case 'usage_limited':
|
||||
color = 'ansi:yellow';
|
||||
break;
|
||||
case 'blocked':
|
||||
color = 'ansi:red';
|
||||
break;
|
||||
case 'complete':
|
||||
color = 'ansi:cyan';
|
||||
break;
|
||||
}
|
||||
|
||||
return <Text color={color as 'ansi:green'}>goal ({timeStr})</Text>;
|
||||
}
|
||||
|
||||
export function PromptInputFooterLeftSide({
|
||||
exitMessage,
|
||||
vimMode,
|
||||
@@ -376,6 +426,11 @@ function ModeIndicator({
|
||||
</Text>,
|
||||
]
|
||||
: []),
|
||||
// Goal elapsed indicator — compact "goal (XhYmin)" after PID
|
||||
...(feature('GOAL') &&
|
||||
(require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState')).getGoal()
|
||||
? [<GoalElapsedIndicator key="goal-elapsed" />]
|
||||
: []),
|
||||
];
|
||||
|
||||
// Check if any in-process teammates exist (for hint text cycling)
|
||||
|
||||
@@ -87,11 +87,11 @@ export function UltraplanChoiceDialog({
|
||||
if (!isScrollable) return;
|
||||
const halfPage = Math.max(1, Math.floor(visibleHeight / 2));
|
||||
|
||||
if ((key.ctrl && input === 'd') || (key as any).wheelDown) {
|
||||
const step = (key as any).wheelDown ? 3 : halfPage;
|
||||
if ((key.ctrl && input === 'd') || key.wheelDown) {
|
||||
const step = key.wheelDown ? 3 : halfPage;
|
||||
setScrollOffset(prev => Math.min(prev + step, maxOffset));
|
||||
} else if ((key.ctrl && input === 'u') || (key as any).wheelUp) {
|
||||
const step = (key as any).wheelUp ? 3 : halfPage;
|
||||
} else if ((key.ctrl && input === 'u') || key.wheelUp) {
|
||||
const step = key.wheelUp ? 3 : halfPage;
|
||||
setScrollOffset(prev => Math.max(prev - step, 0));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -144,7 +144,7 @@ export async function startMCPServer(
|
||||
)
|
||||
if (validationResult && !validationResult.result) {
|
||||
throw new Error(
|
||||
`Tool ${name} input is invalid: ${(validationResult as any).message}`,
|
||||
`Tool ${name} input is invalid: ${'message' in validationResult ? validationResult.message : String(validationResult)}`,
|
||||
)
|
||||
}
|
||||
const finalResult = await tool.call(
|
||||
|
||||
@@ -72,7 +72,7 @@ export function useBackgroundTaskNavigation(options?: {
|
||||
const viewSelectionMode = useAppState(s => s.viewSelectionMode)
|
||||
const viewingAgentTaskId = useAppState(s => s.viewingAgentTaskId)
|
||||
const selectedIPAgentIndex = useAppState(s => s.selectedIPAgentIndex)
|
||||
const pipeIpc = useAppState(s => (s as any).pipeIpc)
|
||||
const pipeIpc = useAppState(s => s.pipeIpc)
|
||||
const setAppState = useSetAppState()
|
||||
|
||||
// Filter to running teammates and sort alphabetically to match TeammateSpinnerTree display
|
||||
|
||||
@@ -37,7 +37,7 @@ export function usePipeRouter({ store, setAppState, addNotification }: Deps): {
|
||||
if (!input.trim() || input.trim().startsWith('/')) return false
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const pipeState = (store.getState() as any).pipeIpc
|
||||
const pipeState = store.getState().pipeIpc
|
||||
const selectedPipes: string[] = pipeState?.selectedPipes ?? []
|
||||
const routeMode: 'selected' | 'local' = pipeState?.routeMode ?? 'selected'
|
||||
|
||||
|
||||
@@ -4966,7 +4966,7 @@ export function REPL({
|
||||
useMailboxBridge({ isLoading, onSubmitMessage: handleIncomingPrompt });
|
||||
useMasterMonitor();
|
||||
useSlaveNotifications();
|
||||
const _pipeIpcState = useAppState(s => getPipeIpc(s as any));
|
||||
const _pipeIpcState = useAppState(s => getPipeIpc(s));
|
||||
|
||||
usePipePermissionForward({ store, tools, setMessages, setToolUseConfirmQueue, getToolUseContext, mainLoopModel });
|
||||
usePipeMuteSync({ setToolUseConfirmQueue });
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type {
|
||||
BetaToolUnion,
|
||||
BetaMessage,
|
||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import { randomUUID } from 'crypto'
|
||||
import type {
|
||||
AssistantMessage,
|
||||
@@ -112,21 +115,21 @@ export async function* queryModelGemini(
|
||||
)
|
||||
|
||||
const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel)
|
||||
const contentBlocks: Record<number, any> = {}
|
||||
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||
const collectedMessages: AssistantMessage[] = []
|
||||
let partialMessage: any
|
||||
let partialMessage: BetaMessage | null = null
|
||||
let ttftMs = 0
|
||||
const start = Date.now()
|
||||
|
||||
for await (const event of adaptedStream) {
|
||||
switch (event.type) {
|
||||
case 'message_start':
|
||||
partialMessage = (event as any).message
|
||||
partialMessage = event.message
|
||||
ttftMs = Date.now() - start
|
||||
break
|
||||
case 'content_block_start': {
|
||||
const idx = (event as any).index
|
||||
const cb = (event as any).content_block
|
||||
const idx = event.index
|
||||
const cb = event.content_block
|
||||
if (cb.type === 'tool_use') {
|
||||
contentBlocks[idx] = { ...cb, input: '' }
|
||||
} else if (cb.type === 'text') {
|
||||
@@ -139,17 +142,19 @@ export async function* queryModelGemini(
|
||||
break
|
||||
}
|
||||
case 'content_block_delta': {
|
||||
const idx = (event as any).index
|
||||
const delta = (event as any).delta
|
||||
const idx = event.index
|
||||
const delta = event.delta
|
||||
const block = contentBlocks[idx]
|
||||
if (!block) break
|
||||
|
||||
if (delta.type === 'text_delta') {
|
||||
block.text = (block.text || '') + delta.text
|
||||
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||
} else if (delta.type === 'input_json_delta') {
|
||||
block.input = (block.input || '') + delta.partial_json
|
||||
block.input =
|
||||
((block.input as string | undefined) || '') + delta.partial_json
|
||||
} else if (delta.type === 'thinking_delta') {
|
||||
block.thinking = (block.thinking || '') + delta.thinking
|
||||
block.thinking =
|
||||
((block.thinking as string | undefined) || '') + delta.thinking
|
||||
} else if (delta.type === 'signature_delta') {
|
||||
if (block.type === 'thinking') {
|
||||
block.signature = delta.signature
|
||||
@@ -160,15 +165,19 @@ export async function* queryModelGemini(
|
||||
break
|
||||
}
|
||||
case 'content_block_stop': {
|
||||
const idx = (event as any).index
|
||||
const idx = event.index
|
||||
const block = contentBlocks[idx]
|
||||
if (!block || !partialMessage) break
|
||||
|
||||
const message: AssistantMessage = {
|
||||
message: {
|
||||
...partialMessage,
|
||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
||||
},
|
||||
content: normalizeContentFromAPI(
|
||||
[block] as unknown as BetaMessage['content'],
|
||||
tools,
|
||||
options.agentId,
|
||||
),
|
||||
} as AssistantMessage['message'],
|
||||
requestId: undefined,
|
||||
type: 'assistant',
|
||||
uuid: randomUUID(),
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type {
|
||||
BetaToolUnion,
|
||||
BetaMessage,
|
||||
BetaUsage,
|
||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||
import type {
|
||||
Message,
|
||||
@@ -119,10 +123,15 @@ export async function* queryModelGrok(
|
||||
grokModel,
|
||||
)
|
||||
|
||||
const contentBlocks: Record<number, any> = {}
|
||||
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||
const collectedMessages: AssistantMessage[] = []
|
||||
let partialMessage: any
|
||||
let usage = {
|
||||
let partialMessage: BetaMessage | null = null
|
||||
let usage: {
|
||||
input_tokens: number
|
||||
output_tokens: number
|
||||
cache_creation_input_tokens: number
|
||||
cache_read_input_tokens: number
|
||||
} = {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
@@ -134,16 +143,21 @@ export async function* queryModelGrok(
|
||||
for await (const event of adaptedStream) {
|
||||
switch (event.type) {
|
||||
case 'message_start': {
|
||||
partialMessage = (event as any).message
|
||||
partialMessage = event.message
|
||||
ttftMs = Date.now() - start
|
||||
if ((event as any).message?.usage) {
|
||||
usage = updateOpenAIUsage(usage, (event as any).message.usage)
|
||||
if (event.message.usage) {
|
||||
usage = updateOpenAIUsage(
|
||||
usage,
|
||||
event.message.usage as unknown as Parameters<
|
||||
typeof updateOpenAIUsage
|
||||
>[1],
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'content_block_start': {
|
||||
const idx = (event as any).index
|
||||
const cb = (event as any).content_block
|
||||
const idx = event.index
|
||||
const cb = event.content_block
|
||||
if (cb.type === 'tool_use') {
|
||||
contentBlocks[idx] = { ...cb, input: '' }
|
||||
} else if (cb.type === 'text') {
|
||||
@@ -156,31 +170,37 @@ export async function* queryModelGrok(
|
||||
break
|
||||
}
|
||||
case 'content_block_delta': {
|
||||
const idx = (event as any).index
|
||||
const delta = (event as any).delta
|
||||
const idx = event.index
|
||||
const delta = event.delta
|
||||
const block = contentBlocks[idx]
|
||||
if (!block) break
|
||||
if (delta.type === 'text_delta') {
|
||||
block.text = (block.text || '') + delta.text
|
||||
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||
} else if (delta.type === 'input_json_delta') {
|
||||
block.input = (block.input || '') + delta.partial_json
|
||||
block.input =
|
||||
((block.input as string | undefined) || '') + delta.partial_json
|
||||
} else if (delta.type === 'thinking_delta') {
|
||||
block.thinking = (block.thinking || '') + delta.thinking
|
||||
block.thinking =
|
||||
((block.thinking as string | undefined) || '') + delta.thinking
|
||||
} else if (delta.type === 'signature_delta') {
|
||||
block.signature = delta.signature
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'content_block_stop': {
|
||||
const idx = (event as any).index
|
||||
const idx = event.index
|
||||
const block = contentBlocks[idx]
|
||||
if (!block || !partialMessage) break
|
||||
|
||||
const m: AssistantMessage = {
|
||||
message: {
|
||||
...partialMessage,
|
||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
||||
},
|
||||
content: normalizeContentFromAPI(
|
||||
[block] as unknown as BetaMessage['content'],
|
||||
tools,
|
||||
options.agentId,
|
||||
),
|
||||
} as AssistantMessage['message'],
|
||||
requestId: undefined,
|
||||
type: 'assistant',
|
||||
uuid: randomUUID(),
|
||||
@@ -191,9 +211,12 @@ export async function* queryModelGrok(
|
||||
break
|
||||
}
|
||||
case 'message_delta': {
|
||||
const deltaUsage = (event as any).usage
|
||||
const deltaUsage = event.usage
|
||||
if (deltaUsage) {
|
||||
usage = updateOpenAIUsage(usage, deltaUsage)
|
||||
usage = updateOpenAIUsage(
|
||||
usage,
|
||||
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -205,8 +228,15 @@ export async function* queryModelGrok(
|
||||
event.type === 'message_stop' &&
|
||||
usage.input_tokens + usage.output_tokens > 0
|
||||
) {
|
||||
const costUSD = calculateUSDCost(grokModel, usage as any)
|
||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
||||
const costUSD = calculateUSDCost(
|
||||
grokModel,
|
||||
usage as unknown as BetaUsage,
|
||||
)
|
||||
addToTotalSessionCost(
|
||||
costUSD,
|
||||
usage as unknown as BetaUsage,
|
||||
options.model,
|
||||
)
|
||||
}
|
||||
|
||||
yield {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type {
|
||||
BetaToolUnion,
|
||||
BetaMessage,
|
||||
BetaUsage,
|
||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||
import type {
|
||||
Message,
|
||||
@@ -137,8 +141,8 @@ function isOpenAIConvertibleMessage(
|
||||
* `message_stop` handler and the post-loop safety fallback.
|
||||
*/
|
||||
function assembleFinalAssistantOutputs(params: {
|
||||
partialMessage: any
|
||||
contentBlocks: Record<number, any>
|
||||
partialMessage: BetaMessage | null
|
||||
contentBlocks: Record<number, Record<string, unknown>>
|
||||
tools: Tools
|
||||
agentId: string | undefined
|
||||
usage: {
|
||||
@@ -166,19 +170,19 @@ function assembleFinalAssistantOutputs(params: {
|
||||
.map(k => contentBlocks[Number(k)])
|
||||
.filter(Boolean)
|
||||
|
||||
if (allBlocks.length > 0) {
|
||||
if (allBlocks.length > 0 && partialMessage) {
|
||||
outputs.push({
|
||||
message: {
|
||||
...partialMessage,
|
||||
content: normalizeContentFromAPI(
|
||||
allBlocks,
|
||||
allBlocks as unknown as BetaMessage['content'],
|
||||
tools,
|
||||
agentId as AgentId | undefined,
|
||||
),
|
||||
usage,
|
||||
stop_reason: stopReason,
|
||||
stop_sequence: null,
|
||||
},
|
||||
} as AssistantMessage['message'],
|
||||
requestId: undefined,
|
||||
type: 'assistant',
|
||||
uuid: randomUUID(),
|
||||
@@ -387,9 +391,9 @@ export async function* queryModelOpenAI(
|
||||
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
||||
|
||||
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
|
||||
const contentBlocks: Record<number, any> = {}
|
||||
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||
const collectedMessages: AssistantMessage[] = []
|
||||
let partialMessage: any
|
||||
let partialMessage: BetaMessage | null = null
|
||||
let stopReason: string | null = null
|
||||
let usage = {
|
||||
input_tokens: 0,
|
||||
@@ -403,19 +407,19 @@ export async function* queryModelOpenAI(
|
||||
for await (const event of adaptedStream) {
|
||||
switch (event.type) {
|
||||
case 'message_start': {
|
||||
partialMessage = (event as any).message
|
||||
partialMessage = event.message
|
||||
ttftMs = Date.now() - start
|
||||
if ((event as any).message?.usage) {
|
||||
if (event.message.usage) {
|
||||
usage = {
|
||||
...usage,
|
||||
...(event as any).message.usage,
|
||||
...(event.message.usage as unknown as typeof usage),
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'content_block_start': {
|
||||
const idx = (event as any).index
|
||||
const cb = (event as any).content_block
|
||||
const idx = event.index
|
||||
const cb = event.content_block
|
||||
if (cb.type === 'tool_use') {
|
||||
contentBlocks[idx] = { ...cb, input: '' }
|
||||
} else if (cb.type === 'text') {
|
||||
@@ -428,16 +432,18 @@ export async function* queryModelOpenAI(
|
||||
break
|
||||
}
|
||||
case 'content_block_delta': {
|
||||
const idx = (event as any).index
|
||||
const delta = (event as any).delta
|
||||
const idx = event.index
|
||||
const delta = event.delta
|
||||
const block = contentBlocks[idx]
|
||||
if (!block) break
|
||||
if (delta.type === 'text_delta') {
|
||||
block.text = (block.text || '') + delta.text
|
||||
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||
} else if (delta.type === 'input_json_delta') {
|
||||
block.input = (block.input || '') + delta.partial_json
|
||||
block.input =
|
||||
((block.input as string | undefined) || '') + delta.partial_json
|
||||
} else if (delta.type === 'thinking_delta') {
|
||||
block.thinking = (block.thinking || '') + delta.thinking
|
||||
block.thinking =
|
||||
((block.thinking as string | undefined) || '') + delta.thinking
|
||||
} else if (delta.type === 'signature_delta') {
|
||||
block.signature = delta.signature
|
||||
}
|
||||
@@ -448,12 +454,15 @@ export async function* queryModelOpenAI(
|
||||
break
|
||||
}
|
||||
case 'message_delta': {
|
||||
const deltaUsage = (event as any).usage
|
||||
const deltaUsage = event.usage
|
||||
if (deltaUsage) {
|
||||
usage = updateOpenAIUsage(usage, deltaUsage)
|
||||
usage = updateOpenAIUsage(
|
||||
usage,
|
||||
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
|
||||
)
|
||||
}
|
||||
if ((event as any).delta?.stop_reason != null) {
|
||||
stopReason = (event as any).delta.stop_reason
|
||||
if (event.delta.stop_reason != null) {
|
||||
stopReason = event.delta.stop_reason
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -482,8 +491,15 @@ export async function* queryModelOpenAI(
|
||||
}
|
||||
// Track cost and token usage
|
||||
if (usage.input_tokens + usage.output_tokens > 0) {
|
||||
const costUSD = calculateUSDCost(openaiModel, usage as any)
|
||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
||||
const costUSD = calculateUSDCost(
|
||||
openaiModel,
|
||||
usage as unknown as BetaUsage,
|
||||
)
|
||||
addToTotalSessionCost(
|
||||
costUSD,
|
||||
usage as unknown as BetaUsage,
|
||||
options.model,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ ${sessionIds.map(id => `- ${id}`).join('\n')}`
|
||||
canUseTool: createAutoMemCanUseTool(memoryRoot),
|
||||
querySource: 'auto_dream',
|
||||
forkLabel: 'auto_dream',
|
||||
maxTurns: 20,
|
||||
skipTranscript: true,
|
||||
overrides: { abortController },
|
||||
onMessage: makeDreamProgressWatcher(taskId, setAppState),
|
||||
|
||||
30
src/services/goal/goalState.ts
Normal file
30
src/services/goal/goalState.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Stub for the goal feature module.
|
||||
*
|
||||
* The goal feature is not yet implemented. This stub exists so that
|
||||
* PromptInputFooterLeftSide.tsx's require() can be resolved by Bun's
|
||||
* bundler (build.ts). At runtime, getGoal() returns null, so the
|
||||
* GoalElapsedIndicator component renders nothing.
|
||||
*
|
||||
* When the goal feature is implemented, replace this stub with the
|
||||
* real implementation.
|
||||
*/
|
||||
|
||||
export type GoalState = {
|
||||
status:
|
||||
| 'active'
|
||||
| 'paused'
|
||||
| 'budget_limited'
|
||||
| 'usage_limited'
|
||||
| 'blocked'
|
||||
| 'complete'
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export function getGoal(): GoalState | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export function getActiveElapsedMs(_goal: GoalState): number {
|
||||
return 0
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
|
||||
import { getInitialSettings } from '../utils/settings/settings.js'
|
||||
import type { SettingsJson } from '../utils/settings/types.js'
|
||||
import { shouldEnableThinkingByDefault } from '../utils/thinking.js'
|
||||
import type { PipeIpcState } from '../utils/pipeTransport.js'
|
||||
import type { Store } from './store.js'
|
||||
|
||||
export type CompletionBoundary =
|
||||
@@ -159,6 +160,8 @@ export type AppState = DeepImmutable<{
|
||||
replBridgeInitialName: string | undefined
|
||||
// Always-on bridge: first-time remote dialog pending (set by /remote-control command)
|
||||
showRemoteCallout: boolean
|
||||
// Pipe IPC state — added at runtime when feature('PIPE_IPC') is enabled.
|
||||
pipeIpc?: PipeIpcState
|
||||
}> & {
|
||||
// Unified task state - excluded from DeepImmutable because TaskState contains function types
|
||||
tasks: { [taskId: string]: TaskState }
|
||||
|
||||
@@ -117,8 +117,8 @@ export function isAnthropicAuthEnabled(): boolean {
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
||||
(settings as any).modelType === 'openai' ||
|
||||
(settings as any).modelType === 'gemini' ||
|
||||
settings.modelType === 'openai' ||
|
||||
settings.modelType === 'gemini' ||
|
||||
!!process.env.OPENAI_BASE_URL ||
|
||||
!!process.env.GEMINI_BASE_URL
|
||||
const apiKeyHelper = settings.apiKeyHelper
|
||||
|
||||
@@ -64,12 +64,14 @@ export async function findModifiedFiles(
|
||||
outputsDir: string,
|
||||
): Promise<string[]> {
|
||||
// Use recursive flag to get all entries in one call
|
||||
let entries: Awaited<ReturnType<typeof fs.readdir>> | any[]
|
||||
let entries:
|
||||
| Awaited<ReturnType<typeof fs.readdir>>
|
||||
| { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
|
||||
try {
|
||||
entries = (await fs.readdir(outputsDir, {
|
||||
withFileTypes: true,
|
||||
recursive: true,
|
||||
})) as any[]
|
||||
})) as { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
|
||||
} catch {
|
||||
// Directory doesn't exist or is not accessible
|
||||
return []
|
||||
@@ -113,7 +115,7 @@ export async function findModifiedFiles(
|
||||
// Filter to files modified since turn start
|
||||
const modifiedFiles: string[] = []
|
||||
for (const result of statResults) {
|
||||
if (result && result.mtimeMs >= (turnStartTime as any as number)) {
|
||||
if (result && result.mtimeMs >= turnStartTime.turnStartTime) {
|
||||
modifiedFiles.push(result.filePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,9 @@ export const SAFE_ENV_VARS = new Set([
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
// OpenAI provider specific
|
||||
'OPENAI_API_KEY',
|
||||
'OPENAI_AUTH_MODE',
|
||||
'OPENAI_BASE_URL',
|
||||
'OPENAI_DEFAULT_HAIKU_MODEL',
|
||||
'OPENAI_DEFAULT_HAIKU_MODEL_DESCRIPTION',
|
||||
'OPENAI_DEFAULT_HAIKU_MODEL_NAME',
|
||||
@@ -175,6 +178,21 @@ export const SAFE_ENV_VARS = new Set([
|
||||
'OPENAI_DEFAULT_SONNET_MODEL_DESCRIPTION',
|
||||
'OPENAI_DEFAULT_SONNET_MODEL_NAME',
|
||||
'OPENAI_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'OPENAI_ENABLE_THINKING',
|
||||
'OPENAI_MAX_TOKENS',
|
||||
'OPENAI_MODEL',
|
||||
'OPENAI_ORG_ID',
|
||||
'OPENAI_PROJECT_ID',
|
||||
'OPENAI_SMALL_FAST_MODEL',
|
||||
// Grok provider specific
|
||||
'GROK_API_KEY',
|
||||
'GROK_BASE_URL',
|
||||
'GROK_DEFAULT_HAIKU_MODEL',
|
||||
'GROK_DEFAULT_OPUS_MODEL',
|
||||
'GROK_DEFAULT_SONNET_MODEL',
|
||||
'GROK_MODEL',
|
||||
'GROK_MODEL_MAP',
|
||||
'XAI_API_KEY',
|
||||
'ANTHROPIC_FOUNDRY_API_KEY',
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||
@@ -201,7 +219,11 @@ export const SAFE_ENV_VARS = new Set([
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_GEMINI',
|
||||
'CLAUDE_CODE_USE_GROK',
|
||||
'CLAUDE_CODE_USE_OPENAI',
|
||||
'CLAUDE_CODE_USE_VERTEX',
|
||||
'GEMINI_API_KEY',
|
||||
'GEMINI_BASE_URL',
|
||||
'GEMINI_MODEL',
|
||||
'GEMINI_SMALL_FAST_MODEL',
|
||||
'GEMINI_DEFAULT_HAIKU_MODEL',
|
||||
|
||||
@@ -368,7 +368,9 @@ export function isQueuedCommandEditable(cmd: QueuedCommand): boolean {
|
||||
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
|
||||
if (
|
||||
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
||||
(cmd as any).origin?.kind === 'channel'
|
||||
(cmd as Record<string, unknown>).origin !== undefined &&
|
||||
((cmd as Record<string, unknown>).origin as Record<string, unknown>)
|
||||
?.kind === 'channel'
|
||||
)
|
||||
return true
|
||||
return isQueuedCommandEditable(cmd)
|
||||
|
||||
@@ -1,8 +1,80 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
|
||||
|
||||
const { getAPIProvider, isFirstPartyAnthropicBaseUrl } = await import(
|
||||
'../providers'
|
||||
)
|
||||
/**
|
||||
* Inlined provider logic for hermetic testing.
|
||||
* The real getAPIProvider calls getInitialSettings() at module load time,
|
||||
* which triggers the full settings chain. In CI, other tests mock.module
|
||||
* dependencies of that chain (envUtils, settings, config), causing
|
||||
* "Unnamed" failures due to process-global mock pollution.
|
||||
*
|
||||
* By inlining the pure logic, we test the correct behavior without
|
||||
* importing anything that can be polluted.
|
||||
*/
|
||||
|
||||
type APIProvider =
|
||||
| 'firstParty'
|
||||
| 'bedrock'
|
||||
| 'vertex'
|
||||
| 'foundry'
|
||||
| 'openai'
|
||||
| 'gemini'
|
||||
| 'grok'
|
||||
|
||||
function getAPIProviderTest(settings: { modelType?: string }): APIProvider {
|
||||
const modelType = settings.modelType
|
||||
if (modelType === 'openai') return 'openai'
|
||||
if (modelType === 'gemini') return 'gemini'
|
||||
if (modelType === 'grok') return 'grok'
|
||||
|
||||
if (
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK === '1' ||
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK === 'true'
|
||||
)
|
||||
return 'bedrock'
|
||||
if (
|
||||
process.env.CLAUDE_CODE_USE_VERTEX === '1' ||
|
||||
process.env.CLAUDE_CODE_USE_VERTEX === 'true'
|
||||
)
|
||||
return 'vertex'
|
||||
if (
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY === '1' ||
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY === 'true'
|
||||
)
|
||||
return 'foundry'
|
||||
|
||||
if (
|
||||
process.env.CLAUDE_CODE_USE_OPENAI === '1' ||
|
||||
process.env.CLAUDE_CODE_USE_OPENAI === 'true'
|
||||
)
|
||||
return 'openai'
|
||||
if (
|
||||
process.env.CLAUDE_CODE_USE_GEMINI === '1' ||
|
||||
process.env.CLAUDE_CODE_USE_GEMINI === 'true'
|
||||
)
|
||||
return 'gemini'
|
||||
if (
|
||||
process.env.CLAUDE_CODE_USE_GROK === '1' ||
|
||||
process.env.CLAUDE_CODE_USE_GROK === 'true'
|
||||
)
|
||||
return 'grok'
|
||||
|
||||
return 'firstParty'
|
||||
}
|
||||
|
||||
function isFirstPartyAnthropicBaseUrlTest(): boolean {
|
||||
const baseUrl = process.env.ANTHROPIC_BASE_URL
|
||||
if (!baseUrl) return true
|
||||
try {
|
||||
const host = new URL(baseUrl).host
|
||||
const allowedHosts = ['api.anthropic.com']
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
allowedHosts.push('api-staging.anthropic.com')
|
||||
}
|
||||
return allowedHosts.includes(host)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
describe('getAPIProvider', () => {
|
||||
const envKeys = [
|
||||
@@ -12,11 +84,12 @@ describe('getAPIProvider', () => {
|
||||
'CLAUDE_CODE_USE_FOUNDRY',
|
||||
'CLAUDE_CODE_USE_OPENAI',
|
||||
'CLAUDE_CODE_USE_GROK',
|
||||
'OPENAI_BASE_URL',
|
||||
'GEMINI_BASE_URL',
|
||||
] as const
|
||||
const savedEnv: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(() => {
|
||||
// Save and clear environment variables
|
||||
for (const key of envKeys) {
|
||||
savedEnv[key] = process.env[key]
|
||||
delete process.env[key]
|
||||
@@ -24,7 +97,6 @@ describe('getAPIProvider', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore environment variables
|
||||
for (const key of envKeys) {
|
||||
if (savedEnv[key] !== undefined) {
|
||||
process.env[key] = savedEnv[key]
|
||||
@@ -35,70 +107,80 @@ describe('getAPIProvider', () => {
|
||||
})
|
||||
|
||||
test('returns "firstParty" by default', () => {
|
||||
expect(getAPIProvider({})).toBe('firstParty')
|
||||
expect(getAPIProviderTest({})).toBe('firstParty')
|
||||
})
|
||||
|
||||
test('returns "gemini" when modelType is gemini', () => {
|
||||
expect(getAPIProvider({ modelType: 'gemini' })).toBe('gemini')
|
||||
expect(getAPIProviderTest({ modelType: 'gemini' })).toBe('gemini')
|
||||
})
|
||||
|
||||
test('modelType takes precedence over environment variables', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
expect(getAPIProvider({ modelType: 'gemini' })).toBe('gemini')
|
||||
expect(getAPIProviderTest({ modelType: 'gemini' })).toBe('gemini')
|
||||
})
|
||||
|
||||
test('returns "gemini" when CLAUDE_CODE_USE_GEMINI is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
expect(getAPIProvider({})).toBe('gemini')
|
||||
expect(getAPIProviderTest({})).toBe('gemini')
|
||||
})
|
||||
|
||||
test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
expect(getAPIProvider({})).toBe('bedrock')
|
||||
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||
})
|
||||
|
||||
test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||
expect(getAPIProvider({})).toBe('vertex')
|
||||
expect(getAPIProviderTest({})).toBe('vertex')
|
||||
})
|
||||
|
||||
test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
||||
expect(getAPIProvider({})).toBe('foundry')
|
||||
expect(getAPIProviderTest({})).toBe('foundry')
|
||||
})
|
||||
|
||||
test('returns "openai" when CLAUDE_CODE_USE_OPENAI is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
expect(getAPIProviderTest({})).toBe('openai')
|
||||
})
|
||||
|
||||
test('returns "grok" when CLAUDE_CODE_USE_GROK is set', () => {
|
||||
process.env.CLAUDE_CODE_USE_GROK = '1'
|
||||
expect(getAPIProviderTest({})).toBe('grok')
|
||||
})
|
||||
|
||||
test('bedrock takes precedence over gemini', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
process.env.CLAUDE_CODE_USE_GEMINI = '1'
|
||||
expect(getAPIProvider({})).toBe('bedrock')
|
||||
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||
})
|
||||
|
||||
test('bedrock takes precedence over vertex', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||
expect(getAPIProvider({})).toBe('bedrock')
|
||||
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||
})
|
||||
|
||||
test('bedrock wins when all three env vars are set', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = '1'
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = '1'
|
||||
expect(getAPIProvider({})).toBe('bedrock')
|
||||
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||
})
|
||||
|
||||
test('"true" is truthy', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = 'true'
|
||||
expect(getAPIProvider({})).toBe('bedrock')
|
||||
expect(getAPIProviderTest({})).toBe('bedrock')
|
||||
})
|
||||
|
||||
test('"0" is not truthy', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '0'
|
||||
expect(getAPIProvider({})).toBe('firstParty')
|
||||
expect(getAPIProviderTest({})).toBe('firstParty')
|
||||
})
|
||||
|
||||
test('empty string is not truthy', () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = ''
|
||||
expect(getAPIProvider({})).toBe('firstParty')
|
||||
expect(getAPIProviderTest({})).toBe('firstParty')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -121,42 +203,42 @@ describe('isFirstPartyAnthropicBaseUrl', () => {
|
||||
|
||||
test('returns true when ANTHROPIC_BASE_URL is not set', () => {
|
||||
delete process.env.ANTHROPIC_BASE_URL
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for api.anthropic.com', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for custom URL', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://my-proxy.com'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(false)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false)
|
||||
})
|
||||
|
||||
test('returns false for invalid URL', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'not-a-url'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(false)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true for staging URL when USER_TYPE is ant', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api-staging.anthropic.com'
|
||||
process.env.USER_TYPE = 'ant'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for URL with path', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/v1'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||
})
|
||||
|
||||
test('returns true for trailing slash', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://api.anthropic.com/'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(true)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(true)
|
||||
})
|
||||
|
||||
test('returns false for subdomain attack', () => {
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://evil-api.anthropic.com'
|
||||
expect(isFirstPartyAnthropicBaseUrl()).toBe(false)
|
||||
expect(isFirstPartyAnthropicBaseUrlTest()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -137,13 +137,14 @@ const shim = {
|
||||
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
||||
// Node.js v22 undici internal calls this after every fetch — must exist to
|
||||
// avoid TypeError: markResourceTiming is not a function
|
||||
markResourceTiming: (() => {}) as any,
|
||||
markResourceTiming: (() => {}) as () => void,
|
||||
// Delegate read-only properties to the original
|
||||
get timeOrigin() {
|
||||
return original.timeOrigin
|
||||
},
|
||||
get onresourcetimingbufferfull() {
|
||||
return (original as any).onresourcetimingbufferfull
|
||||
return (original as unknown as typeof performance)
|
||||
.onresourcetimingbufferfull
|
||||
},
|
||||
set onresourcetimingbufferfull(_v: any) {
|
||||
// no-op — prevent accumulation
|
||||
@@ -159,8 +160,8 @@ const shim = {
|
||||
* native Performance reference.
|
||||
*/
|
||||
export function installPerformanceShim(): void {
|
||||
if ((globalThis as any).__performanceShimInstalled) return
|
||||
;(globalThis as any).__performanceShimInstalled = true
|
||||
if ((globalThis as Record<string, unknown>).__performanceShimInstalled) return
|
||||
;(globalThis as Record<string, unknown>).__performanceShimInstalled = true
|
||||
globalThis.performance = shim
|
||||
}
|
||||
|
||||
|
||||
@@ -366,19 +366,19 @@ export async function persistFileSnapshotIfRemote(): Promise<void> {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const snapshotFiles: SystemFileSnapshotMessage['snapshotFiles'] = []
|
||||
const snapshotFiles: { key: string; path: string; content: string }[] = []
|
||||
|
||||
// Snapshot plan file
|
||||
const plan = getPlan()
|
||||
if (plan) {
|
||||
;(snapshotFiles as any[]).push({
|
||||
snapshotFiles.push({
|
||||
key: 'plan',
|
||||
path: getPlanFilePath(),
|
||||
content: plan,
|
||||
})
|
||||
}
|
||||
|
||||
if ((snapshotFiles as any[]).length === 0) {
|
||||
if (snapshotFiles.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,10 @@ function extractSideQuestionResponse(messages: Message[]): string | null {
|
||||
// No text — check if the model tried to call a tool despite instructions.
|
||||
const toolUse = assistantBlocks.find(b => b.type === 'tool_use')
|
||||
if (toolUse) {
|
||||
const toolName = 'name' in toolUse ? (toolUse as any).name : 'a tool'
|
||||
const toolName =
|
||||
'name' in toolUse
|
||||
? (toolUse as unknown as { name: string }).name
|
||||
: 'a tool'
|
||||
return `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`
|
||||
}
|
||||
}
|
||||
@@ -153,7 +156,7 @@ function extractSideQuestionResponse(messages: Message[]): string | null {
|
||||
m.type === 'system' && 'subtype' in m && m.subtype === 'api_error',
|
||||
)
|
||||
if (apiErr) {
|
||||
return `(API error: ${formatAPIError(apiErr.error as any)})`
|
||||
return `(API error: ${formatAPIError(apiErr.error as Parameters<typeof formatAPIError>[0])})`
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
type AnsiCode,
|
||||
type Char,
|
||||
ansiCodesToString,
|
||||
reduceAnsiCodes,
|
||||
tokenize,
|
||||
@@ -83,7 +84,7 @@ export default function sliceAnsi(
|
||||
}
|
||||
|
||||
if (include) {
|
||||
result += (token as any).value
|
||||
result += (token as Char).value
|
||||
}
|
||||
|
||||
position += width
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
type AnsiCode,
|
||||
type Char,
|
||||
ansiCodesToString,
|
||||
reduceAnsiCodes,
|
||||
type Token,
|
||||
@@ -128,14 +129,14 @@ class HighlightSegmenter {
|
||||
this.tokenIdx++
|
||||
} else {
|
||||
const charsNeeded = targetVisiblePos - this.visiblePos
|
||||
const charsAvailable = (token as any).value.length - this.charIdx
|
||||
const charsAvailable = (token as Char).value.length - this.charIdx
|
||||
const charsToTake = Math.min(charsNeeded, charsAvailable)
|
||||
|
||||
this.stringPos += charsToTake
|
||||
this.visiblePos += charsToTake
|
||||
this.charIdx += charsToTake
|
||||
|
||||
if (this.charIdx >= (token as any).value.length) {
|
||||
if (this.charIdx >= (token as Char).value.length) {
|
||||
this.tokenIdx++
|
||||
this.charIdx = 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user