mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-19 20:11:30 +00:00
Move provider authentication status logic out of the CLI auth route so auth checks live with the provider implementations that understand each provider's install and credential model. Add provider-specific auth runtime classes for Claude, Codex, Cursor, and Gemini, and expose them through the shared provider contract as `provider.auth`. Add a provider auth service that resolves providers through the registry and delegates status checks via `auth.getStatus()`. Keep the existing `/api/cli/<provider>/status` endpoints, but make them thin route adapters over the new provider auth service. This removes duplicated route-local credential parsing and makes auth status a first-class provider capability beside MCP and message handling.
322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
import { getSessionMessages } from '@/projects.js';
|
|
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
|
import { ClaudeAuthProvider } from '@/modules/providers/list/claude/claude-auth.provider.js';
|
|
import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js';
|
|
import type { IProviderAuthRuntime } from '@/shared/interfaces.js';
|
|
import type { FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
|
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
|
|
|
const PROVIDER = 'claude';
|
|
|
|
type RawProviderMessage = Record<string, any>;
|
|
|
|
type ClaudeToolResult = {
|
|
content: unknown;
|
|
isError: boolean;
|
|
subagentTools?: unknown;
|
|
toolUseResult?: unknown;
|
|
};
|
|
|
|
type ClaudeHistoryResult =
|
|
| RawProviderMessage[]
|
|
| {
|
|
messages?: RawProviderMessage[];
|
|
total?: number;
|
|
hasMore?: boolean;
|
|
};
|
|
|
|
const loadClaudeSessionMessages = getSessionMessages as unknown as (
|
|
projectName: string,
|
|
sessionId: string,
|
|
limit: number | null,
|
|
offset: number,
|
|
) => Promise<ClaudeHistoryResult>;
|
|
|
|
/**
|
|
* Claude writes internal command and system reminder entries into history.
|
|
* Those are useful for the CLI but should not appear in the user-facing chat.
|
|
*/
|
|
const INTERNAL_CONTENT_PREFIXES = [
|
|
'<command-name>',
|
|
'<command-message>',
|
|
'<command-args>',
|
|
'<local-command-stdout>',
|
|
'<system-reminder>',
|
|
'Caveat:',
|
|
'This session is being continued from a previous',
|
|
'[Request interrupted',
|
|
] as const;
|
|
|
|
function isInternalContent(content: string): boolean {
|
|
return INTERNAL_CONTENT_PREFIXES.some((prefix) => content.startsWith(prefix));
|
|
}
|
|
|
|
function readRawProviderMessage(raw: unknown): RawProviderMessage | null {
|
|
return readObjectRecord(raw) as RawProviderMessage | null;
|
|
}
|
|
|
|
export class ClaudeProvider extends AbstractProvider {
|
|
readonly mcp = new ClaudeMcpProvider();
|
|
readonly auth: IProviderAuthRuntime = new ClaudeAuthProvider();
|
|
|
|
constructor() {
|
|
super('claude');
|
|
}
|
|
|
|
/**
|
|
* Normalizes one Claude JSONL entry or live SDK stream event into the shared
|
|
* message shape consumed by REST and WebSocket clients.
|
|
*/
|
|
normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
|
|
const raw = readRawProviderMessage(rawMessage);
|
|
if (!raw) {
|
|
return [];
|
|
}
|
|
|
|
if (raw.type === 'content_block_delta' && raw.delta?.text) {
|
|
return [createNormalizedMessage({ kind: 'stream_delta', content: raw.delta.text, sessionId, provider: PROVIDER })];
|
|
}
|
|
if (raw.type === 'content_block_stop') {
|
|
return [createNormalizedMessage({ kind: 'stream_end', sessionId, provider: PROVIDER })];
|
|
}
|
|
|
|
const messages: NormalizedMessage[] = [];
|
|
const ts = raw.timestamp || new Date().toISOString();
|
|
const baseId = raw.uuid || generateMessageId('claude');
|
|
|
|
if (raw.message?.role === 'user' && raw.message?.content) {
|
|
if (Array.isArray(raw.message.content)) {
|
|
for (const part of raw.message.content) {
|
|
if (part.type === 'tool_result') {
|
|
messages.push(createNormalizedMessage({
|
|
id: `${baseId}_tr_${part.tool_use_id}`,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'tool_result',
|
|
toolId: part.tool_use_id,
|
|
content: typeof part.content === 'string' ? part.content : JSON.stringify(part.content),
|
|
isError: Boolean(part.is_error),
|
|
subagentTools: raw.subagentTools,
|
|
toolUseResult: raw.toolUseResult,
|
|
}));
|
|
} else if (part.type === 'text') {
|
|
const text = part.text || '';
|
|
if (text && !isInternalContent(text)) {
|
|
messages.push(createNormalizedMessage({
|
|
id: `${baseId}_text`,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'user',
|
|
content: text,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (messages.length === 0) {
|
|
const textParts = raw.message.content
|
|
.filter((part: RawProviderMessage) => part.type === 'text')
|
|
.map((part: RawProviderMessage) => part.text)
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
if (textParts && !isInternalContent(textParts)) {
|
|
messages.push(createNormalizedMessage({
|
|
id: `${baseId}_text`,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'user',
|
|
content: textParts,
|
|
}));
|
|
}
|
|
}
|
|
} else if (typeof raw.message.content === 'string') {
|
|
const text = raw.message.content;
|
|
if (text && !isInternalContent(text)) {
|
|
messages.push(createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'user',
|
|
content: text,
|
|
}));
|
|
}
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
if (raw.type === 'thinking' && raw.message?.content) {
|
|
messages.push(createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'thinking',
|
|
content: raw.message.content,
|
|
}));
|
|
return messages;
|
|
}
|
|
|
|
if (raw.type === 'tool_use' && raw.toolName) {
|
|
messages.push(createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'tool_use',
|
|
toolName: raw.toolName,
|
|
toolInput: raw.toolInput,
|
|
toolId: raw.toolCallId || baseId,
|
|
}));
|
|
return messages;
|
|
}
|
|
|
|
if (raw.type === 'tool_result') {
|
|
messages.push(createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'tool_result',
|
|
toolId: raw.toolCallId || '',
|
|
content: raw.output || '',
|
|
isError: false,
|
|
}));
|
|
return messages;
|
|
}
|
|
|
|
if (raw.message?.role === 'assistant' && raw.message?.content) {
|
|
if (Array.isArray(raw.message.content)) {
|
|
let partIndex = 0;
|
|
for (const part of raw.message.content) {
|
|
if (part.type === 'text' && part.text) {
|
|
messages.push(createNormalizedMessage({
|
|
id: `${baseId}_${partIndex}`,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'assistant',
|
|
content: part.text,
|
|
}));
|
|
} else if (part.type === 'tool_use') {
|
|
messages.push(createNormalizedMessage({
|
|
id: `${baseId}_${partIndex}`,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'tool_use',
|
|
toolName: part.name,
|
|
toolInput: part.input,
|
|
toolId: part.id,
|
|
}));
|
|
} else if (part.type === 'thinking' && part.thinking) {
|
|
messages.push(createNormalizedMessage({
|
|
id: `${baseId}_${partIndex}`,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'thinking',
|
|
content: part.thinking,
|
|
}));
|
|
}
|
|
partIndex++;
|
|
}
|
|
} else if (typeof raw.message.content === 'string') {
|
|
messages.push(createNormalizedMessage({
|
|
id: baseId,
|
|
sessionId,
|
|
timestamp: ts,
|
|
provider: PROVIDER,
|
|
kind: 'text',
|
|
role: 'assistant',
|
|
content: raw.message.content,
|
|
}));
|
|
}
|
|
return messages;
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
/**
|
|
* Loads Claude JSONL history for a project/session and returns normalized
|
|
* messages, preserving the existing pagination behavior from projects.js.
|
|
*/
|
|
async fetchHistory(
|
|
sessionId: string,
|
|
options: FetchHistoryOptions = {},
|
|
): Promise<FetchHistoryResult> {
|
|
const { projectName, limit = null, offset = 0 } = options;
|
|
if (!projectName) {
|
|
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
}
|
|
|
|
let result: ClaudeHistoryResult;
|
|
try {
|
|
result = await loadClaudeSessionMessages(projectName, sessionId, limit, offset);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
|
|
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
}
|
|
|
|
const rawMessages = Array.isArray(result) ? result : (result.messages || []);
|
|
const total = Array.isArray(result) ? rawMessages.length : (result.total || 0);
|
|
const hasMore = Array.isArray(result) ? false : Boolean(result.hasMore);
|
|
|
|
const toolResultMap = new Map<string, ClaudeToolResult>();
|
|
for (const raw of rawMessages) {
|
|
if (raw.message?.role === 'user' && Array.isArray(raw.message?.content)) {
|
|
for (const part of raw.message.content) {
|
|
if (part.type === 'tool_result' && part.tool_use_id) {
|
|
toolResultMap.set(part.tool_use_id, {
|
|
content: part.content,
|
|
isError: Boolean(part.is_error),
|
|
subagentTools: raw.subagentTools,
|
|
toolUseResult: raw.toolUseResult,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const normalized: NormalizedMessage[] = [];
|
|
for (const raw of rawMessages) {
|
|
normalized.push(...this.normalizeMessage(raw, sessionId));
|
|
}
|
|
|
|
for (const msg of normalized) {
|
|
if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
|
|
const toolResult = toolResultMap.get(msg.toolId);
|
|
if (!toolResult) {
|
|
continue;
|
|
}
|
|
|
|
msg.toolResult = {
|
|
content: typeof toolResult.content === 'string'
|
|
? toolResult.content
|
|
: JSON.stringify(toolResult.content),
|
|
isError: toolResult.isError,
|
|
toolUseResult: toolResult.toolUseResult,
|
|
};
|
|
msg.subagentTools = toolResult.subagentTools;
|
|
}
|
|
}
|
|
|
|
return {
|
|
messages: normalized,
|
|
total,
|
|
hasMore,
|
|
offset,
|
|
limit,
|
|
};
|
|
}
|
|
}
|