mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Add environment file support for CLI tools
- Introduced a new input group for environment file configuration in the dashboard CSS. - Updated hook manager to queue CLAUDE.md updates with configurable threshold and timeout. - Enhanced CLI manager view to include environment file input for built-in tools (gemini, qwen). - Implemented environment file loading mechanism in cli-executor-core, allowing custom environment variables. - Added unit tests for environment file parsing and loading functionalities. - Updated memory update queue to support dynamic configuration of threshold and timeout settings.
This commit is contained in:
@@ -304,6 +304,51 @@
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Environment File Input Group */
|
||||
.env-file-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.env-file-input-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.env-file-input-row .tool-config-input {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 0.8125rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.env-file-input-row .btn-sm {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.env-file-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.env-file-hint i {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-ghost.text-destructive:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
@@ -49,43 +49,17 @@ const HOOK_TEMPLATES = {
|
||||
description: 'Auto-update code index when files are written or edited',
|
||||
category: 'indexing'
|
||||
},
|
||||
'memory-update-related': {
|
||||
'memory-update-queue': {
|
||||
event: 'Stop',
|
||||
matcher: '',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"gemini\\",\\"strategy\\":\\"single-layer\\"}"'],
|
||||
description: 'Queue CLAUDE.md update for changed modules when session ends',
|
||||
args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\"}"'],
|
||||
description: 'Queue CLAUDE.md update when session ends (batched by threshold/timeout)',
|
||||
category: 'memory',
|
||||
configurable: true,
|
||||
config: {
|
||||
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
|
||||
strategy: { type: 'select', options: ['single-layer', 'multi-layer'], default: 'single-layer', label: 'Strategy' }
|
||||
}
|
||||
},
|
||||
'memory-update-periodic': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"gemini\\",\\"strategy\\":\\"single-layer\\"}"'],
|
||||
description: 'Queue CLAUDE.md update on file changes (batched with threshold/timeout)',
|
||||
category: 'memory',
|
||||
configurable: true,
|
||||
config: {
|
||||
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
|
||||
strategy: { type: 'select', options: ['single-layer', 'multi-layer'], default: 'single-layer', label: 'Strategy' }
|
||||
}
|
||||
},
|
||||
'memory-update-count-based': {
|
||||
event: 'PostToolUse',
|
||||
matcher: 'Write|Edit',
|
||||
command: 'bash',
|
||||
args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"gemini\\",\\"strategy\\":\\"single-layer\\"}"'],
|
||||
description: 'Queue CLAUDE.md update on file changes (auto-flush at 5 paths or 5min timeout)',
|
||||
category: 'memory',
|
||||
configurable: true,
|
||||
config: {
|
||||
tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' },
|
||||
strategy: { type: 'select', options: ['single-layer', 'multi-layer'], default: 'single-layer', label: 'Strategy' }
|
||||
threshold: { type: 'number', default: 5, min: 1, max: 20, label: 'Threshold (paths)', step: 1 },
|
||||
timeout: { type: 'number', default: 300, min: 60, max: 1800, label: 'Timeout (seconds)', step: 60 }
|
||||
}
|
||||
},
|
||||
// SKILL Context Loader templates
|
||||
@@ -210,33 +184,19 @@ const HOOK_TEMPLATES = {
|
||||
const WIZARD_TEMPLATES = {
|
||||
'memory-update': {
|
||||
name: 'Memory Update Hook',
|
||||
description: 'Automatically update CLAUDE.md documentation based on code changes',
|
||||
description: 'Queue-based CLAUDE.md updates with configurable threshold and timeout',
|
||||
icon: 'brain',
|
||||
options: [
|
||||
{
|
||||
id: 'on-stop',
|
||||
name: 'On Session End',
|
||||
description: 'Update documentation when Claude session ends',
|
||||
templateId: 'memory-update-related'
|
||||
},
|
||||
{
|
||||
id: 'periodic',
|
||||
name: 'Periodic Update',
|
||||
description: 'Update documentation at regular intervals during session',
|
||||
templateId: 'memory-update-periodic'
|
||||
},
|
||||
{
|
||||
id: 'count-based',
|
||||
name: 'Count-Based Update',
|
||||
description: 'Update documentation when file changes reach threshold',
|
||||
templateId: 'memory-update-count-based'
|
||||
id: 'queue',
|
||||
name: 'Queue-Based Update',
|
||||
description: 'Batch updates when threshold reached or timeout expires',
|
||||
templateId: 'memory-update-queue'
|
||||
}
|
||||
],
|
||||
configFields: [
|
||||
{ key: 'tool', type: 'select', label: 'CLI Tool', options: ['gemini', 'qwen', 'codex'], default: 'gemini', description: 'Tool for documentation generation' },
|
||||
{ key: 'interval', type: 'number', label: 'Interval (seconds)', default: 300, min: 60, max: 3600, step: 60, showFor: ['periodic'], description: 'Time between updates' },
|
||||
{ key: 'threshold', type: 'number', label: 'File Count Threshold', default: 10, min: 3, max: 50, step: 1, showFor: ['count-based'], description: 'Number of file changes to trigger update' },
|
||||
{ key: 'strategy', type: 'select', label: 'Update Strategy', options: ['related', 'single-layer'], default: 'related', description: 'Related: changed modules, Single-layer: current directory' }
|
||||
{ key: 'threshold', type: 'number', label: 'Threshold (paths)', default: 5, min: 1, max: 20, step: 1, description: 'Number of paths to trigger batch update' },
|
||||
{ key: 'timeout', type: 'number', label: 'Timeout (seconds)', default: 300, min: 60, max: 1800, step: 60, description: 'Auto-flush queue after this time' }
|
||||
]
|
||||
},
|
||||
'skill-context': {
|
||||
@@ -730,9 +690,7 @@ function renderWizardModalContent() {
|
||||
// Helper to get translated option names
|
||||
const getOptionName = (optId) => {
|
||||
if (wizardId === 'memory-update') {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEnd');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdate');
|
||||
if (optId === 'count-based') return t('hook.wizard.countBasedUpdate');
|
||||
if (optId === 'queue') return t('hook.wizard.queueBasedUpdate') || 'Queue-Based Update';
|
||||
}
|
||||
if (wizardId === 'memory-setup') {
|
||||
if (optId === 'file-read') return t('hook.wizard.fileReadTracker');
|
||||
@@ -748,9 +706,7 @@ function renderWizardModalContent() {
|
||||
|
||||
const getOptionDesc = (optId) => {
|
||||
if (wizardId === 'memory-update') {
|
||||
if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc');
|
||||
if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc');
|
||||
if (optId === 'count-based') return t('hook.wizard.countBasedUpdateDesc');
|
||||
if (optId === 'queue') return t('hook.wizard.queueBasedUpdateDesc') || 'Batch updates when threshold reached or timeout expires';
|
||||
}
|
||||
if (wizardId === 'memory-setup') {
|
||||
if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc');
|
||||
@@ -767,20 +723,16 @@ function renderWizardModalContent() {
|
||||
// Helper to get translated field labels
|
||||
const getFieldLabel = (fieldKey) => {
|
||||
const labels = {
|
||||
'tool': t('hook.wizard.cliTool'),
|
||||
'interval': t('hook.wizard.intervalSeconds'),
|
||||
'threshold': t('hook.wizard.fileCountThreshold'),
|
||||
'strategy': t('hook.wizard.updateStrategy')
|
||||
'threshold': t('hook.wizard.thresholdPaths') || 'Threshold (paths)',
|
||||
'timeout': t('hook.wizard.timeoutSeconds') || 'Timeout (seconds)'
|
||||
};
|
||||
return labels[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.label || fieldKey;
|
||||
};
|
||||
|
||||
const getFieldDesc = (fieldKey) => {
|
||||
const descs = {
|
||||
'tool': t('hook.wizard.toolForDocGen'),
|
||||
'interval': t('hook.wizard.timeBetweenUpdates'),
|
||||
'threshold': t('hook.wizard.fileCountThresholdDesc'),
|
||||
'strategy': t('hook.wizard.relatedStrategy')
|
||||
'threshold': t('hook.wizard.thresholdPathsDesc') || 'Number of paths to trigger batch update',
|
||||
'timeout': t('hook.wizard.timeoutSecondsDesc') || 'Auto-flush queue after this time'
|
||||
};
|
||||
return descs[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.description || '';
|
||||
};
|
||||
@@ -1154,16 +1106,9 @@ function generateWizardCommand() {
|
||||
}
|
||||
|
||||
// Handle memory-update wizard (default)
|
||||
// Now uses memory_queue for batched updates
|
||||
const tool = wizardConfig.tool || 'gemini';
|
||||
const strategy = wizardConfig.strategy || 'single-layer';
|
||||
|
||||
// Build the ccw tool command using memory_queue
|
||||
// Use double quotes to allow bash $CLAUDE_PROJECT_DIR expansion
|
||||
const params = `"{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"${tool}\\",\\"strategy\\":\\"${strategy}\\"}"`;
|
||||
|
||||
// All trigger types now use the same queue-based command
|
||||
// The queue handles batching (threshold: 5 paths, timeout: 5 min)
|
||||
// Now uses memory_queue for batched updates with configurable threshold/timeout
|
||||
// The command adds to queue, configuration is applied separately via submitHookWizard
|
||||
const params = `"{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\"}"`;
|
||||
return `ccw tool exec memory_queue ${params}`;
|
||||
}
|
||||
|
||||
@@ -1259,6 +1204,26 @@ async function submitHookWizard() {
|
||||
}
|
||||
|
||||
await saveHook(scope, baseTemplate.event, hookData);
|
||||
|
||||
// For memory-update wizard, also configure queue settings
|
||||
if (wizard.id === 'memory-update') {
|
||||
const threshold = wizardConfig.threshold || 5;
|
||||
const timeout = wizardConfig.timeout || 300;
|
||||
try {
|
||||
const configParams = JSON.stringify({ action: 'configure', threshold, timeout });
|
||||
const response = await fetch('/api/tools/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tool: 'memory_queue', params: configParams })
|
||||
});
|
||||
if (response.ok) {
|
||||
showRefreshToast(`Queue configured: threshold=${threshold}, timeout=${timeout}s`, 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to configure memory queue:', e);
|
||||
}
|
||||
}
|
||||
|
||||
closeHookWizardModal();
|
||||
}
|
||||
|
||||
|
||||
@@ -261,6 +261,13 @@ const i18n = {
|
||||
'cli.wrapper': 'Wrapper',
|
||||
'cli.customClaudeSettings': 'Custom Claude CLI settings',
|
||||
'cli.updateFailed': 'Failed to update',
|
||||
|
||||
// CLI Tool Config - Environment File
|
||||
'cli.envFile': 'Environment File',
|
||||
'cli.envFileOptional': '(optional)',
|
||||
'cli.envFilePlaceholder': 'Path to .env file (e.g., ~/.gemini-env or C:/Users/xxx/.env)',
|
||||
'cli.envFileHint': 'Load environment variables (e.g., API keys) before CLI execution. Supports ~ for home directory.',
|
||||
'cli.envFileBrowse': 'Browse',
|
||||
|
||||
// CodexLens Configuration
|
||||
'codexlens.config': 'CodexLens Configuration',
|
||||
@@ -2421,6 +2428,13 @@ const i18n = {
|
||||
'cli.wrapper': '封装',
|
||||
'cli.customClaudeSettings': '自定义 Claude CLI 配置',
|
||||
'cli.updateFailed': '更新失败',
|
||||
|
||||
// CLI 工具配置 - 环境文件
|
||||
'cli.envFile': '环境文件',
|
||||
'cli.envFileOptional': '(可选)',
|
||||
'cli.envFilePlaceholder': '.env 文件路径(如 ~/.gemini-env 或 C:/Users/xxx/.env)',
|
||||
'cli.envFileHint': '在 CLI 执行前加载环境变量(如 API 密钥)。支持 ~ 表示用户目录。',
|
||||
'cli.envFileBrowse': '浏览',
|
||||
|
||||
// CodexLens 配置
|
||||
'codexlens.config': 'CodexLens 配置',
|
||||
|
||||
@@ -523,6 +523,27 @@ function buildToolConfigModalContent(tool, config, models, status) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Environment File Section (only for builtin tools: gemini, qwen)
|
||||
(tool === 'gemini' || tool === 'qwen' ? (
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4><i data-lucide="file-key" class="w-3.5 h-3.5"></i> ' + t('cli.envFile') + ' <span class="text-muted">' + t('cli.envFileOptional') + '</span></h4>' +
|
||||
'<div class="env-file-input-group">' +
|
||||
'<div class="env-file-input-row">' +
|
||||
'<input type="text" id="envFileInput" class="tool-config-input" ' +
|
||||
'placeholder="' + t('cli.envFilePlaceholder') + '" ' +
|
||||
'value="' + (config.envFile ? escapeHtml(config.envFile) : '') + '" />' +
|
||||
'<button type="button" class="btn-sm btn-outline" id="envFileBrowseBtn">' +
|
||||
'<i data-lucide="folder-open" class="w-3.5 h-3.5"></i> ' + t('cli.envFileBrowse') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<p class="env-file-hint">' +
|
||||
'<i data-lucide="info" class="w-3 h-3"></i> ' +
|
||||
t('cli.envFileHint') +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
) : '') +
|
||||
|
||||
// Footer
|
||||
'<div class="tool-config-footer">' +
|
||||
'<button class="btn btn-outline" onclick="closeModal()">' + t('common.cancel') + '</button>' +
|
||||
@@ -701,12 +722,23 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get envFile value (only for gemini/qwen)
|
||||
var envFileInput = document.getElementById('envFileInput');
|
||||
var envFile = envFileInput ? envFileInput.value.trim() : '';
|
||||
|
||||
try {
|
||||
await updateCliToolConfig(tool, {
|
||||
var updateData = {
|
||||
primaryModel: primaryModel,
|
||||
secondaryModel: secondaryModel,
|
||||
tags: currentTags
|
||||
});
|
||||
};
|
||||
|
||||
// Only include envFile for gemini/qwen tools
|
||||
if (tool === 'gemini' || tool === 'qwen') {
|
||||
updateData.envFile = envFile || null;
|
||||
}
|
||||
|
||||
await updateCliToolConfig(tool, updateData);
|
||||
// Reload config to reflect changes
|
||||
await loadCliToolConfig();
|
||||
showRefreshToast('Configuration saved', 'success');
|
||||
@@ -719,6 +751,44 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
};
|
||||
}
|
||||
|
||||
// Environment file browse button (only for gemini/qwen)
|
||||
var envFileBrowseBtn = document.getElementById('envFileBrowseBtn');
|
||||
if (envFileBrowseBtn) {
|
||||
envFileBrowseBtn.onclick = async function() {
|
||||
try {
|
||||
// Use file dialog API if available
|
||||
var response = await fetch('/api/dialog/open-file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: t('cli.envFile'),
|
||||
filters: [
|
||||
{ name: 'Environment Files', extensions: ['env'] },
|
||||
{ name: 'All Files', extensions: ['*'] }
|
||||
],
|
||||
defaultPath: ''
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
var data = await response.json();
|
||||
if (data.filePath) {
|
||||
var envFileInput = document.getElementById('envFileInput');
|
||||
if (envFileInput) {
|
||||
envFileInput.value = data.filePath;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: prompt user to enter path manually
|
||||
showRefreshToast('File dialog not available. Please enter path manually.', 'info');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to open file dialog:', err);
|
||||
showRefreshToast('File dialog not available. Please enter path manually.', 'info');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize lucide icons in modal
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user