feat: Enhance CLI tools and settings management

- Added auto-initialization of CSRF token for state-changing requests in cli-manager.js.
- Refactored Claude CLI Tools configuration to separate tools and settings into cli-tools.json and cli-settings.json respectively.
- Introduced new interfaces for Claude CLI Tools and Settings, including support for tags and primary models.
- Implemented loading and saving functions for CLI settings, ensuring backward compatibility with legacy combined config.
- Updated functions to synchronize tags between CLI tools and configuration manager.
- Added error handling and logging for loading and saving configurations.
- Created initial cli-settings.json with default settings.
This commit is contained in:
catlog22
2026-01-08 22:00:07 +08:00
parent 0bd2cff5b7
commit ea5c0bc9a4
12 changed files with 1376 additions and 180 deletions

View File

@@ -1,8 +1,9 @@
/**
* Claude CLI Tools Configuration Manager
* Manages .claude/cli-tools.json with fallback:
* 1. Project workspace: {projectDir}/.claude/cli-tools.json (priority)
* 2. Global: ~/.claude/cli-tools.json (fallback)
* Manages .claude/cli-tools.json (tools) and .claude/cli-settings.json (settings)
* with fallback:
* 1. Project workspace: {projectDir}/.claude/ (priority)
* 2. Global: ~/.claude/ (fallback)
*/
import * as fs from 'fs';
import * as path from 'path';
@@ -15,6 +16,15 @@ export interface ClaudeCliTool {
isBuiltin: boolean;
command: string;
description: string;
primaryModel?: string;
tags: string[];
}
export interface ClaudeCustomEndpoint {
id: string;
name: string;
enabled: boolean;
tags: string[];
}
export interface ClaudeCacheSettings {
@@ -23,76 +33,107 @@ export interface ClaudeCacheSettings {
defaultSuffix: string;
}
// New: Tools-only config (cli-tools.json)
export interface ClaudeCliToolsConfig {
$schema?: string;
version: string;
tools: Record<string, ClaudeCliTool>;
customEndpoints: Array<{
id: string;
name: string;
enabled: boolean;
}>;
customEndpoints: ClaudeCustomEndpoint[];
}
// New: Settings-only config (cli-settings.json)
export interface ClaudeCliSettingsConfig {
$schema?: string;
version: string;
defaultTool: string;
settings: {
promptFormat: 'plain' | 'yaml' | 'json';
smartContext: {
enabled: boolean;
maxFiles: number;
promptFormat: 'plain' | 'yaml' | 'json';
smartContext: {
enabled: boolean;
maxFiles: number;
};
nativeResume: boolean;
recursiveQuery: boolean;
cache: ClaudeCacheSettings;
codeIndexMcp: 'codexlens' | 'ace' | 'none';
}
// Legacy combined config (for backward compatibility)
export interface ClaudeCliCombinedConfig extends ClaudeCliToolsConfig {
defaultTool?: string;
settings?: {
promptFormat?: 'plain' | 'yaml' | 'json';
smartContext?: {
enabled?: boolean;
maxFiles?: number;
};
nativeResume: boolean;
recursiveQuery: boolean;
cache: ClaudeCacheSettings;
codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
nativeResume?: boolean;
recursiveQuery?: boolean;
cache?: Partial<ClaudeCacheSettings>;
codeIndexMcp?: 'codexlens' | 'ace' | 'none';
};
}
// ========== Default Config ==========
const DEFAULT_CONFIG: ClaudeCliToolsConfig = {
version: '1.0.0',
const DEFAULT_TOOLS_CONFIG: ClaudeCliToolsConfig = {
version: '2.0.0',
tools: {
gemini: {
enabled: true,
isBuiltin: true,
command: 'gemini',
description: 'Google AI for code analysis'
description: 'Google AI for code analysis',
tags: []
},
qwen: {
enabled: true,
isBuiltin: true,
command: 'qwen',
description: 'Alibaba AI assistant'
description: 'Alibaba AI assistant',
tags: []
},
codex: {
enabled: true,
isBuiltin: true,
command: 'codex',
description: 'OpenAI code generation'
description: 'OpenAI code generation',
tags: []
},
claude: {
enabled: true,
isBuiltin: true,
command: 'claude',
description: 'Anthropic AI assistant'
description: 'Anthropic AI assistant',
tags: []
},
opencode: {
enabled: true,
isBuiltin: true,
command: 'opencode',
description: 'OpenCode AI assistant',
primaryModel: 'opencode/glm-4.7-free',
tags: []
}
},
customEndpoints: [],
customEndpoints: []
};
const DEFAULT_SETTINGS_CONFIG: ClaudeCliSettingsConfig = {
version: '1.0.0',
defaultTool: 'gemini',
settings: {
promptFormat: 'plain',
smartContext: {
enabled: false,
maxFiles: 10
},
nativeResume: true,
recursiveQuery: true,
cache: {
injectionMode: 'auto',
defaultPrefix: '',
defaultSuffix: ''
},
codeIndexMcp: 'codexlens' // Default to CodexLens
}
promptFormat: 'plain',
smartContext: {
enabled: false,
maxFiles: 10
},
nativeResume: true,
recursiveQuery: true,
cache: {
injectionMode: 'auto',
defaultPrefix: '',
defaultSuffix: ''
},
codeIndexMcp: 'ace'
};
// ========== Helper Functions ==========
@@ -101,10 +142,18 @@ function getProjectConfigPath(projectDir: string): string {
return path.join(projectDir, '.claude', 'cli-tools.json');
}
function getProjectSettingsPath(projectDir: string): string {
return path.join(projectDir, '.claude', 'cli-settings.json');
}
function getGlobalConfigPath(): string {
return path.join(os.homedir(), '.claude', 'cli-tools.json');
}
function getGlobalSettingsPath(): string {
return path.join(os.homedir(), '.claude', 'cli-settings.json');
}
/**
* Resolve config path with fallback:
* 1. Project: {projectDir}/.claude/cli-tools.json
@@ -125,6 +174,25 @@ function resolveConfigPath(projectDir: string): { path: string; source: 'project
return { path: projectPath, source: 'default' };
}
/**
* Resolve settings path with fallback:
* 1. Project: {projectDir}/.claude/cli-settings.json
* 2. Global: ~/.claude/cli-settings.json
*/
function resolveSettingsPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
const projectPath = getProjectSettingsPath(projectDir);
if (fs.existsSync(projectPath)) {
return { path: projectPath, source: 'project' };
}
const globalPath = getGlobalSettingsPath();
if (fs.existsSync(globalPath)) {
return { path: globalPath, source: 'global' };
}
return { path: projectPath, source: 'default' };
}
function ensureClaudeDir(projectDir: string): void {
const claudeDir = path.join(projectDir, '.claude');
if (!fs.existsSync(claudeDir)) {
@@ -134,6 +202,20 @@ function ensureClaudeDir(projectDir: string): void {
// ========== Main Functions ==========
/**
* Ensure tool has tags field (for backward compatibility)
*/
function ensureToolTags(tool: Partial<ClaudeCliTool>): ClaudeCliTool {
return {
enabled: tool.enabled ?? true,
isBuiltin: tool.isBuiltin ?? false,
command: tool.command ?? '',
description: tool.description ?? '',
primaryModel: tool.primaryModel,
tags: tool.tags ?? []
};
}
/**
* Load CLI tools configuration with fallback:
* 1. Project: {projectDir}/.claude/cli-tools.json
@@ -145,61 +227,115 @@ export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & {
try {
if (resolved.source === 'default') {
// No config file found, return defaults
return { ...DEFAULT_CONFIG, _source: 'default' };
return { ...DEFAULT_TOOLS_CONFIG, _source: 'default' };
}
const content = fs.readFileSync(resolved.path, 'utf-8');
const parsed = JSON.parse(content) as Partial<ClaudeCliToolsConfig>;
const parsed = JSON.parse(content) as Partial<ClaudeCliCombinedConfig>;
// Merge with defaults
const config = {
...DEFAULT_CONFIG,
...parsed,
tools: { ...DEFAULT_CONFIG.tools, ...(parsed.tools || {}) },
settings: {
...DEFAULT_CONFIG.settings,
...(parsed.settings || {}),
smartContext: {
...DEFAULT_CONFIG.settings.smartContext,
...(parsed.settings?.smartContext || {})
},
cache: {
...DEFAULT_CONFIG.settings.cache,
...(parsed.settings?.cache || {})
}
},
// Merge tools with defaults and ensure tags exist
const mergedTools: Record<string, ClaudeCliTool> = {};
for (const [key, tool] of Object.entries({ ...DEFAULT_TOOLS_CONFIG.tools, ...(parsed.tools || {}) })) {
mergedTools[key] = ensureToolTags(tool);
}
// Ensure customEndpoints have tags
const mergedEndpoints = (parsed.customEndpoints || []).map(ep => ({
...ep,
tags: ep.tags ?? []
}));
const config: ClaudeCliToolsConfig & { _source?: string } = {
version: parsed.version || DEFAULT_TOOLS_CONFIG.version,
tools: mergedTools,
customEndpoints: mergedEndpoints,
$schema: parsed.$schema,
_source: resolved.source
};
console.log(`[claude-cli-tools] Loaded config from ${resolved.source}: ${resolved.path}`);
console.log(`[claude-cli-tools] Loaded tools config from ${resolved.source}: ${resolved.path}`);
return config;
} catch (err) {
console.error('[claude-cli-tools] Error loading config:', err);
return { ...DEFAULT_CONFIG, _source: 'default' };
console.error('[claude-cli-tools] Error loading tools config:', err);
return { ...DEFAULT_TOOLS_CONFIG, _source: 'default' };
}
}
/**
* Save CLI tools configuration to project .claude/cli-tools.json
* Always saves to project directory (not global)
*/
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
ensureClaudeDir(projectDir);
const configPath = getProjectConfigPath(projectDir);
// Remove internal _source field before saving
const { _source, ...configToSave } = config;
try {
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
console.log(`[claude-cli-tools] Saved config to project: ${configPath}`);
console.log(`[claude-cli-tools] Saved tools config to: ${configPath}`);
} catch (err) {
console.error('[claude-cli-tools] Error saving config:', err);
console.error('[claude-cli-tools] Error saving tools config:', err);
throw new Error(`Failed to save CLI tools config: ${err}`);
}
}
/**
* Load CLI settings configuration with fallback:
* 1. Project: {projectDir}/.claude/cli-settings.json
* 2. Global: ~/.claude/cli-settings.json
* 3. Default settings
*/
export function loadClaudeCliSettings(projectDir: string): ClaudeCliSettingsConfig & { _source?: string } {
const resolved = resolveSettingsPath(projectDir);
try {
if (resolved.source === 'default') {
return { ...DEFAULT_SETTINGS_CONFIG, _source: 'default' };
}
const content = fs.readFileSync(resolved.path, 'utf-8');
const parsed = JSON.parse(content) as Partial<ClaudeCliSettingsConfig>;
const config: ClaudeCliSettingsConfig & { _source?: string } = {
...DEFAULT_SETTINGS_CONFIG,
...parsed,
smartContext: {
...DEFAULT_SETTINGS_CONFIG.smartContext,
...(parsed.smartContext || {})
},
cache: {
...DEFAULT_SETTINGS_CONFIG.cache,
...(parsed.cache || {})
},
_source: resolved.source
};
console.log(`[claude-cli-tools] Loaded settings from ${resolved.source}: ${resolved.path}`);
return config;
} catch (err) {
console.error('[claude-cli-tools] Error loading settings:', err);
return { ...DEFAULT_SETTINGS_CONFIG, _source: 'default' };
}
}
/**
* Save CLI settings configuration to project .claude/cli-settings.json
*/
export function saveClaudeCliSettings(projectDir: string, config: ClaudeCliSettingsConfig & { _source?: string }): void {
ensureClaudeDir(projectDir);
const settingsPath = getProjectSettingsPath(projectDir);
const { _source, ...configToSave } = config;
try {
fs.writeFileSync(settingsPath, JSON.stringify(configToSave, null, 2), 'utf-8');
console.log(`[claude-cli-tools] Saved settings to: ${settingsPath}`);
} catch (err) {
console.error('[claude-cli-tools] Error saving settings:', err);
throw new Error(`Failed to save CLI settings: ${err}`);
}
}
/**
* Update enabled status for a specific tool
*/
@@ -224,16 +360,16 @@ export function updateClaudeToolEnabled(
export function updateClaudeCacheSettings(
projectDir: string,
cacheSettings: Partial<ClaudeCacheSettings>
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
): ClaudeCliSettingsConfig {
const settings = loadClaudeCliSettings(projectDir);
config.settings.cache = {
...config.settings.cache,
settings.cache = {
...settings.cache,
...cacheSettings
};
saveClaudeCliTools(projectDir, config);
return config;
saveClaudeCliSettings(projectDir, settings);
return settings;
}
/**
@@ -242,11 +378,11 @@ export function updateClaudeCacheSettings(
export function updateClaudeDefaultTool(
projectDir: string,
defaultTool: string
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
config.defaultTool = defaultTool;
saveClaudeCliTools(projectDir, config);
return config;
): ClaudeCliSettingsConfig {
const settings = loadClaudeCliSettings(projectDir);
settings.defaultTool = defaultTool;
saveClaudeCliSettings(projectDir, settings);
return settings;
}
/**
@@ -254,16 +390,23 @@ export function updateClaudeDefaultTool(
*/
export function addClaudeCustomEndpoint(
projectDir: string,
endpoint: { id: string; name: string; enabled: boolean }
endpoint: { id: string; name: string; enabled: boolean; tags?: string[] }
): ClaudeCliToolsConfig {
const config = loadClaudeCliTools(projectDir);
const newEndpoint: ClaudeCustomEndpoint = {
id: endpoint.id,
name: endpoint.name,
enabled: endpoint.enabled,
tags: endpoint.tags || []
};
// Check if endpoint already exists
const existingIndex = config.customEndpoints.findIndex(e => e.id === endpoint.id);
if (existingIndex >= 0) {
config.customEndpoints[existingIndex] = endpoint;
config.customEndpoints[existingIndex] = newEndpoint;
} else {
config.customEndpoints.push(endpoint);
config.customEndpoints.push(newEndpoint);
}
saveClaudeCliTools(projectDir, config);
@@ -309,12 +452,12 @@ export function getClaudeCliToolsInfo(projectDir: string): {
export function updateCodeIndexMcp(
projectDir: string,
provider: 'codexlens' | 'ace' | 'none'
): { success: boolean; error?: string; config?: ClaudeCliToolsConfig } {
): { success: boolean; error?: string; settings?: ClaudeCliSettingsConfig } {
try {
// Update config
const config = loadClaudeCliTools(projectDir);
config.settings.codeIndexMcp = provider;
saveClaudeCliTools(projectDir, config);
// Update settings config
const settings = loadClaudeCliSettings(projectDir);
settings.codeIndexMcp = provider;
saveClaudeCliSettings(projectDir, settings);
// Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
const globalClaudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
@@ -358,7 +501,7 @@ export function updateCodeIndexMcp(
console.log(`[claude-cli-tools] Updated global CLAUDE.md to use ${provider}`);
}
return { success: true, config };
return { success: true, settings };
} catch (err) {
console.error('[claude-cli-tools] Error updating Code Index MCP:', err);
return { success: false, error: (err as Error).message };
@@ -369,8 +512,8 @@ export function updateCodeIndexMcp(
* Get current Code Index MCP provider
*/
export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
const config = loadClaudeCliTools(projectDir);
return config.settings.codeIndexMcp || 'codexlens';
const settings = loadClaudeCliSettings(projectDir);
return settings.codeIndexMcp || 'ace';
}
/**

View File

@@ -6,6 +6,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { StoragePaths, ensureStorageDir } from '../config/storage-paths.js';
import { loadClaudeCliTools, saveClaudeCliTools } from './claude-cli-tools.js';
// ========== Types ==========
@@ -234,6 +235,20 @@ export function updateToolConfig(
config.tools[tool] = updatedToolConfig;
saveCliConfig(baseDir, config);
// Also sync tags to cli-tools.json
if (updates.tags !== undefined) {
try {
const claudeCliTools = loadClaudeCliTools(baseDir);
if (claudeCliTools.tools[tool]) {
claudeCliTools.tools[tool].tags = updatedToolConfig.tags || [];
saveClaudeCliTools(baseDir, claudeCliTools);
}
} catch (err) {
// Log warning instead of ignoring errors syncing to cli-tools.json
console.warn(`[cli-config] Failed to sync tags to cli-tools.json for tool '${tool}'.`, err);
}
}
return updatedToolConfig;
}
@@ -298,14 +313,30 @@ export function getPredefinedModels(tool: string): string[] {
}
/**
* Get full config response for API (includes predefined models)
* Get full config response for API (includes predefined models and tags from cli-tools.json)
*/
export function getFullConfigResponse(baseDir: string): {
config: CliConfig;
predefinedModels: Record<string, string[]>;
} {
const config = loadCliConfig(baseDir);
// Merge tags from cli-tools.json
try {
const claudeCliTools = loadClaudeCliTools(baseDir);
for (const [toolName, toolConfig] of Object.entries(config.tools)) {
const claudeTool = claudeCliTools.tools[toolName];
if (claudeTool && claudeTool.tags) {
toolConfig.tags = claudeTool.tags;
}
}
} catch (err) {
// Log warning instead of ignoring errors loading cli-tools.json
console.warn('[cli-config] Could not merge tags from cli-tools.json.', err);
}
return {
config: loadCliConfig(baseDir),
config,
predefinedModels: { ...PREDEFINED_MODELS }
};
}