mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user