@golba98/codexa 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -0
- package/bin/codexa.js +445 -0
- package/package.json +45 -0
- package/scripts/audit-codexa-capabilities.mjs +466 -0
- package/scripts/smoke-terminal-bench.mjs +35 -0
- package/src/app.tsx +4561 -0
- package/src/appRenderStability.test.ts +131 -0
- package/src/commands/handler.test.ts +643 -0
- package/src/commands/handler.ts +875 -0
- package/src/config/launchArgs.test.ts +158 -0
- package/src/config/launchArgs.ts +186 -0
- package/src/config/layeredConfig.test.ts +143 -0
- package/src/config/layeredConfig.ts +836 -0
- package/src/config/persistence.test.ts +110 -0
- package/src/config/persistence.ts +311 -0
- package/src/config/runtimeConfig.test.ts +218 -0
- package/src/config/runtimeConfig.ts +554 -0
- package/src/config/settings.test.ts +155 -0
- package/src/config/settings.ts +401 -0
- package/src/config/toml-serialize.ts +98 -0
- package/src/config/trustStore.test.ts +29 -0
- package/src/config/trustStore.ts +68 -0
- package/src/core/attachments.test.ts +155 -0
- package/src/core/attachments.ts +71 -0
- package/src/core/auth/codexAuth.test.ts +68 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/cleanupFastFail.test.ts +76 -0
- package/src/core/cleanupFastFail.ts +67 -0
- package/src/core/clipboard.ts +24 -0
- package/src/core/codex.ts +124 -0
- package/src/core/codexExecArgs.test.ts +195 -0
- package/src/core/codexExecArgs.ts +152 -0
- package/src/core/codexLaunch.test.ts +205 -0
- package/src/core/codexLaunch.ts +162 -0
- package/src/core/codexPrompt.test.ts +252 -0
- package/src/core/codexPrompt.ts +428 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.test.ts +212 -0
- package/src/core/executables/codexExecutable.ts +159 -0
- package/src/core/executables/executableResolver.test.ts +129 -0
- package/src/core/executables/executableResolver.ts +138 -0
- package/src/core/executables/geminiExecutable.test.ts +116 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/executables/pathSanityScan.test.ts +47 -0
- package/src/core/githubDiagnostics.test.ts +92 -0
- package/src/core/githubDiagnostics.ts +222 -0
- package/src/core/hollowResponseFormat.test.ts +58 -0
- package/src/core/hollowResponseFormat.ts +39 -0
- package/src/core/inputDebug.ts +51 -0
- package/src/core/launchContext.test.ts +157 -0
- package/src/core/launchContext.ts +266 -0
- package/src/core/models/codexCapabilities.test.ts +45 -0
- package/src/core/models/codexCapabilities.ts +95 -0
- package/src/core/models/codexModelCapabilities.test.ts +246 -0
- package/src/core/models/codexModelCapabilities.ts +571 -0
- package/src/core/models/modelSpecs.test.ts +283 -0
- package/src/core/models/modelSpecs.ts +300 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.test.ts +230 -0
- package/src/core/perf/renderDebug.ts +373 -0
- package/src/core/planStorage.test.ts +143 -0
- package/src/core/planStorage.ts +141 -0
- package/src/core/process/CommandRunner.test.ts +105 -0
- package/src/core/process/CommandRunner.ts +269 -0
- package/src/core/process/processValidation.ts +101 -0
- package/src/core/projectInstructions.test.ts +50 -0
- package/src/core/projectInstructions.ts +54 -0
- package/src/core/providerLauncher/launcher.test.ts +238 -0
- package/src/core/providerLauncher/launcher.ts +203 -0
- package/src/core/providerLauncher/registry.test.ts +324 -0
- package/src/core/providerLauncher/registry.ts +253 -0
- package/src/core/providerLauncher/types.ts +84 -0
- package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
- package/src/core/providerLauncher/workspaceConfig.ts +407 -0
- package/src/core/providerRuntime/anthropic.test.ts +1120 -0
- package/src/core/providerRuntime/anthropic.ts +576 -0
- package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
- package/src/core/providerRuntime/capabilityProfile.ts +288 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
- package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
- package/src/core/providerRuntime/contextMetadata.ts +409 -0
- package/src/core/providerRuntime/gemini.test.ts +437 -0
- package/src/core/providerRuntime/gemini.ts +784 -0
- package/src/core/providerRuntime/lmstudio.test.ts +168 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.test.ts +787 -0
- package/src/core/providerRuntime/local.ts +754 -0
- package/src/core/providerRuntime/models.ts +150 -0
- package/src/core/providerRuntime/reasoning.ts +17 -0
- package/src/core/providerRuntime/registry.test.ts +233 -0
- package/src/core/providerRuntime/registry.ts +203 -0
- package/src/core/providerRuntime/types.ts +103 -0
- package/src/core/providers/codexJsonStream.test.ts +148 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.test.ts +68 -0
- package/src/core/providers/codexSubprocess.ts +372 -0
- package/src/core/providers/codexTranscript.test.ts +284 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +59 -0
- package/src/core/terminal/terminalCapabilities.test.ts +93 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.test.ts +75 -0
- package/src/core/terminal/terminalControl.ts +147 -0
- package/src/core/terminal/terminalSanitize.test.ts +22 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalSelection.test.ts +42 -0
- package/src/core/terminal/terminalSelection.ts +66 -0
- package/src/core/terminal/terminalTitle.test.ts +328 -0
- package/src/core/terminal/terminalTitle.ts +483 -0
- package/src/core/workspaceActivity.test.ts +163 -0
- package/src/core/workspaceActivity.ts +380 -0
- package/src/core/workspaceGuard.test.ts +151 -0
- package/src/core/workspaceGuard.ts +288 -0
- package/src/core/workspaceRoot.test.ts +23 -0
- package/src/core/workspaceRoot.ts +47 -0
- package/src/exec.test.ts +13 -0
- package/src/exec.ts +72 -0
- package/src/headless/execArgs.test.ts +147 -0
- package/src/headless/execArgs.ts +294 -0
- package/src/headless/execRunner.test.ts +434 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.test.tsx +618 -0
- package/src/index.tsx +296 -0
- package/src/session/appSession.test.ts +897 -0
- package/src/session/appSession.ts +761 -0
- package/src/session/chatLifecycle.test.ts +64 -0
- package/src/session/chatLifecycle.ts +951 -0
- package/src/session/liveRenderScheduler.test.ts +201 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/planFlow.test.ts +103 -0
- package/src/session/planFlow.ts +149 -0
- package/src/session/planTranscript.test.ts +65 -0
- package/src/session/planTranscript.ts +15 -0
- package/src/session/promptRunSchedule.test.ts +36 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +228 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/ActionRequiredBlock.tsx +38 -0
- package/src/ui/ActivityBars.tsx +68 -0
- package/src/ui/ActivityIndicator.test.tsx +58 -0
- package/src/ui/ActivityIndicator.tsx +58 -0
- package/src/ui/AgentBlock.test.ts +6 -0
- package/src/ui/AgentBlock.tsx +130 -0
- package/src/ui/AnimatedStatusText.test.ts +16 -0
- package/src/ui/AnimatedStatusText.tsx +69 -0
- package/src/ui/AppShell.test.tsx +1739 -0
- package/src/ui/AppShell.tsx +698 -0
- package/src/ui/AttachmentImportPanel.test.tsx +204 -0
- package/src/ui/AttachmentImportPanel.tsx +98 -0
- package/src/ui/AuthPanel.tsx +113 -0
- package/src/ui/BackendPicker.tsx +28 -0
- package/src/ui/BottomComposer.test.ts +674 -0
- package/src/ui/BottomComposer.tsx +1028 -0
- package/src/ui/CodexLogo.tsx +55 -0
- package/src/ui/DashCard.tsx +82 -0
- package/src/ui/Markdown.test.ts +157 -0
- package/src/ui/Markdown.tsx +310 -0
- package/src/ui/ModePicker.tsx +27 -0
- package/src/ui/ModelPicker.tsx +31 -0
- package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
- package/src/ui/ModelPickerScreen.test.tsx +99 -0
- package/src/ui/ModelPickerScreen.tsx +416 -0
- package/src/ui/ModelPickerState.test.tsx +151 -0
- package/src/ui/ModelReasoningPicker.test.tsx +447 -0
- package/src/ui/ModelReasoningPicker.tsx +458 -0
- package/src/ui/Panel.tsx +51 -0
- package/src/ui/PermissionsPanel.tsx +78 -0
- package/src/ui/PlanActionPicker.tsx +119 -0
- package/src/ui/PlanReviewPanel.test.tsx +267 -0
- package/src/ui/PlanReviewPanel.tsx +212 -0
- package/src/ui/PromptCardBorder.test.tsx +161 -0
- package/src/ui/ProviderPicker.test.tsx +289 -0
- package/src/ui/ProviderPicker.tsx +321 -0
- package/src/ui/ProviderShortcut.test.tsx +143 -0
- package/src/ui/ReasoningPicker.tsx +46 -0
- package/src/ui/RunFooter.tsx +65 -0
- package/src/ui/SelectionPanel.tsx +67 -0
- package/src/ui/SettingsPanel.test.tsx +233 -0
- package/src/ui/SettingsPanel.tsx +156 -0
- package/src/ui/Spinner.tsx +25 -0
- package/src/ui/StaticIntroItem.tsx +54 -0
- package/src/ui/StaticTranscriptItem.tsx +56 -0
- package/src/ui/TextEntryPanel.tsx +139 -0
- package/src/ui/ThemePicker.tsx +31 -0
- package/src/ui/ThinkingBlock.tsx +100 -0
- package/src/ui/Timeline.test.ts +2067 -0
- package/src/ui/Timeline.tsx +1472 -0
- package/src/ui/TimelineNavigation.test.tsx +201 -0
- package/src/ui/TopHeader.test.tsx +239 -0
- package/src/ui/TopHeader.tsx +257 -0
- package/src/ui/TurnGroup.test.tsx +365 -0
- package/src/ui/TurnGroup.tsx +657 -0
- package/src/ui/busyStatusAnimation.test.ts +30 -0
- package/src/ui/busyStatusAnimation.ts +11 -0
- package/src/ui/commandNormalize.test.ts +142 -0
- package/src/ui/commandNormalize.ts +66 -0
- package/src/ui/diffRenderer.test.ts +102 -0
- package/src/ui/diffRenderer.ts +116 -0
- package/src/ui/focus.ts +61 -0
- package/src/ui/focusFlow.test.tsx +1098 -0
- package/src/ui/inputBuffer.test.ts +151 -0
- package/src/ui/inputBuffer.ts +203 -0
- package/src/ui/layout.test.ts +145 -0
- package/src/ui/layout.ts +287 -0
- package/src/ui/modeDisplay.test.ts +42 -0
- package/src/ui/modeDisplay.ts +52 -0
- package/src/ui/outputPipeline.ts +64 -0
- package/src/ui/progressEntries.ts +156 -0
- package/src/ui/runActivityView.test.ts +89 -0
- package/src/ui/runActivityView.ts +37 -0
- package/src/ui/runLifecycleView.test.tsx +237 -0
- package/src/ui/slashCommands.ts +41 -0
- package/src/ui/statusRenderIsolation.test.tsx +654 -0
- package/src/ui/terminalAnswerFormat.test.ts +19 -0
- package/src/ui/terminalAnswerFormat.ts +128 -0
- package/src/ui/textLayout.test.ts +18 -0
- package/src/ui/textLayout.ts +338 -0
- package/src/ui/theme.tsx +395 -0
- package/src/ui/themeFlow.test.ts +53 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timelineMeasure.ts +3088 -0
- package/src/ui/timelineMeasureCache.test.ts +986 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { runCommand, type CommandResult } from "../process/CommandRunner.js";
|
|
2
|
+
import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
|
|
3
|
+
import type { BackendRunHandlers } from "../providers/types.js";
|
|
4
|
+
import { ANTHROPIC_FALLBACK_MODELS } from "./models.js";
|
|
5
|
+
import type { ProviderBackendKind, ProviderChatRequest, ProviderModel, ProviderModelDiscoveryResult, ProviderRouteValidationResult, ProviderRuntime } from "./types.js";
|
|
6
|
+
import { buildClaudeSpawnSpec, resetClaudeExecutableCacheForTests } from "../executables/claudeExecutable.js";
|
|
7
|
+
import { CLAUDE_CODE_EFFORT_IDS } from "./reasoning.js";
|
|
8
|
+
import {
|
|
9
|
+
claudeCodeModelsToProviderModels,
|
|
10
|
+
discoverClaudeCodeCapabilities,
|
|
11
|
+
getClaudeModelDefaultEffort,
|
|
12
|
+
modelSupportsClaudeEffort,
|
|
13
|
+
type ClaudeCodeCapabilityDiscovery,
|
|
14
|
+
} from "./claudeCodeDiscovery.js";
|
|
15
|
+
|
|
16
|
+
const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
17
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
18
|
+
const ANTHROPIC_MAX_TOKENS = 1024;
|
|
19
|
+
const ANTHROPIC_TIMEOUT_MS = 120_000;
|
|
20
|
+
const ANTHROPIC_AUTH_CHECK_TIMEOUT_MS = 10_000;
|
|
21
|
+
const ANTHROPIC_ROUTE_VALIDATION_TIMEOUT_MS = 15_000;
|
|
22
|
+
export const ANTHROPIC_ROUTE_SETUP_MESSAGE = "Anthropic/Claude is not configured for in-Codexa routing.\nSign in with Claude Code or set ANTHROPIC_API_KEY.";
|
|
23
|
+
export { parseClaudeAuthStatus } from "./claudeCodeDiscovery.js";
|
|
24
|
+
|
|
25
|
+
type CommandRunner = typeof runCommand;
|
|
26
|
+
|
|
27
|
+
let claudeCodeValidated = false;
|
|
28
|
+
let resolvedClaudeExecutable: string = "claude";
|
|
29
|
+
let discoveredAnthropicModels: readonly ProviderModel[] | null = null;
|
|
30
|
+
let claudeCapabilityDiscovery: ClaudeCodeCapabilityDiscovery | null = null;
|
|
31
|
+
|
|
32
|
+
function getAnthropicApiKey(): string | null {
|
|
33
|
+
return process.env.ANTHROPIC_API_KEY?.trim() || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isAnthropicRouteConfigured(): boolean {
|
|
37
|
+
return getAnthropicApiKey() !== null || claudeCodeValidated;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resetAnthropicRouteValidationCacheForTests(): void {
|
|
41
|
+
claudeCodeValidated = false;
|
|
42
|
+
resolvedClaudeExecutable = "claude";
|
|
43
|
+
discoveredAnthropicModels = null;
|
|
44
|
+
claudeCapabilityDiscovery = null;
|
|
45
|
+
resetClaudeExecutableCacheForTests();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Model / reasoning / permission-mode mapping
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// Claude Code CLI accepts both short aliases ("sonnet", "opus", "haiku") and
|
|
53
|
+
// full versioned IDs ("claude-sonnet-4-20250514"). Pass through unchanged.
|
|
54
|
+
export function mapModelIdToClaudeArg(modelId: string): string {
|
|
55
|
+
return modelId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function mapReasoningToEffort(reasoning: string | null | undefined): string | null {
|
|
59
|
+
if (reasoning && CLAUDE_CODE_EFFORT_IDS.has(reasoning)) return reasoning;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildClaudeCodeBaseArgs(request: ProviderChatRequest): string[] {
|
|
64
|
+
const effort = mapReasoningToEffort(request.route.reasoning ?? null);
|
|
65
|
+
const models = getActiveAnthropicModels();
|
|
66
|
+
const supportedEffort = effort && modelSupportsClaudeEffort(request.route.modelId, effort, models) ? effort : null;
|
|
67
|
+
return [
|
|
68
|
+
...(request.route.modelId ? ["--model", mapModelIdToClaudeArg(request.route.modelId)] : []),
|
|
69
|
+
...(supportedEffort ? ["--effort", supportedEffort] : []),
|
|
70
|
+
"--permission-mode", "default",
|
|
71
|
+
request.prompt,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ensureClaudeStreamJsonVerbose(args: string[]): string[] {
|
|
76
|
+
const outputFormatIndex = args.indexOf("--output-format");
|
|
77
|
+
const usesStreamJson = outputFormatIndex >= 0 && args[outputFormatIndex + 1] === "stream-json";
|
|
78
|
+
const usesPrint = args.includes("-p") || args.includes("--print");
|
|
79
|
+
if (!usesStreamJson || !usesPrint || args.includes("--verbose")) return args;
|
|
80
|
+
|
|
81
|
+
const printIndex = args.findIndex((arg) => arg === "-p" || arg === "--print");
|
|
82
|
+
const next = [...args];
|
|
83
|
+
next.splice(printIndex + 1, 0, "--verbose");
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildClaudeCodeArgs(request: ProviderChatRequest): string[] {
|
|
88
|
+
return ensureClaudeStreamJsonVerbose([
|
|
89
|
+
"-p",
|
|
90
|
+
"--output-format", "stream-json",
|
|
91
|
+
"--include-partial-messages",
|
|
92
|
+
...buildClaudeCodeBaseArgs(request),
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildClaudeCodePlainTextArgs(request: ProviderChatRequest): string[] {
|
|
97
|
+
return [
|
|
98
|
+
"-p",
|
|
99
|
+
...buildClaudeCodeBaseArgs(request),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Stream-JSON parsing
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
interface StreamJsonAssistantEvent {
|
|
108
|
+
type: "assistant";
|
|
109
|
+
message: {
|
|
110
|
+
content?: Array<{ type?: string; text?: string; partial?: boolean }>;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parses one stdout line from `claude -p --output-format stream-json`.
|
|
116
|
+
* Returns:
|
|
117
|
+
* string — extracted assistant text delta
|
|
118
|
+
* null — valid JSON but no text to emit (tool event, system event, etc.)
|
|
119
|
+
* false — parse error; caller should fall back to plain-text mode
|
|
120
|
+
*/
|
|
121
|
+
export function tryParseStreamJsonDelta(line: string): string | null | false {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
if (!trimmed) return null;
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(trimmed);
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
131
|
+
const event = parsed as Record<string, unknown>;
|
|
132
|
+
if (event["type"] !== "assistant") return null;
|
|
133
|
+
const msg = event["message"];
|
|
134
|
+
if (typeof msg !== "object" || msg === null) return null;
|
|
135
|
+
const content = (msg as StreamJsonAssistantEvent["message"]).content;
|
|
136
|
+
if (!Array.isArray(content)) return null;
|
|
137
|
+
const text = content
|
|
138
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
139
|
+
.map((part) => part.text ?? "")
|
|
140
|
+
.join("");
|
|
141
|
+
return text || null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Anthropic Messages API
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
async function runAnthropicApi(request: ProviderChatRequest): Promise<string> {
|
|
149
|
+
const apiKey = getAnthropicApiKey();
|
|
150
|
+
if (!apiKey) {
|
|
151
|
+
throw new Error(ANTHROPIC_ROUTE_SETUP_MESSAGE);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const response = await fetch(ANTHROPIC_MESSAGES_URL, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
"x-api-key": apiKey,
|
|
159
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
model: request.route.modelId,
|
|
163
|
+
max_tokens: ANTHROPIC_MAX_TOKENS,
|
|
164
|
+
...(request.projectInstructions?.content ? { system: request.projectInstructions.content } : {}),
|
|
165
|
+
messages: [
|
|
166
|
+
{
|
|
167
|
+
role: "user",
|
|
168
|
+
content: request.prompt,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const body = await response.text();
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw new Error(`Anthropic API request failed (${response.status}): ${sanitizeTerminalOutput(body).slice(0, 500)}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const parsed = JSON.parse(body) as {
|
|
180
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
181
|
+
};
|
|
182
|
+
const text = parsed.content
|
|
183
|
+
?.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
184
|
+
.map((part) => part.text ?? "")
|
|
185
|
+
.join("")
|
|
186
|
+
.trim();
|
|
187
|
+
|
|
188
|
+
if (!text) {
|
|
189
|
+
throw new Error("Anthropic API returned no assistant text.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Claude Code CLI execution (streaming)
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
function isClaudeStreamJsonRequiresVerboseError(result: CommandResult): boolean {
|
|
200
|
+
const combined = `${result.userMessage}\n${result.stderr}\n${result.stdout}`;
|
|
201
|
+
return /stream-json requires --verbose/i.test(combined);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isClaudeInvalidEffortError(result: CommandResult): boolean {
|
|
205
|
+
const combined = `${result.userMessage}\n${result.stderr}\n${result.stdout}`;
|
|
206
|
+
return /\beffort\b/i.test(combined)
|
|
207
|
+
&& /\b(invalid|unsupported|not supported|unknown|expected|allowed|valid)\b/i.test(combined);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function withClaudeEffort(request: ProviderChatRequest, effort: string): ProviderChatRequest {
|
|
211
|
+
return {
|
|
212
|
+
...request,
|
|
213
|
+
route: {
|
|
214
|
+
...request.route,
|
|
215
|
+
reasoning: effort,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function disableClaudeEffortForSession(modelId: string, effort: string): void {
|
|
221
|
+
const models = getActiveAnthropicModels();
|
|
222
|
+
discoveredAnthropicModels = models.map((model) => {
|
|
223
|
+
const isTarget = model.modelId === modelId || model.id === modelId || model.family === modelId || model.canonicalId === modelId;
|
|
224
|
+
if (!isTarget || !model.supportedReasoningLevels) return model;
|
|
225
|
+
return {
|
|
226
|
+
...model,
|
|
227
|
+
supportedReasoningLevels: model.supportedReasoningLevels.filter((level) => level.id !== effort),
|
|
228
|
+
description: `${model.description ?? model.label} - ${effort} disabled after Claude Code rejection this session`,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatClaudeCommandDiagnostic(
|
|
234
|
+
executable: string,
|
|
235
|
+
args: string[],
|
|
236
|
+
prompt: string,
|
|
237
|
+
): string {
|
|
238
|
+
const safeArgs = args.map((arg) => arg === prompt ? `<prompt redacted: ${prompt.length} chars>` : arg);
|
|
239
|
+
return `Claude Code command args: ${JSON.stringify([executable, ...safeArgs])}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function runClaudeCodeWithRunner(
|
|
243
|
+
request: ProviderChatRequest,
|
|
244
|
+
handlers: BackendRunHandlers,
|
|
245
|
+
runCommandImpl: CommandRunner = runCommand,
|
|
246
|
+
executable: string = resolvedClaudeExecutable,
|
|
247
|
+
): () => void {
|
|
248
|
+
let currentCancel: (() => void) | null = null;
|
|
249
|
+
let canceled = false;
|
|
250
|
+
|
|
251
|
+
const runAttempt = (
|
|
252
|
+
mode: "stream-json" | "plain-text",
|
|
253
|
+
attemptRequest: ProviderChatRequest = request,
|
|
254
|
+
effortFallbackUsed = false,
|
|
255
|
+
) => {
|
|
256
|
+
const args = mode === "stream-json"
|
|
257
|
+
? buildClaudeCodeArgs(attemptRequest)
|
|
258
|
+
: buildClaudeCodePlainTextArgs(attemptRequest);
|
|
259
|
+
const spawnSpec = buildClaudeSpawnSpec(executable, args);
|
|
260
|
+
|
|
261
|
+
let accumulatedText = "";
|
|
262
|
+
let streamingFailed = false;
|
|
263
|
+
let lineBuf = "";
|
|
264
|
+
|
|
265
|
+
const runner = runCommandImpl(
|
|
266
|
+
{
|
|
267
|
+
executable: spawnSpec.executable,
|
|
268
|
+
args: spawnSpec.args,
|
|
269
|
+
cwd: request.workspaceRoot,
|
|
270
|
+
timeoutMs: ANTHROPIC_TIMEOUT_MS,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
onStdout: (chunk) => {
|
|
274
|
+
if (mode !== "stream-json" || streamingFailed) return;
|
|
275
|
+
lineBuf += chunk;
|
|
276
|
+
const lines = lineBuf.split("\n");
|
|
277
|
+
lineBuf = lines.pop() ?? "";
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
const delta = tryParseStreamJsonDelta(line);
|
|
280
|
+
if (delta === false) {
|
|
281
|
+
streamingFailed = true;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (delta) {
|
|
285
|
+
accumulatedText += delta;
|
|
286
|
+
handlers.onAssistantDelta?.(delta);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
currentCancel = runner.cancel;
|
|
294
|
+
runner.result.then((result) => {
|
|
295
|
+
if (canceled || result.status === "canceled") return;
|
|
296
|
+
if (result.status !== "completed" || result.exitCode !== 0) {
|
|
297
|
+
const diagnostic = formatClaudeCommandDiagnostic(spawnSpec.executable, spawnSpec.args, request.prompt);
|
|
298
|
+
if (mode === "stream-json" && isClaudeStreamJsonRequiresVerboseError(result)) {
|
|
299
|
+
handlers.onProgress?.({
|
|
300
|
+
id: "anthropic-claude-command-retry",
|
|
301
|
+
source: "stderr",
|
|
302
|
+
text: `${diagnostic}\nClaude Code rejected stream-json args; retrying once with plain text output.`,
|
|
303
|
+
});
|
|
304
|
+
runAttempt("plain-text");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const requestedEffort = mapReasoningToEffort(attemptRequest.route.reasoning ?? null);
|
|
309
|
+
const fallbackEffort = getClaudeModelDefaultEffort(attemptRequest.route.modelId, getActiveAnthropicModels());
|
|
310
|
+
if (requestedEffort && isClaudeInvalidEffortError(result)) {
|
|
311
|
+
disableClaudeEffortForSession(attemptRequest.route.modelId, requestedEffort);
|
|
312
|
+
}
|
|
313
|
+
if (!effortFallbackUsed && requestedEffort && requestedEffort !== fallbackEffort && isClaudeInvalidEffortError(result)) {
|
|
314
|
+
const fallbackRequest = withClaudeEffort(attemptRequest, fallbackEffort);
|
|
315
|
+
const fallbackArgs = mode === "stream-json"
|
|
316
|
+
? buildClaudeCodeArgs(fallbackRequest)
|
|
317
|
+
: buildClaudeCodePlainTextArgs(fallbackRequest);
|
|
318
|
+
const fallbackSpawnSpec = buildClaudeSpawnSpec(executable, fallbackArgs);
|
|
319
|
+
handlers.onProgress?.({
|
|
320
|
+
id: "anthropic-claude-effort-fallback",
|
|
321
|
+
source: "stderr",
|
|
322
|
+
text: [
|
|
323
|
+
`${diagnostic}`,
|
|
324
|
+
`Claude Code rejected effort "${requestedEffort}" for ${request.route.modelId}; retrying once with --effort ${fallbackEffort}.`,
|
|
325
|
+
formatClaudeCommandDiagnostic(fallbackSpawnSpec.executable, fallbackSpawnSpec.args, request.prompt),
|
|
326
|
+
].join("\n"),
|
|
327
|
+
});
|
|
328
|
+
runAttempt(mode, fallbackRequest, true);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const message = result.userMessage || result.stderr || "Claude Code execution failed.";
|
|
333
|
+
handlers.onError(message, diagnostic);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
let finalText: string;
|
|
337
|
+
if (mode === "stream-json" && !streamingFailed && accumulatedText !== "") {
|
|
338
|
+
finalText = accumulatedText;
|
|
339
|
+
} else {
|
|
340
|
+
finalText = sanitizeTerminalOutput(result.stdout).trim();
|
|
341
|
+
if (finalText && (mode !== "stream-json" || accumulatedText === "")) {
|
|
342
|
+
handlers.onAssistantDelta?.(finalText);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
handlers.onFinalAnswerObserved?.(finalText);
|
|
346
|
+
handlers.onResponse(finalText);
|
|
347
|
+
}).catch((error) => {
|
|
348
|
+
if (canceled) return;
|
|
349
|
+
const message = error instanceof Error ? error.message : "Claude Code execution failed.";
|
|
350
|
+
handlers.onError(message);
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
runAttempt("stream-json");
|
|
355
|
+
|
|
356
|
+
return () => {
|
|
357
|
+
canceled = true;
|
|
358
|
+
currentCancel?.();
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function runClaudeCode(
|
|
363
|
+
request: ProviderChatRequest,
|
|
364
|
+
handlers: BackendRunHandlers,
|
|
365
|
+
): () => void {
|
|
366
|
+
return runClaudeCodeWithRunner(request, handlers);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Route validation
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
export async function validateAnthropicRoute(options: {
|
|
374
|
+
cwd: string;
|
|
375
|
+
env?: NodeJS.ProcessEnv;
|
|
376
|
+
runCommandImpl?: CommandRunner;
|
|
377
|
+
timeoutMs?: number;
|
|
378
|
+
configuredPath?: string | null;
|
|
379
|
+
}): Promise<ProviderRouteValidationResult> {
|
|
380
|
+
let discovery: ClaudeCodeCapabilityDiscovery;
|
|
381
|
+
try {
|
|
382
|
+
discovery = await discoverClaudeCodeCapabilities({
|
|
383
|
+
cwd: options.cwd,
|
|
384
|
+
runCommandImpl: options.runCommandImpl,
|
|
385
|
+
configuredPath: options.configuredPath,
|
|
386
|
+
timeoutMs: options.timeoutMs ?? ANTHROPIC_ROUTE_VALIDATION_TIMEOUT_MS,
|
|
387
|
+
});
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const message = error instanceof Error ? error.message : "Failed to resolve Claude executable.";
|
|
390
|
+
return {
|
|
391
|
+
status: "not-configured",
|
|
392
|
+
providerId: "anthropic",
|
|
393
|
+
backendKind: "unavailable",
|
|
394
|
+
message,
|
|
395
|
+
diagnostics: { resolvedCommand: null },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
resolvedClaudeExecutable = discovery.resolvedCommand;
|
|
400
|
+
claudeCapabilityDiscovery = discovery;
|
|
401
|
+
discoveredAnthropicModels = claudeCodeModelsToProviderModels(discovery.models);
|
|
402
|
+
|
|
403
|
+
const diagnostics: Record<string, string | number | boolean | null> = {
|
|
404
|
+
resolvedCommand: discovery.resolvedCommand,
|
|
405
|
+
executablePath: discovery.resolvedCommand,
|
|
406
|
+
authCommand: `${discovery.resolvedCommand} auth status`,
|
|
407
|
+
modelSource: discovery.modelSource,
|
|
408
|
+
discoveredAt: discovery.discoveredAt,
|
|
409
|
+
loggedIn: discovery.auth.loggedIn,
|
|
410
|
+
authMethod: discovery.auth.authMethod ?? null,
|
|
411
|
+
apiProvider: discovery.auth.apiProvider ?? null,
|
|
412
|
+
subscriptionType: discovery.auth.subscriptionType ?? null,
|
|
413
|
+
settingsPath: discovery.settings?.path ?? null,
|
|
414
|
+
settingsModel: discovery.settings?.model ?? null,
|
|
415
|
+
settingsEffortLevel: discovery.settings?.effortLevel ?? null,
|
|
416
|
+
...(discovery.diagnostics ?? {}),
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (discovery.auth.loggedIn === true) {
|
|
420
|
+
claudeCodeValidated = true;
|
|
421
|
+
const authMethodLabel = discovery.auth.authMethod ?? null;
|
|
422
|
+
return {
|
|
423
|
+
status: "ready",
|
|
424
|
+
providerId: "anthropic",
|
|
425
|
+
backendKind: "claude-code-auth",
|
|
426
|
+
message: authMethodLabel
|
|
427
|
+
? `Claude Code authenticated (${authMethodLabel}).`
|
|
428
|
+
: "Claude Code auth is configured.",
|
|
429
|
+
diagnostics,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
claudeCodeValidated = false;
|
|
434
|
+
|
|
435
|
+
if (getAnthropicApiKey() !== null) {
|
|
436
|
+
return {
|
|
437
|
+
status: "ready",
|
|
438
|
+
providerId: "anthropic",
|
|
439
|
+
backendKind: "anthropic-api-key",
|
|
440
|
+
message: "Anthropic API key is configured.",
|
|
441
|
+
diagnostics,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const authStatus = String(diagnostics["authStatus"] ?? "");
|
|
446
|
+
const authExitCode = diagnostics["authExitCode"];
|
|
447
|
+
const authJsonParsed = diagnostics["authJsonParsed"] === true;
|
|
448
|
+
let message: string;
|
|
449
|
+
if (authStatus === "spawn_error") {
|
|
450
|
+
message = `\`${discovery.resolvedCommand}\` command not found. Install Claude Code from https://claude.ai/code or set CLAUDE_EXECUTABLE to the full path.`;
|
|
451
|
+
} else if (authStatus === "timeout") {
|
|
452
|
+
message = `Claude Code auth check timed out. Run: ${discovery.resolvedCommand} auth status`;
|
|
453
|
+
} else if (authExitCode === 1) {
|
|
454
|
+
message = `Claude Code is installed but not signed in. Run: ${discovery.resolvedCommand} auth login`;
|
|
455
|
+
} else if (!authJsonParsed) {
|
|
456
|
+
message = `Claude Code auth status did not return valid JSON. Run: ${discovery.resolvedCommand} auth status`;
|
|
457
|
+
} else {
|
|
458
|
+
message = `Claude Code is not signed in. Run: ${discovery.resolvedCommand} auth login`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
status: "not-configured",
|
|
463
|
+
providerId: "anthropic",
|
|
464
|
+
backendKind: "unavailable",
|
|
465
|
+
message,
|
|
466
|
+
diagnostics,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Runtime
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
function getAnthropicRuntimeBackendKind(): ProviderBackendKind {
|
|
475
|
+
return claudeCodeValidated ? "claude-code-auth" : getAnthropicApiKey() ? "anthropic-api-key" : "claude-code-auth";
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function getActiveAnthropicModels(): readonly ProviderModel[] {
|
|
479
|
+
return discoveredAnthropicModels ?? ANTHROPIC_FALLBACK_MODELS;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const anthropicRuntime: ProviderRuntime = {
|
|
483
|
+
providerId: "anthropic",
|
|
484
|
+
label: "Anthropic/Claude",
|
|
485
|
+
modelPickerLabel: "Claude",
|
|
486
|
+
backendKind: "claude-code-auth",
|
|
487
|
+
routeAvailable: true,
|
|
488
|
+
routeStatus: "Routes through Claude Code CLI when authenticated (`claude auth status` exit 0), or via ANTHROPIC_API_KEY.",
|
|
489
|
+
routeSetupMessage: ANTHROPIC_ROUTE_SETUP_MESSAGE,
|
|
490
|
+
launchAvailable: true,
|
|
491
|
+
isRouteConfigured: isAnthropicRouteConfigured,
|
|
492
|
+
validateRoute: async ({ workspaceRoot, claudeCommandPath }) => validateAnthropicRoute({
|
|
493
|
+
cwd: workspaceRoot,
|
|
494
|
+
configuredPath: claudeCommandPath,
|
|
495
|
+
}),
|
|
496
|
+
discoverModels: (): ProviderModelDiscoveryResult => ({
|
|
497
|
+
status: "ready",
|
|
498
|
+
providerId: "anthropic",
|
|
499
|
+
backendKind: getAnthropicRuntimeBackendKind(),
|
|
500
|
+
models: getActiveAnthropicModels(),
|
|
501
|
+
diagnostics: claudeCapabilityDiscovery ? {
|
|
502
|
+
resolvedCommand: claudeCapabilityDiscovery.resolvedCommand,
|
|
503
|
+
modelSource: claudeCapabilityDiscovery.modelSource,
|
|
504
|
+
discoveredAt: claudeCapabilityDiscovery.discoveredAt,
|
|
505
|
+
loggedIn: claudeCapabilityDiscovery.auth.loggedIn,
|
|
506
|
+
settingsPath: claudeCapabilityDiscovery.settings?.path ?? null,
|
|
507
|
+
} : { modelSource: "fallback" },
|
|
508
|
+
}),
|
|
509
|
+
refreshModels: async ({ cwd }): Promise<ProviderModelDiscoveryResult> => {
|
|
510
|
+
let discovery: ClaudeCodeCapabilityDiscovery;
|
|
511
|
+
try {
|
|
512
|
+
discovery = await discoverClaudeCodeCapabilities({ cwd, runCommandImpl: runCommand });
|
|
513
|
+
} catch (error) {
|
|
514
|
+
const message = error instanceof Error ? error.message : "Claude capability refresh failed.";
|
|
515
|
+
return {
|
|
516
|
+
status: "ready",
|
|
517
|
+
providerId: "anthropic",
|
|
518
|
+
backendKind: getAnthropicRuntimeBackendKind(),
|
|
519
|
+
models: getActiveAnthropicModels(),
|
|
520
|
+
message: `Refresh Claude capabilities failed; keeping previous Claude capability data. ${message}`,
|
|
521
|
+
diagnostics: {
|
|
522
|
+
resolvedCommand: resolvedClaudeExecutable,
|
|
523
|
+
modelSource: claudeCapabilityDiscovery?.modelSource ?? "fallback",
|
|
524
|
+
refreshFailed: true,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
resolvedClaudeExecutable = discovery.resolvedCommand;
|
|
529
|
+
claudeCapabilityDiscovery = discovery;
|
|
530
|
+
claudeCodeValidated = discovery.auth.loggedIn;
|
|
531
|
+
discoveredAnthropicModels = claudeCodeModelsToProviderModels(discovery.models);
|
|
532
|
+
return {
|
|
533
|
+
status: "ready",
|
|
534
|
+
providerId: "anthropic",
|
|
535
|
+
backendKind: getAnthropicRuntimeBackendKind(),
|
|
536
|
+
models: discoveredAnthropicModels,
|
|
537
|
+
message: `Refreshed Claude capabilities (${discovery.modelSource}).`,
|
|
538
|
+
diagnostics: {
|
|
539
|
+
resolvedCommand: discovery.resolvedCommand,
|
|
540
|
+
modelSource: discovery.modelSource,
|
|
541
|
+
discoveredAt: discovery.discoveredAt,
|
|
542
|
+
loggedIn: discovery.auth.loggedIn,
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
},
|
|
546
|
+
run: (request, handlers: BackendRunHandlers) => {
|
|
547
|
+
handlers.onProgress?.({
|
|
548
|
+
id: "anthropic-route",
|
|
549
|
+
source: "stdout",
|
|
550
|
+
text: "Starting Claude Code",
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (claudeCodeValidated) {
|
|
554
|
+
return runClaudeCode(request, handlers);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (getAnthropicApiKey()) {
|
|
558
|
+
let cancelled = false;
|
|
559
|
+
runAnthropicApi(request)
|
|
560
|
+
.then((text) => {
|
|
561
|
+
if (cancelled) return;
|
|
562
|
+
handlers.onAssistantDelta?.(text);
|
|
563
|
+
handlers.onFinalAnswerObserved?.(text);
|
|
564
|
+
handlers.onResponse(text);
|
|
565
|
+
})
|
|
566
|
+
.catch((error) => {
|
|
567
|
+
if (cancelled) return;
|
|
568
|
+
const message = error instanceof Error ? error.message : "Anthropic/Claude in-Codexa routing failed.";
|
|
569
|
+
handlers.onError(message);
|
|
570
|
+
});
|
|
571
|
+
return () => { cancelled = true; };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return runClaudeCode(request, handlers);
|
|
575
|
+
},
|
|
576
|
+
};
|