feat: add CLI config preview API for Codex and Gemini

- Implemented `fetchCodexConfigPreview` and `fetchGeminiConfigPreview` functions in the API layer to retrieve masked configuration files.
- Added new interfaces `CodexConfigPreviewResponse` and `GeminiConfigPreviewResponse` to define the structure of the API responses.
- Created utility functions to read and mask sensitive values from `config.toml` and `auth.json` for Codex, and `settings.json` for Gemini.
- Updated CLI settings routes to handle new preview endpoints.
- Enhanced session content parser to support Claude JSONL format.
- Updated UI components to reflect changes in history page and navigation, including new tabs for observability.
- Localized changes for English and Chinese languages to reflect "CLI History" terminology.
This commit is contained in:
catlog22
2026-02-25 22:37:30 +08:00
parent c92754505a
commit b4d3426e6a
15 changed files with 1137 additions and 163 deletions

View File

@@ -3,6 +3,10 @@
* Handles Claude CLI settings file management API endpoints
*/
import { homedir } from 'os';
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import type { RouteContext } from './types.js';
import {
saveEndpointSettings,
@@ -20,6 +24,129 @@ import type { SaveEndpointRequest, ImportOptions } from '../../types/cli-setting
import { validateSettings } from '../../types/cli-settings.js';
import { syncBuiltinToolsAvailability, getBuiltinToolsSyncReport } from '../../tools/claude-cli-tools.js';
/**
* Mask sensitive values (API keys) for display
* Shows first 8 characters, masks the rest with asterisks
*/
function maskSensitiveValue(value: string): string {
if (!value || value.length <= 8) {
return value;
}
return value.substring(0, 8) + '*'.repeat(value.length - 8);
}
/**
* Mask API keys in JSON content
*/
function maskJsonApiKeys(content: string): string {
try {
const parsed = JSON.parse(content);
if (parsed && typeof parsed === 'object') {
// Mask common API key fields
const sensitiveFields = ['api_key', 'apiKey', 'API_KEY', 'OPENAI_API_KEY', 'token', 'auth_token'];
for (const field of sensitiveFields) {
if (parsed[field] && typeof parsed[field] === 'string') {
parsed[field] = maskSensitiveValue(parsed[field]);
}
}
// Handle nested objects (e.g., api key in providers)
if (parsed.providers && Array.isArray(parsed.providers)) {
parsed.providers = parsed.providers.map((provider: any) => {
if (provider.api_key) {
return { ...provider, api_key: maskSensitiveValue(provider.api_key) };
}
return provider;
});
}
return JSON.stringify(parsed, null, 2);
}
} catch {
// If not valid JSON, return as-is
}
return content;
}
/**
* Mask API keys in TOML content
*/
function maskTomlApiKeys(content: string): string {
// Match patterns like api_key = "value" or apiKey = "value"
return content.replace(
/(api[_-]?key|apiKey|API_KEY|token|auth_token)\s*=\s*["']([^"']+)["']/gi,
(match, key, value) => {
return `${key} = "${maskSensitiveValue(value)}"`;
}
);
}
/**
* Read and mask Codex config files for preview
*/
function readCodexConfigPreview(): { configToml: string | null; authJson: string | null; errors: string[] } {
const home = homedir();
const codexDir = join(home, '.codex');
const result: { configToml: string | null; authJson: string | null; errors: string[] } = {
configToml: null,
authJson: null,
errors: []
};
// Read config.toml
const configTomlPath = join(codexDir, 'config.toml');
if (existsSync(configTomlPath)) {
try {
const content = readFileSync(configTomlPath, 'utf-8');
result.configToml = maskTomlApiKeys(content);
} catch (err) {
result.errors.push(`Failed to read config.toml: ${(err as Error).message}`);
}
} else {
result.errors.push('config.toml not found');
}
// Read auth.json
const authJsonPath = join(codexDir, 'auth.json');
if (existsSync(authJsonPath)) {
try {
const content = readFileSync(authJsonPath, 'utf-8');
result.authJson = maskJsonApiKeys(content);
} catch (err) {
result.errors.push(`Failed to read auth.json: ${(err as Error).message}`);
}
} else {
result.errors.push('auth.json not found');
}
return result;
}
/**
* Read and mask Gemini settings file for preview
*/
function readGeminiConfigPreview(): { settingsJson: string | null; errors: string[] } {
const home = homedir();
const geminiDir = join(home, '.gemini');
const result: { settingsJson: string | null; errors: string[] } = {
settingsJson: null,
errors: []
};
// Read settings.json
const settingsPath = join(geminiDir, 'settings.json');
if (existsSync(settingsPath)) {
try {
const content = readFileSync(settingsPath, 'utf-8');
result.settingsJson = maskJsonApiKeys(content);
} catch (err) {
result.errors.push(`Failed to read settings.json: ${(err as Error).message}`);
}
} else {
result.errors.push('settings.json not found');
}
return result;
}
/**
* Handle CLI Settings routes
* @returns true if route was handled, false otherwise
@@ -334,5 +461,49 @@ export async function handleCliSettingsRoutes(ctx: RouteContext): Promise<boolea
return true;
}
// ========== CODEX CONFIG PREVIEW ==========
// GET /api/cli/settings/codex/preview
if (pathname === '/api/cli/settings/codex/preview' && req.method === 'GET') {
try {
const preview = readCodexConfigPreview();
const home = homedir();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
configPath: join(home, '.codex', 'config.toml'),
authPath: join(home, '.codex', 'auth.json'),
configToml: preview.configToml,
authJson: preview.authJson,
errors: preview.errors.length > 0 ? preview.errors : undefined
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
// ========== GEMINI CONFIG PREVIEW ==========
// GET /api/cli/settings/gemini/preview
if (pathname === '/api/cli/settings/gemini/preview' && req.method === 'GET') {
try {
const preview = readGeminiConfigPreview();
const home = homedir();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
settingsPath: join(home, '.gemini', 'settings.json'),
settingsJson: preview.settingsJson,
errors: preview.errors.length > 0 ? preview.errors : undefined
}));
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: (err as Error).message }));
}
return true;
}
return false;
}