mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-02 10:35:37 +08:00
refactor: make cli-auth part of the providers class
This commit is contained in:
@@ -4,6 +4,7 @@ import { asyncHandler } from '@/shared/http/async-handler.js';
|
|||||||
import { AppError } from '@/shared/utils/app-error.js';
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js';
|
import { createApiErrorResponse, createApiSuccessResponse } from '@/shared/http/api-response.js';
|
||||||
import { llmService } from '@/modules/ai-runtime/services/ai-runtime.service.js';
|
import { llmService } from '@/modules/ai-runtime/services/ai-runtime.service.js';
|
||||||
|
import { llmAuthService } from '@/modules/ai-runtime/services/auth.service.js';
|
||||||
import { llmSessionsService } from '@/modules/ai-runtime/services/sessions.service.js';
|
import { llmSessionsService } from '@/modules/ai-runtime/services/sessions.service.js';
|
||||||
import { llmMcpService } from '@/modules/ai-runtime/services/mcp.service.js';
|
import { llmMcpService } from '@/modules/ai-runtime/services/mcp.service.js';
|
||||||
import { llmSkillsService } from '@/modules/ai-runtime/services/skills.service.js';
|
import { llmSkillsService } from '@/modules/ai-runtime/services/skills.service.js';
|
||||||
@@ -233,6 +234,15 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/providers/:provider/auth/status',
|
||||||
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
const provider = parseProvider(req.params.provider);
|
||||||
|
const auth = await llmAuthService.getProviderAuthStatus(provider);
|
||||||
|
res.json(createApiSuccessResponse({ provider, auth }));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/providers/:provider/sessions',
|
'/providers/:provider/sessions',
|
||||||
asyncHandler(async (req: Request, res: Response) => {
|
asyncHandler(async (req: Request, res: Response) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
IProvider,
|
IProvider,
|
||||||
|
IProviderAuthRuntime,
|
||||||
IProviderMcpRuntime,
|
IProviderMcpRuntime,
|
||||||
IProviderSessionSynchronizerRuntime,
|
IProviderSessionSynchronizerRuntime,
|
||||||
IProviderSkillsRuntime,
|
IProviderSkillsRuntime,
|
||||||
@@ -25,6 +26,7 @@ export abstract class AbstractProvider implements IProvider {
|
|||||||
abstract readonly mcp: IProviderMcpRuntime;
|
abstract readonly mcp: IProviderMcpRuntime;
|
||||||
abstract readonly skills: IProviderSkillsRuntime;
|
abstract readonly skills: IProviderSkillsRuntime;
|
||||||
abstract readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime;
|
abstract readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime;
|
||||||
|
abstract readonly auth: IProviderAuthRuntime;
|
||||||
|
|
||||||
protected readonly sessions = new Map<string, MutableProviderSession>();
|
protected readonly sessions = new Map<string, MutableProviderSession>();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
import type { IProviderAuthRuntime, ProviderAuthStatus } from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
type ClaudeCredentialsFile = {
|
||||||
|
email?: string;
|
||||||
|
user?: string;
|
||||||
|
claudeAiOauth?: {
|
||||||
|
accessToken?: string;
|
||||||
|
expiresAt?: number | string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads auth status for Claude from env/settings and OAuth credentials.
|
||||||
|
*/
|
||||||
|
export class ClaudeAuthRuntime implements IProviderAuthRuntime {
|
||||||
|
async getStatus(): Promise<ProviderAuthStatus> {
|
||||||
|
try {
|
||||||
|
if (process.env.ANTHROPIC_API_KEY?.trim()) {
|
||||||
|
return {
|
||||||
|
provider: 'claude',
|
||||||
|
authenticated: true,
|
||||||
|
email: 'API Key Auth',
|
||||||
|
method: 'api_key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsEnv = await this.loadClaudeSettingsEnv();
|
||||||
|
if (settingsEnv.ANTHROPIC_API_KEY?.trim()) {
|
||||||
|
return {
|
||||||
|
provider: 'claude',
|
||||||
|
authenticated: true,
|
||||||
|
email: 'API Key Auth',
|
||||||
|
method: 'api_key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsEnv.ANTHROPIC_AUTH_TOKEN?.trim()) {
|
||||||
|
return {
|
||||||
|
provider: 'claude',
|
||||||
|
authenticated: true,
|
||||||
|
email: 'Configured via settings.json',
|
||||||
|
method: 'api_key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialsPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
||||||
|
const content = await readFile(credentialsPath, 'utf8');
|
||||||
|
const credentials = JSON.parse(content) as ClaudeCredentialsFile;
|
||||||
|
const oauth = credentials.claudeAiOauth;
|
||||||
|
const accessToken = oauth?.accessToken;
|
||||||
|
|
||||||
|
if (accessToken && !this.isExpired(oauth?.expiresAt)) {
|
||||||
|
return {
|
||||||
|
provider: 'claude',
|
||||||
|
authenticated: true,
|
||||||
|
email: credentials.email ?? credentials.user ?? null,
|
||||||
|
method: 'credentials_file',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: 'claude',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Not authenticated',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
provider: 'claude',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Not authenticated',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads optional env values from ~/.claude/settings.json.
|
||||||
|
*/
|
||||||
|
private async loadClaudeSettingsEnv(): Promise<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
||||||
|
const content = await readFile(settingsPath, 'utf8');
|
||||||
|
const settings = JSON.parse(content) as { env?: unknown };
|
||||||
|
if (!settings.env || typeof settings.env !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(settings.env as Record<string, unknown>).filter(
|
||||||
|
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when an OAuth expiration timestamp is in the past.
|
||||||
|
*/
|
||||||
|
private isExpired(expiresAt: number | string | undefined): boolean {
|
||||||
|
if (expiresAt === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof expiresAt === 'number') {
|
||||||
|
return Date.now() >= expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numeric = Number.parseInt(expiresAt, 10);
|
||||||
|
return Number.isFinite(numeric) ? Date.now() >= numeric : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { readFile } from 'node:fs/promises';
|
|||||||
|
|
||||||
import { BaseSdkProvider } from '@/modules/ai-runtime/providers/base/base-sdk.provider.js';
|
import { BaseSdkProvider } from '@/modules/ai-runtime/providers/base/base-sdk.provider.js';
|
||||||
import type {
|
import type {
|
||||||
|
IProviderAuthRuntime,
|
||||||
IProviderMcpRuntime,
|
IProviderMcpRuntime,
|
||||||
IProviderSessionSynchronizerRuntime,
|
IProviderSessionSynchronizerRuntime,
|
||||||
IProviderSkillsRuntime,
|
IProviderSkillsRuntime,
|
||||||
@@ -18,6 +19,7 @@ import type {
|
|||||||
StartSessionInput,
|
StartSessionInput,
|
||||||
} from '@/modules/ai-runtime/types/index.js';
|
} from '@/modules/ai-runtime/types/index.js';
|
||||||
import { ClaudeMcpRuntime } from '@/modules/ai-runtime/providers/claude/claude-mcp.runtime.js';
|
import { ClaudeMcpRuntime } from '@/modules/ai-runtime/providers/claude/claude-mcp.runtime.js';
|
||||||
|
import { ClaudeAuthRuntime } from '@/modules/ai-runtime/providers/claude/claude-auth.runtime.js';
|
||||||
import { ClaudeSkillsRuntime } from '@/modules/ai-runtime/providers/claude/claude-skills.runtime.js';
|
import { ClaudeSkillsRuntime } from '@/modules/ai-runtime/providers/claude/claude-skills.runtime.js';
|
||||||
import { ClaudeSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/claude/claude-session-synchronizer.runtime.js';
|
import { ClaudeSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/claude/claude-session-synchronizer.runtime.js';
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ const readString = (value: unknown): string | undefined => {
|
|||||||
* Claude SDK provider implementation.
|
* Claude SDK provider implementation.
|
||||||
*/
|
*/
|
||||||
export class ClaudeProvider extends BaseSdkProvider {
|
export class ClaudeProvider extends BaseSdkProvider {
|
||||||
|
readonly auth: IProviderAuthRuntime = new ClaudeAuthRuntime();
|
||||||
readonly mcp: IProviderMcpRuntime = new ClaudeMcpRuntime();
|
readonly mcp: IProviderMcpRuntime = new ClaudeMcpRuntime();
|
||||||
readonly skills: IProviderSkillsRuntime = new ClaudeSkillsRuntime();
|
readonly skills: IProviderSkillsRuntime = new ClaudeSkillsRuntime();
|
||||||
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new ClaudeSessionSynchronizerRuntime();
|
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new ClaudeSessionSynchronizerRuntime();
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
import type { IProviderAuthRuntime, ProviderAuthStatus } from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
type CodexAuthFile = {
|
||||||
|
OPENAI_API_KEY?: string;
|
||||||
|
tokens?: {
|
||||||
|
id_token?: string;
|
||||||
|
access_token?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads auth status from ~/.codex/auth.json.
|
||||||
|
*/
|
||||||
|
export class CodexAuthRuntime implements IProviderAuthRuntime {
|
||||||
|
async getStatus(): Promise<ProviderAuthStatus> {
|
||||||
|
try {
|
||||||
|
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
||||||
|
const content = await readFile(authPath, 'utf8');
|
||||||
|
const auth = JSON.parse(content) as CodexAuthFile;
|
||||||
|
const tokens = auth.tokens ?? {};
|
||||||
|
|
||||||
|
if (tokens.id_token || tokens.access_token) {
|
||||||
|
return {
|
||||||
|
provider: 'codex',
|
||||||
|
authenticated: true,
|
||||||
|
email: this.extractEmail(tokens.id_token),
|
||||||
|
method: 'token_file',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auth.OPENAI_API_KEY?.trim()) {
|
||||||
|
return {
|
||||||
|
provider: 'codex',
|
||||||
|
authenticated: true,
|
||||||
|
email: 'API Key Auth',
|
||||||
|
method: 'api_key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
provider: 'codex',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'No valid tokens found',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const code = (error as NodeJS.ErrnoException)?.code;
|
||||||
|
return {
|
||||||
|
provider: 'codex',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: code === 'ENOENT'
|
||||||
|
? 'Codex not configured'
|
||||||
|
: (error instanceof Error ? error.message : 'Failed to read Codex auth state'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort id_token email extraction from JWT payload.
|
||||||
|
*/
|
||||||
|
private extractEmail(idToken: string | undefined): string {
|
||||||
|
if (!idToken) {
|
||||||
|
return 'Authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parts = idToken.split('.');
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return 'Authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8')) as {
|
||||||
|
email?: string;
|
||||||
|
user?: string;
|
||||||
|
};
|
||||||
|
return payload.email ?? payload.user ?? 'Authenticated';
|
||||||
|
} catch {
|
||||||
|
return 'Authenticated';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { readFile } from 'node:fs/promises';
|
|||||||
|
|
||||||
import { BaseSdkProvider } from '@/modules/ai-runtime/providers/base/base-sdk.provider.js';
|
import { BaseSdkProvider } from '@/modules/ai-runtime/providers/base/base-sdk.provider.js';
|
||||||
import type {
|
import type {
|
||||||
|
IProviderAuthRuntime,
|
||||||
IProviderMcpRuntime,
|
IProviderMcpRuntime,
|
||||||
IProviderSessionSynchronizerRuntime,
|
IProviderSessionSynchronizerRuntime,
|
||||||
IProviderSkillsRuntime,
|
IProviderSkillsRuntime,
|
||||||
@@ -12,6 +13,7 @@ import type {
|
|||||||
StartSessionInput,
|
StartSessionInput,
|
||||||
} from '@/modules/ai-runtime/types/index.js';
|
} from '@/modules/ai-runtime/types/index.js';
|
||||||
import { CodexMcpRuntime } from '@/modules/ai-runtime/providers/codex/codex-mcp.runtime.js';
|
import { CodexMcpRuntime } from '@/modules/ai-runtime/providers/codex/codex-mcp.runtime.js';
|
||||||
|
import { CodexAuthRuntime } from '@/modules/ai-runtime/providers/codex/codex-auth.runtime.js';
|
||||||
import { CodexSkillsRuntime } from '@/modules/ai-runtime/providers/codex/codex-skills.runtime.js';
|
import { CodexSkillsRuntime } from '@/modules/ai-runtime/providers/codex/codex-skills.runtime.js';
|
||||||
import { CodexSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/codex/codex-session-synchronizer.runtime.js';
|
import { CodexSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/codex/codex-session-synchronizer.runtime.js';
|
||||||
import { AppError } from '@/shared/utils/app-error.js';
|
import { AppError } from '@/shared/utils/app-error.js';
|
||||||
@@ -67,6 +69,7 @@ type CodexSdkModule = {
|
|||||||
* Codex SDK provider implementation.
|
* Codex SDK provider implementation.
|
||||||
*/
|
*/
|
||||||
export class CodexProvider extends BaseSdkProvider {
|
export class CodexProvider extends BaseSdkProvider {
|
||||||
|
readonly auth: IProviderAuthRuntime = new CodexAuthRuntime();
|
||||||
readonly mcp: IProviderMcpRuntime = new CodexMcpRuntime();
|
readonly mcp: IProviderMcpRuntime = new CodexMcpRuntime();
|
||||||
readonly skills: IProviderSkillsRuntime = new CodexSkillsRuntime();
|
readonly skills: IProviderSkillsRuntime = new CodexSkillsRuntime();
|
||||||
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new CodexSessionSynchronizerRuntime();
|
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new CodexSessionSynchronizerRuntime();
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import spawn from 'cross-spawn';
|
||||||
|
|
||||||
|
import type { IProviderAuthRuntime, ProviderAuthStatus } from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
const CURSOR_STATUS_TIMEOUT_MS = 5_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads auth status from `cursor-agent status`.
|
||||||
|
*/
|
||||||
|
export class CursorAuthRuntime implements IProviderAuthRuntime {
|
||||||
|
async getStatus(): Promise<ProviderAuthStatus> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let completed = false;
|
||||||
|
let childProcess: ReturnType<typeof spawn> | null = null;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed = true;
|
||||||
|
if (childProcess) {
|
||||||
|
childProcess.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Command timeout',
|
||||||
|
});
|
||||||
|
}, CURSOR_STATUS_TIMEOUT_MS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
childProcess = spawn('cursor-agent', ['status']);
|
||||||
|
} catch {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
completed = true;
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Cursor CLI not found or not installed',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
childProcess.stdout?.on('data', (chunk) => {
|
||||||
|
stdout += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.stderr?.on('data', (chunk) => {
|
||||||
|
stderr += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('close', (code) => {
|
||||||
|
if (completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
const emailMatch = stdout.match(/Logged in as ([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
||||||
|
if (emailMatch) {
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: true,
|
||||||
|
email: emailMatch[1],
|
||||||
|
method: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdout.includes('Logged in')) {
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: true,
|
||||||
|
email: 'Logged in',
|
||||||
|
method: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Not logged in',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: stderr.trim() || 'Not logged in',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('error', () => {
|
||||||
|
if (completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
completed = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve({
|
||||||
|
provider: 'cursor',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Cursor CLI not found or not installed',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BaseCliProvider } from '@/modules/ai-runtime/providers/base/base-cli.provider.js';
|
import { BaseCliProvider } from '@/modules/ai-runtime/providers/base/base-cli.provider.js';
|
||||||
import type {
|
import type {
|
||||||
|
IProviderAuthRuntime,
|
||||||
IProviderMcpRuntime,
|
IProviderMcpRuntime,
|
||||||
IProviderSessionSynchronizerRuntime,
|
IProviderSessionSynchronizerRuntime,
|
||||||
IProviderSkillsRuntime,
|
IProviderSkillsRuntime,
|
||||||
@@ -7,6 +8,7 @@ import type {
|
|||||||
StartSessionInput,
|
StartSessionInput,
|
||||||
} from '@/modules/ai-runtime/types/index.js';
|
} from '@/modules/ai-runtime/types/index.js';
|
||||||
import { CursorMcpRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-mcp.runtime.js';
|
import { CursorMcpRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-mcp.runtime.js';
|
||||||
|
import { CursorAuthRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-auth.runtime.js';
|
||||||
import { CursorSkillsRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-skills.runtime.js';
|
import { CursorSkillsRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-skills.runtime.js';
|
||||||
import { CursorSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-session-synchronizer.runtime.js';
|
import { CursorSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/cursor/cursor-session-synchronizer.runtime.js';
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ const ANSI_REGEX =
|
|||||||
* Cursor CLI provider implementation.
|
* Cursor CLI provider implementation.
|
||||||
*/
|
*/
|
||||||
export class CursorProvider extends BaseCliProvider {
|
export class CursorProvider extends BaseCliProvider {
|
||||||
|
readonly auth: IProviderAuthRuntime = new CursorAuthRuntime();
|
||||||
readonly mcp: IProviderMcpRuntime = new CursorMcpRuntime();
|
readonly mcp: IProviderMcpRuntime = new CursorMcpRuntime();
|
||||||
readonly skills: IProviderSkillsRuntime = new CursorSkillsRuntime();
|
readonly skills: IProviderSkillsRuntime = new CursorSkillsRuntime();
|
||||||
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new CursorSessionSynchronizerRuntime();
|
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new CursorSessionSynchronizerRuntime();
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
import type { IProviderAuthRuntime, ProviderAuthStatus } from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
type GeminiOauthCreds = {
|
||||||
|
access_token?: string;
|
||||||
|
refresh_token?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads auth status from env and Gemini OAuth files.
|
||||||
|
*/
|
||||||
|
export class GeminiAuthRuntime implements IProviderAuthRuntime {
|
||||||
|
async getStatus(): Promise<ProviderAuthStatus> {
|
||||||
|
if (process.env.GEMINI_API_KEY?.trim()) {
|
||||||
|
return {
|
||||||
|
provider: 'gemini',
|
||||||
|
authenticated: true,
|
||||||
|
email: 'API Key Auth',
|
||||||
|
method: 'api_key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credsPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
||||||
|
const content = await readFile(credsPath, 'utf8');
|
||||||
|
const creds = JSON.parse(content) as GeminiOauthCreds;
|
||||||
|
if (!creds.access_token) {
|
||||||
|
return {
|
||||||
|
provider: 'gemini',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'No valid tokens found in oauth_creds',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const validated = await this.resolveEmailFromAccessToken(creds.access_token);
|
||||||
|
if (validated.email) {
|
||||||
|
return {
|
||||||
|
provider: 'gemini',
|
||||||
|
authenticated: true,
|
||||||
|
email: validated.email,
|
||||||
|
method: 'oauth',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validated.tokenValid && !creds.refresh_token) {
|
||||||
|
return {
|
||||||
|
provider: 'gemini',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Access token invalid and no refresh token found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackEmail = await this.readActiveGoogleAccountEmail();
|
||||||
|
return {
|
||||||
|
provider: 'gemini',
|
||||||
|
authenticated: true,
|
||||||
|
email: fallbackEmail ?? 'OAuth Session',
|
||||||
|
method: 'oauth',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
provider: 'gemini',
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
method: null,
|
||||||
|
error: 'Gemini CLI not configured',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates token and extracts email via Google's tokeninfo endpoint.
|
||||||
|
*/
|
||||||
|
private async resolveEmailFromAccessToken(
|
||||||
|
accessToken: string,
|
||||||
|
): Promise<{ tokenValid: boolean; email: string | null }> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${accessToken}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
return { tokenValid: false, email: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenInfo = await response.json() as { email?: string };
|
||||||
|
return {
|
||||||
|
tokenValid: true,
|
||||||
|
email: tokenInfo.email ?? null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { tokenValid: false, email: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads active Google account email from ~/.gemini/google_accounts.json.
|
||||||
|
*/
|
||||||
|
private async readActiveGoogleAccountEmail(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const accountsPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
||||||
|
const content = await readFile(accountsPath, 'utf8');
|
||||||
|
const accounts = JSON.parse(content) as { active?: string };
|
||||||
|
return typeof accounts.active === 'string' && accounts.active.trim()
|
||||||
|
? accounts.active
|
||||||
|
: null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { BaseCliProvider } from '@/modules/ai-runtime/providers/base/base-cli.provider.js';
|
import { BaseCliProvider } from '@/modules/ai-runtime/providers/base/base-cli.provider.js';
|
||||||
import type {
|
import type {
|
||||||
|
IProviderAuthRuntime,
|
||||||
IProviderMcpRuntime,
|
IProviderMcpRuntime,
|
||||||
IProviderSessionSynchronizerRuntime,
|
IProviderSessionSynchronizerRuntime,
|
||||||
IProviderSkillsRuntime,
|
IProviderSkillsRuntime,
|
||||||
@@ -7,6 +8,7 @@ import type {
|
|||||||
StartSessionInput,
|
StartSessionInput,
|
||||||
} from '@/modules/ai-runtime/types/index.js';
|
} from '@/modules/ai-runtime/types/index.js';
|
||||||
import { GeminiMcpRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-mcp.runtime.js';
|
import { GeminiMcpRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-mcp.runtime.js';
|
||||||
|
import { GeminiAuthRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-auth.runtime.js';
|
||||||
import { GeminiSkillsRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-skills.runtime.js';
|
import { GeminiSkillsRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-skills.runtime.js';
|
||||||
import { GeminiSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-session-synchronizer.runtime.js';
|
import { GeminiSessionSynchronizerRuntime } from '@/modules/ai-runtime/providers/gemini/gemini-session-synchronizer.runtime.js';
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ const GEMINI_MODELS: ProviderModel[] = [
|
|||||||
* Gemini CLI provider implementation.
|
* Gemini CLI provider implementation.
|
||||||
*/
|
*/
|
||||||
export class GeminiProvider extends BaseCliProvider {
|
export class GeminiProvider extends BaseCliProvider {
|
||||||
|
readonly auth: IProviderAuthRuntime = new GeminiAuthRuntime();
|
||||||
readonly mcp: IProviderMcpRuntime = new GeminiMcpRuntime();
|
readonly mcp: IProviderMcpRuntime = new GeminiMcpRuntime();
|
||||||
readonly skills: IProviderSkillsRuntime = new GeminiSkillsRuntime();
|
readonly skills: IProviderSkillsRuntime = new GeminiSkillsRuntime();
|
||||||
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new GeminiSessionSynchronizerRuntime();
|
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime = new GeminiSessionSynchronizerRuntime();
|
||||||
|
|||||||
12
server/src/modules/ai-runtime/services/auth.service.ts
Normal file
12
server/src/modules/ai-runtime/services/auth.service.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { llmProviderRegistry } from '@/modules/ai-runtime/ai-runtime.registry.js';
|
||||||
|
import type { ProviderAuthStatus } from '@/modules/ai-runtime/types/index.js';
|
||||||
|
|
||||||
|
export const llmAuthService = {
|
||||||
|
/**
|
||||||
|
* Returns auth status for one provider.
|
||||||
|
*/
|
||||||
|
async getProviderAuthStatus(providerName: string): Promise<ProviderAuthStatus> {
|
||||||
|
const provider = llmProviderRegistry.resolveProvider(providerName);
|
||||||
|
return provider.auth.getStatus();
|
||||||
|
},
|
||||||
|
};
|
||||||
19
server/src/modules/ai-runtime/types/auth.types.ts
Normal file
19
server/src/modules/ai-runtime/types/auth.types.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { LLMProvider } from '@/shared/types/app.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider authentication status normalized for frontend consumption.
|
||||||
|
*/
|
||||||
|
export type ProviderAuthStatus = {
|
||||||
|
provider: LLMProvider;
|
||||||
|
authenticated: boolean;
|
||||||
|
email: string | null;
|
||||||
|
method: string | null;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth runtime contract for one provider.
|
||||||
|
*/
|
||||||
|
export interface IProviderAuthRuntime {
|
||||||
|
getStatus(): Promise<ProviderAuthStatus>;
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export * from '@/modules/ai-runtime/types/provider.types.js';
|
|||||||
export * from '@/modules/ai-runtime/types/mcp.types.js';
|
export * from '@/modules/ai-runtime/types/mcp.types.js';
|
||||||
export * from '@/modules/ai-runtime/types/skills.types.js';
|
export * from '@/modules/ai-runtime/types/skills.types.js';
|
||||||
export * from '@/modules/ai-runtime/types/session-synchronizer.types.js';
|
export * from '@/modules/ai-runtime/types/session-synchronizer.types.js';
|
||||||
|
export * from '@/modules/ai-runtime/types/auth.types.js';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { LLMProvider } from '@/shared/types/app.js';
|
|||||||
import type { IProviderMcpRuntime } from '@/modules/ai-runtime/types/mcp.types.js';
|
import type { IProviderMcpRuntime } from '@/modules/ai-runtime/types/mcp.types.js';
|
||||||
import type { IProviderSkillsRuntime } from '@/modules/ai-runtime/types/skills.types.js';
|
import type { IProviderSkillsRuntime } from '@/modules/ai-runtime/types/skills.types.js';
|
||||||
import type { IProviderSessionSynchronizerRuntime } from '@/modules/ai-runtime/types/session-synchronizer.types.js';
|
import type { IProviderSessionSynchronizerRuntime } from '@/modules/ai-runtime/types/session-synchronizer.types.js';
|
||||||
|
import type { IProviderAuthRuntime } from '@/modules/ai-runtime/types/auth.types.js';
|
||||||
|
|
||||||
export type ProviderExecutionFamily = 'sdk' | 'cli';
|
export type ProviderExecutionFamily = 'sdk' | 'cli';
|
||||||
|
|
||||||
@@ -80,6 +81,7 @@ export interface IProvider {
|
|||||||
readonly mcp: IProviderMcpRuntime;
|
readonly mcp: IProviderMcpRuntime;
|
||||||
readonly skills: IProviderSkillsRuntime;
|
readonly skills: IProviderSkillsRuntime;
|
||||||
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime;
|
readonly sessionSynchronizer: IProviderSessionSynchronizerRuntime;
|
||||||
|
readonly auth: IProviderAuthRuntime;
|
||||||
|
|
||||||
listModels(): Promise<ProviderModel[]>;
|
listModels(): Promise<ProviderModel[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,434 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import spawn from 'cross-spawn';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
import os from 'os';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/claude/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const credentialsResult = await checkClaudeCredentials();
|
|
||||||
|
|
||||||
if (credentialsResult.authenticated) {
|
|
||||||
return res.json({
|
|
||||||
authenticated: true,
|
|
||||||
email: credentialsResult.email || 'Authenticated',
|
|
||||||
method: credentialsResult.method // 'api_key' or 'credentials_file'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
method: null,
|
|
||||||
error: credentialsResult.error || 'Not authenticated'
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking Claude auth status:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
method: null,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/cursor/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await checkCursorStatus();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
authenticated: result.authenticated,
|
|
||||||
email: result.email,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking Cursor auth status:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/codex/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await checkCodexCredentials();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
authenticated: result.authenticated,
|
|
||||||
email: result.email,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking Codex auth status:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/gemini/status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const result = await checkGeminiCredentials();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
authenticated: result.authenticated,
|
|
||||||
email: result.email,
|
|
||||||
error: result.error
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking Gemini auth status:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadClaudeSettingsEnv() {
|
|
||||||
try {
|
|
||||||
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
||||||
const content = await fs.readFile(settingsPath, 'utf8');
|
|
||||||
const settings = JSON.parse(content);
|
|
||||||
|
|
||||||
if (settings?.env && typeof settings.env === 'object') {
|
|
||||||
return settings.env;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore missing or malformed settings and fall back to other auth sources.
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks Claude authentication credentials using two methods with priority order:
|
|
||||||
*
|
|
||||||
* Priority 1: ANTHROPIC_API_KEY environment variable
|
|
||||||
* Priority 1b: ~/.claude/settings.json env values
|
|
||||||
* Priority 2: ~/.claude/.credentials.json OAuth tokens
|
|
||||||
*
|
|
||||||
* The Claude Agent SDK prioritizes environment variables over authenticated subscriptions.
|
|
||||||
* This matching behavior ensures consistency with how the SDK authenticates.
|
|
||||||
*
|
|
||||||
* References:
|
|
||||||
* - https://support.claude.com/en/articles/12304248-managing-api-key-environment-variables-in-claude-code
|
|
||||||
* "Claude Code prioritizes environment variable API keys over authenticated subscriptions"
|
|
||||||
* - https://platform.claude.com/docs/en/agent-sdk/overview
|
|
||||||
* SDK authentication documentation
|
|
||||||
*
|
|
||||||
* @returns {Promise<Object>} Authentication status with { authenticated, email, method }
|
|
||||||
* - authenticated: boolean indicating if valid credentials exist
|
|
||||||
* - email: user email or auth method identifier
|
|
||||||
* - method: 'api_key' for env var, 'credentials_file' for OAuth tokens
|
|
||||||
*/
|
|
||||||
async function checkClaudeCredentials() {
|
|
||||||
// Priority 1: Check for ANTHROPIC_API_KEY environment variable
|
|
||||||
// The SDK checks this first and uses it if present, even if OAuth tokens exist.
|
|
||||||
// When set, API calls are charged via pay-as-you-go rates instead of subscription.
|
|
||||||
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: 'API Key Auth',
|
|
||||||
method: 'api_key'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 1b: Check ~/.claude/settings.json env values.
|
|
||||||
// Claude Code can read proxy/auth values from settings.json even when the
|
|
||||||
// CloudCLI server process itself was not started with those env vars exported.
|
|
||||||
const settingsEnv = await loadClaudeSettingsEnv();
|
|
||||||
|
|
||||||
if (typeof settingsEnv.ANTHROPIC_API_KEY === 'string' && settingsEnv.ANTHROPIC_API_KEY.trim()) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: 'API Key Auth',
|
|
||||||
method: 'api_key'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof settingsEnv.ANTHROPIC_AUTH_TOKEN === 'string' && settingsEnv.ANTHROPIC_AUTH_TOKEN.trim()) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: 'Configured via settings.json',
|
|
||||||
method: 'api_key'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Check ~/.claude/.credentials.json for OAuth tokens
|
|
||||||
// This is the standard authentication method used by Claude CLI after running
|
|
||||||
// 'claude /login' or 'claude setup-token' commands.
|
|
||||||
try {
|
|
||||||
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
||||||
const content = await fs.readFile(credPath, 'utf8');
|
|
||||||
const creds = JSON.parse(content);
|
|
||||||
|
|
||||||
const oauth = creds.claudeAiOauth;
|
|
||||||
if (oauth && oauth.accessToken) {
|
|
||||||
const isExpired = oauth.expiresAt && Date.now() >= oauth.expiresAt;
|
|
||||||
|
|
||||||
if (!isExpired) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: creds.email || creds.user || null,
|
|
||||||
method: 'credentials_file'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
method: null
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
method: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkCursorStatus() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let processCompleted = false;
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
if (!processCompleted) {
|
|
||||||
processCompleted = true;
|
|
||||||
if (childProcess) {
|
|
||||||
childProcess.kill();
|
|
||||||
}
|
|
||||||
resolve({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Command timeout'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
let childProcess;
|
|
||||||
try {
|
|
||||||
childProcess = spawn('cursor-agent', ['status']);
|
|
||||||
} catch (err) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
processCompleted = true;
|
|
||||||
resolve({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Cursor CLI not found or not installed'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
|
|
||||||
childProcess.stdout.on('data', (data) => {
|
|
||||||
stdout += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.stderr.on('data', (data) => {
|
|
||||||
stderr += data.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.on('close', (code) => {
|
|
||||||
if (processCompleted) return;
|
|
||||||
processCompleted = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
const emailMatch = stdout.match(/Logged in as ([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
|
||||||
|
|
||||||
if (emailMatch) {
|
|
||||||
resolve({
|
|
||||||
authenticated: true,
|
|
||||||
email: emailMatch[1],
|
|
||||||
output: stdout
|
|
||||||
});
|
|
||||||
} else if (stdout.includes('Logged in')) {
|
|
||||||
resolve({
|
|
||||||
authenticated: true,
|
|
||||||
email: 'Logged in',
|
|
||||||
output: stdout
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Not logged in'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: stderr || 'Not logged in'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
childProcess.on('error', (err) => {
|
|
||||||
if (processCompleted) return;
|
|
||||||
processCompleted = true;
|
|
||||||
clearTimeout(timeout);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Cursor CLI not found or not installed'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkCodexCredentials() {
|
|
||||||
try {
|
|
||||||
const authPath = path.join(os.homedir(), '.codex', 'auth.json');
|
|
||||||
const content = await fs.readFile(authPath, 'utf8');
|
|
||||||
const auth = JSON.parse(content);
|
|
||||||
|
|
||||||
// Tokens are nested under 'tokens' key
|
|
||||||
const tokens = auth.tokens || {};
|
|
||||||
|
|
||||||
// Check for valid tokens (id_token or access_token)
|
|
||||||
if (tokens.id_token || tokens.access_token) {
|
|
||||||
// Try to extract email from id_token JWT payload
|
|
||||||
let email = 'Authenticated';
|
|
||||||
if (tokens.id_token) {
|
|
||||||
try {
|
|
||||||
// JWT is base64url encoded: header.payload.signature
|
|
||||||
const parts = tokens.id_token.split('.');
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
// Decode the payload (second part)
|
|
||||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
||||||
email = payload.email || payload.user || 'Authenticated';
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// If JWT decoding fails, use fallback
|
|
||||||
email = 'Authenticated';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for OPENAI_API_KEY as fallback auth method
|
|
||||||
if (auth.OPENAI_API_KEY) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: 'API Key Auth'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'No valid tokens found'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === 'ENOENT') {
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Codex not configured'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkGeminiCredentials() {
|
|
||||||
if (process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY.trim()) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: 'API Key Auth'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const credsPath = path.join(os.homedir(), '.gemini', 'oauth_creds.json');
|
|
||||||
const content = await fs.readFile(credsPath, 'utf8');
|
|
||||||
const creds = JSON.parse(content);
|
|
||||||
|
|
||||||
if (creds.access_token) {
|
|
||||||
let email = 'OAuth Session';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate token against Google API
|
|
||||||
const tokenRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?access_token=${creds.access_token}`);
|
|
||||||
if (tokenRes.ok) {
|
|
||||||
const tokenInfo = await tokenRes.json();
|
|
||||||
if (tokenInfo.email) {
|
|
||||||
email = tokenInfo.email;
|
|
||||||
}
|
|
||||||
} else if (!creds.refresh_token) {
|
|
||||||
// Token invalid and no refresh token available
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Access token invalid and no refresh token found'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Token might be expired but we have a refresh token, so CLI will refresh it
|
|
||||||
try {
|
|
||||||
const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
||||||
const accContent = await fs.readFile(accPath, 'utf8');
|
|
||||||
const accounts = JSON.parse(accContent);
|
|
||||||
if (accounts.active) {
|
|
||||||
email = accounts.active;
|
|
||||||
}
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Network error, fallback to checking local accounts file
|
|
||||||
try {
|
|
||||||
const accPath = path.join(os.homedir(), '.gemini', 'google_accounts.json');
|
|
||||||
const accContent = await fs.readFile(accPath, 'utf8');
|
|
||||||
const accounts = JSON.parse(accContent);
|
|
||||||
if (accounts.active) {
|
|
||||||
email = accounts.active;
|
|
||||||
}
|
|
||||||
} catch (err) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
email: email
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'No valid tokens found in oauth_creds'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
email: null,
|
|
||||||
error: 'Gemini CLI not configured'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -56,7 +56,6 @@ const [
|
|||||||
pushSubRoutes,
|
pushSubRoutes,
|
||||||
agentRoutes,
|
agentRoutes,
|
||||||
projectsRoutes,
|
projectsRoutes,
|
||||||
cliAuthRoutes,
|
|
||||||
codexRoutes,
|
codexRoutes,
|
||||||
geminiRoutes,
|
geminiRoutes,
|
||||||
pluginsRoutes,
|
pluginsRoutes,
|
||||||
@@ -83,7 +82,6 @@ const [
|
|||||||
importRoute('./modules/push-sub/push-sub.routes.js'),
|
importRoute('./modules/push-sub/push-sub.routes.js'),
|
||||||
importRoute('./modules/agent/agent.routes.js'),
|
importRoute('./modules/agent/agent.routes.js'),
|
||||||
importRoute('./modules/projects/projects.routes.js'),
|
importRoute('./modules/projects/projects.routes.js'),
|
||||||
importRoute('./modules/cli-auth/cli-auth.routes.js'),
|
|
||||||
importRoute('./modules/codex/codex.routes.js'),
|
importRoute('./modules/codex/codex.routes.js'),
|
||||||
importRoute('./modules/gemini/gemini.routes.js'),
|
importRoute('./modules/gemini/gemini.routes.js'),
|
||||||
importRoute('./modules/plugins/plugins.routes.js'),
|
importRoute('./modules/plugins/plugins.routes.js'),
|
||||||
@@ -162,9 +160,6 @@ app.use('/api/credentials', authenticateToken, credentialsRoutes);
|
|||||||
app.use('/api/notification-preferences', authenticateToken, notificationPreferencesRoutes);
|
app.use('/api/notification-preferences', authenticateToken, notificationPreferencesRoutes);
|
||||||
app.use('/api/push-sub', authenticateToken, pushSubRoutes);
|
app.use('/api/push-sub', authenticateToken, pushSubRoutes);
|
||||||
|
|
||||||
// CLI Authentication API Routes (protected)
|
|
||||||
app.use('/api/cli', authenticateToken, cliAuthRoutes);
|
|
||||||
|
|
||||||
// User API Routes (protected)
|
// User API Routes (protected)
|
||||||
app.use('/api/user', authenticateToken, userRoutes);
|
app.use('/api/user', authenticateToken, userRoutes);
|
||||||
|
|
||||||
|
|||||||
138
src/App.tsx
138
src/App.tsx
@@ -1,11 +1,8 @@
|
|||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Navigate,
|
Navigate,
|
||||||
Outlet,
|
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
useLocation,
|
|
||||||
useParams,
|
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { AuthProvider, ProtectedRoute } from './components/auth';
|
import { AuthProvider, ProtectedRoute } from './components/auth';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
@@ -21,95 +18,8 @@ import FileTreeRouterAdapter from '@/components/file-tree/view/FileTreeRouterAda
|
|||||||
import GitPanelRouterAdapter from '@/components/git-panel/view/GitPanelRouterAdapter.js';
|
import GitPanelRouterAdapter from '@/components/git-panel/view/GitPanelRouterAdapter.js';
|
||||||
import { TaskMasterPanel } from '@/components/task-master/index.js';
|
import { TaskMasterPanel } from '@/components/task-master/index.js';
|
||||||
import PluginContentRouterAdapter from '@/components/plugins/view/PluginContentRouterAdapter.js';
|
import PluginContentRouterAdapter from '@/components/plugins/view/PluginContentRouterAdapter.js';
|
||||||
import ChatInterface from '@/components/refactored/chat/view/ChatInterface.js';
|
|
||||||
import ChooseWorkspaceView from '@/components/refactored/shared/view/ChooseWorkspaceView.js';
|
import ChooseWorkspaceView from '@/components/refactored/shared/view/ChooseWorkspaceView.js';
|
||||||
|
|
||||||
const isValidRouteTab = (value: string | undefined): boolean => {
|
|
||||||
if (!value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedValue = decodeURIComponent(value);
|
|
||||||
return (
|
|
||||||
normalizedValue === 'chat' ||
|
|
||||||
normalizedValue === 'shell' ||
|
|
||||||
normalizedValue === 'files' ||
|
|
||||||
normalizedValue === 'git' ||
|
|
||||||
normalizedValue === 'tasks' ||
|
|
||||||
normalizedValue === 'plugins' ||
|
|
||||||
normalizedValue === 'preview' ||
|
|
||||||
normalizedValue.startsWith('plugin:')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function NoWorkspaceRoute() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center p-8">
|
|
||||||
<div className="max-w-md space-y-2 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold">Choose Your Project</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
This is the root route (`/`) empty state. Select a workspace from the sidebar to continue.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspaceLayout() {
|
|
||||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
|
||||||
if (!workspaceId) {
|
|
||||||
return <Navigate to="/" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkspaceTabRoute() {
|
|
||||||
const location = useLocation();
|
|
||||||
const { workspaceId, sessionId, tab } = useParams<{
|
|
||||||
workspaceId: string;
|
|
||||||
sessionId?: string;
|
|
||||||
tab: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
if (!workspaceId && !sessionId) {
|
|
||||||
return <Navigate to="/" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidRouteTab(tab)) {
|
|
||||||
return <Navigate to="../chat" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedWorkspaceId = workspaceId ? decodeURIComponent(workspaceId) : null;
|
|
||||||
const decodedSessionId = sessionId ? decodeURIComponent(sessionId) : null;
|
|
||||||
const decodedTab = tab ? decodeURIComponent(tab) : 'chat';
|
|
||||||
const pluginName = decodeURIComponent(new URLSearchParams(location.search).get('name') || '');
|
|
||||||
const tabLabel = decodedTab === 'plugins' && pluginName ? `plugin:${pluginName}` : decodedTab;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full p-6">
|
|
||||||
<div className="rounded-xl border border-border/70 bg-card/30 p-5">
|
|
||||||
<h2 className="text-lg font-semibold">{tabLabel} view</h2>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
Workspace:{' '}
|
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
{decodedWorkspaceId || 'none (session-level route)'}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
Session:{' '}
|
|
||||||
<span className="font-medium text-foreground">
|
|
||||||
{decodedSessionId || 'none (workspace-level tab)'}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = createBrowserRouter(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
@@ -120,16 +30,14 @@ const router = createBrowserRouter(
|
|||||||
{ index: true, element: <ChooseWorkspaceView /> }, // TODO: Show empty state component loader here.
|
{ index: true, element: <ChooseWorkspaceView /> }, // TODO: Show empty state component loader here.
|
||||||
{
|
{
|
||||||
path: 'workspaces/:workspaceId',
|
path: 'workspaces/:workspaceId',
|
||||||
element: <WorkspaceLayout />,
|
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="chat" replace /> },
|
{ index: true, element: <Navigate to="chat" replace /> },
|
||||||
{ path: 'chat', element: <ChatInterface /> },
|
|
||||||
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
|
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
|
||||||
{ path: 'files', element: <FileTreeRouterAdapter /> },
|
{ path: 'files', element: <FileTreeRouterAdapter /> },
|
||||||
{ path: 'git', element: <GitPanelRouterAdapter /> },
|
{ path: 'git', element: <GitPanelRouterAdapter /> },
|
||||||
{ path: 'tasks', element: <TaskMasterPanel isVisible={true} /> },
|
{ path: 'tasks', element: <TaskMasterPanel isVisible={true} /> },
|
||||||
{ path: 'plugins', element: <PluginContentRouterAdapter /> },
|
{ path: 'plugins', element: <PluginContentRouterAdapter /> },
|
||||||
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
{ path: 'chat', element: <h1>Sample component</h1>}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -137,7 +45,11 @@ const router = createBrowserRouter(
|
|||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="chat" replace /> },
|
{ index: true, element: <Navigate to="chat" replace /> },
|
||||||
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
|
{ path: 'shell', element: <StandaloneShellRouterAdapter /> },
|
||||||
{ path: ':tab', element: <WorkspaceTabRoute /> },
|
{ path: 'files', element: <FileTreeRouterAdapter /> },
|
||||||
|
{ path: 'git', element: <GitPanelRouterAdapter /> },
|
||||||
|
{ path: 'tasks', element: <TaskMasterPanel isVisible={true} /> },
|
||||||
|
{ path: 'plugins', element: <PluginContentRouterAdapter /> },
|
||||||
|
{ path: 'chat', element: <h1>Sample component</h1>}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -169,41 +81,3 @@ export default function App() {
|
|||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
|
||||||
// import { I18nextProvider } from 'react-i18next';
|
|
||||||
// import { ThemeProvider } from './contexts/ThemeContext';
|
|
||||||
// import { AuthProvider, ProtectedRoute } from './components/auth';
|
|
||||||
// import { TaskMasterProvider } from './contexts/TaskMasterContext';
|
|
||||||
// import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
|
|
||||||
// import { WebSocketProvider } from './contexts/WebSocketContext';
|
|
||||||
// import { PluginsProvider } from './contexts/PluginsContext';
|
|
||||||
// import AppContent from './components/app/AppContent';
|
|
||||||
// import i18n from './i18n/config.js';
|
|
||||||
|
|
||||||
// export default function App() {
|
|
||||||
// return (
|
|
||||||
// <I18nextProvider i18n={i18n}>
|
|
||||||
// <ThemeProvider>
|
|
||||||
// <AuthProvider>
|
|
||||||
// <WebSocketProvider>
|
|
||||||
// 1<PluginsProvider>
|
|
||||||
// <TasksSettingsProvider>
|
|
||||||
// <TaskMasterProvider>
|
|
||||||
// <ProtectedRoute>
|
|
||||||
// <Router basename={window.__ROUTER_BASENAME__ || ''}>
|
|
||||||
// <Routes>
|
|
||||||
// <Route path="/" element={<AppContent />} />
|
|
||||||
// <Route path="/session/:sessionId" element={<AppContent />} />
|
|
||||||
// </Routes>
|
|
||||||
// </Router>
|
|
||||||
// </ProtectedRoute>
|
|
||||||
// </TaskMasterProvider>
|
|
||||||
// </TasksSettingsProvider>
|
|
||||||
// </PluginsProvider>
|
|
||||||
// </WebSocketProvider>
|
|
||||||
// </AuthProvider>
|
|
||||||
// </ThemeProvider>
|
|
||||||
// </I18nextProvider>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
|||||||
|
|
||||||
const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => {
|
const checkProviderAuthStatus = useCallback(async (provider: CliProvider) => {
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(`/api/cli/${provider}/status`);
|
const response = await authenticatedFetch(`/api/llm/providers/${provider}/auth/status`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
setProviderStatuses((previous) => ({
|
setProviderStatuses((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
@@ -46,18 +46,39 @@ export default function Onboarding({ onComplete }: OnboardingProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = (await response.json()) as {
|
const payload = (await response.json()) as {
|
||||||
authenticated?: boolean;
|
success?: boolean;
|
||||||
email?: string | null;
|
data?: {
|
||||||
error?: string | null;
|
auth?: {
|
||||||
|
authenticated?: boolean;
|
||||||
|
email?: string | null;
|
||||||
|
error?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
const auth = payload.data?.auth;
|
||||||
|
if (!payload.success || !auth) {
|
||||||
|
setProviderStatuses((previous) => ({
|
||||||
|
...previous,
|
||||||
|
[provider]: {
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: false,
|
||||||
|
error: payload.error?.message ?? 'Failed to parse authentication status',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setProviderStatuses((previous) => ({
|
setProviderStatuses((previous) => ({
|
||||||
...previous,
|
...previous,
|
||||||
[provider]: {
|
[provider]: {
|
||||||
authenticated: Boolean(payload.authenticated),
|
authenticated: Boolean(auth.authenticated),
|
||||||
email: payload.email ?? null,
|
email: auth.email ?? null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: payload.error ?? null,
|
error: auth.error ?? null,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} catch (caughtError) {
|
} catch (caughtError) {
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
// Create a sample component
|
|
||||||
|
|
||||||
// TODO: Place this in a shared folder
|
|
||||||
import ErrorBoundary from "@/components/main-content/view/ErrorBoundary.js";
|
|
||||||
import { QuickSettingsPanel } from "@/components/quick-settings-panel/index.js";
|
|
||||||
|
|
||||||
export default function ChatInterface() {
|
|
||||||
return (
|
|
||||||
<div className={`h-full`}>
|
|
||||||
<ErrorBoundary showDetails>
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<p className="text-gray-500">Chat interface goes here</p>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
|
||||||
|
|
||||||
<QuickSettingsPanel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -80,8 +80,8 @@ export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AUTH_STATUS_ENDPOINTS: Record<AgentProvider, string> = {
|
export const AUTH_STATUS_ENDPOINTS: Record<AgentProvider, string> = {
|
||||||
claude: '/api/cli/claude/status',
|
claude: '/api/llm/providers/claude/auth/status',
|
||||||
cursor: '/api/cli/cursor/status',
|
cursor: '/api/llm/providers/cursor/auth/status',
|
||||||
codex: '/api/cli/codex/status',
|
codex: '/api/llm/providers/codex/auth/status',
|
||||||
gemini: '/api/cli/gemini/status',
|
gemini: '/api/llm/providers/gemini/auth/status',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,11 +33,22 @@ type UseSettingsControllerArgs = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StatusApiResponse = {
|
type AuthStatusPayload = {
|
||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
method?: string;
|
method?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StatusApiResponse = {
|
||||||
|
success?: boolean;
|
||||||
|
data?: {
|
||||||
|
provider?: AgentProvider;
|
||||||
|
auth?: AuthStatusPayload;
|
||||||
|
};
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type JsonResult = {
|
type JsonResult = {
|
||||||
@@ -249,13 +260,24 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await toResponseJson<StatusApiResponse>(response);
|
const payload = await toResponseJson<StatusApiResponse>(response);
|
||||||
|
const data = payload.data?.auth;
|
||||||
|
if (!payload.success || !data) {
|
||||||
|
setAuthStatusByProvider(provider, {
|
||||||
|
authenticated: false,
|
||||||
|
email: null,
|
||||||
|
loading: false,
|
||||||
|
error: payload.error?.message || 'Failed to parse authentication status',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setAuthStatusByProvider(provider, {
|
setAuthStatusByProvider(provider, {
|
||||||
authenticated: Boolean(data.authenticated),
|
authenticated: Boolean(data.authenticated),
|
||||||
email: data.email || null,
|
email: data.email || null,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: data.error || null,
|
error: data.error || null,
|
||||||
method: data.method,
|
method: data.method ?? undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error checking ${provider} auth status:`, error);
|
console.error(`Error checking ${provider} auth status:`, error);
|
||||||
|
|||||||
Reference in New Issue
Block a user